chromium/chrome/android/java/src/org/chromium/chrome/browser/webapps/PwaRestorePromoUtils.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.webapps;

import android.app.Activity;
import android.graphics.Bitmap;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import org.jni_zero.JNINamespace;

import org.chromium.base.Log;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.components.webapps.pwa_restore_ui.PwaRestoreBottomSheetCoordinator;
import org.chromium.ui.base.WindowAndroid;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

/**
 * This class is responsible for coordinating the showing of the PWA Restore promo (which aims to
 * remind users that they had PWAs installed on their old device, and can restore them on their new
 * device.
 */
@JNINamespace("webapps")
public class PwaRestorePromoUtils {
    private static final String TAG = "PwaRestore";

    /** Used to determine at what stage in the display process this promo is. */
    @IntDef({
        DisplayStage.UNKNOWN_STATUS,
        DisplayStage.SHOW_PROMO,
        DisplayStage.ALREADY_LAUNCHED,
        DisplayStage.NO_APPS_AVAILABLE,
        DisplayStage.PRE_EXISTING_PROFILE,
        DisplayStage.ERROR_LAUNCHING_PROMO,
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface DisplayStage {
        int UNKNOWN_STATUS = 0;
        int SHOW_PROMO = 1;
        int ALREADY_LAUNCHED = 2;
        int NO_APPS_AVAILABLE = 3;
        int PRE_EXISTING_PROFILE = 4;
        int ERROR_LAUNCHING_PROMO = 5;
    }

    public static void notifyFirstRunPromoTriggered() {
        Log.i(TAG, "First Run Promo has been triggered, setting flag to show on next launch.");
        SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
        // Promos are not shown alongside the First Run Experience, so write a flag to trigger the
        // promo to show during next launch.
        preferenceManager.writeInt(
                ChromePreferenceKeys.PWA_RESTORE_PROMO_STAGE, DisplayStage.SHOW_PROMO);
    }

    public static boolean maybeForceShowPromo(Profile profile, WindowAndroid windowAndroid) {
        if (ChromeFeatureList.isEnabled(ChromeFeatureList.PWA_RESTORE_UI_AT_STARTUP)) {
            Log.i(TAG, "Force showing PWA Restore promo at startup, feature flag is enabled.");
            launchPromo(profile, windowAndroid);
            return true;
        }
        return false;
    }

    /**
     * Launch the PWA Restore promotion, if we've determined that this launch meets the criteria for
     * for showing it. The promo is intended to show as soon as possible after the user has switched
     * to a new device. It keeps track of when the first-run experience has been triggered (see
     * `notifyFirstRunPromoTriggered`) and launches when the flag is seen, which normally is the
     * next launch _after_ the first run experience launch.
     *
     * @param activity The current {@link Activity} to use for this promo.
     * @param windowAndroid The current {@link WindowAndroid} to use for this promo.
     * @param arrowResourceId The resource id for the Back arrow to use.
     * @return Whether the PWA Restore promo was shown.
     */
    public static boolean launchPromoIfNeeded(Profile profile, WindowAndroid windowAndroid) {
        if (!ChromeFeatureList.isEnabled(ChromeFeatureList.PWA_RESTORE_UI)) {
            Log.i(TAG, "Not showing PWA Restore promo, feature flag is disabled.");
            return false;
        }

        SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
        // TODO(finnur): Change the default to be false when the backend writes this pref.
        if (!preferenceManager.readBoolean(ChromePreferenceKeys.PWA_RESTORE_APPS_AVAILABLE, true)) {
            Log.i(TAG, "Not showing PWA Restore promo, no apps available for restoring.");
            preferenceManager.writeInt(
                    ChromePreferenceKeys.PWA_RESTORE_PROMO_STAGE, DisplayStage.NO_APPS_AVAILABLE);
            return false;
        }

        @DisplayStage
        int promoStage =
                preferenceManager.readInt(
                        ChromePreferenceKeys.PWA_RESTORE_PROMO_STAGE, DisplayStage.UNKNOWN_STATUS);
        Log.i(TAG, "Currently saved promo stage: " + promoStage + ".");

        switch (promoStage) {
            case DisplayStage.UNKNOWN_STATUS:
                // For most users, the current launch is not the initial or second startup, and
                // therefore nothing has been written as the DisplayStage for this promo (because
                // `notifyFirstRunPromoTriggered` has not been called). The promo will therefore
                // write PRE_EXISTING_PROFILE to the DisplayStage to prevent the promo from showing
                // now and in the future.
                Log.i(TAG, "Promo stage is unknown - marking as existing profile.");
                preferenceManager.writeInt(
                        ChromePreferenceKeys.PWA_RESTORE_PROMO_STAGE,
                        DisplayStage.PRE_EXISTING_PROFILE);
                return false;
            case DisplayStage.PRE_EXISTING_PROFILE:
                // This prevents the promo from showing in the future.
                Log.i(TAG, "Not showing promo - old profile.");
                return false;
            case DisplayStage.ALREADY_LAUNCHED:
                // If the promo has already launched, our work is done.
                Log.i(TAG, "Not showing promo - promo has already launched.");
                return false;
            case DisplayStage.NO_APPS_AVAILABLE:
                // If the promo has already launched, our work is done.
                Log.i(TAG, "Not showing promo - no apps available.");
                return false;
            case DisplayStage.ERROR_LAUNCHING_PROMO:
                // Last time we launched there was an error. For now, don't launch again.
                Log.i(TAG, "Not showing promo - prior error.");
                return false;
            case DisplayStage.SHOW_PROMO:
                // We've determined that the promo should show. If successfully shown, we'll mark
                // it as such, to prevent the promo from appearing in the future.
                launchPromo(profile, windowAndroid);
                return true;
            default:
                assert false;
                return false;
        }
    }

    private static void launchPromo(Profile profile, WindowAndroid windowAndroid) {
        WebApkSyncService.fetchRestorableApps(
                profile,
                (success, appIds, names, lastUsedInDays, icons) -> {
                    onRestorableAppsAvailable(
                            success,
                            appIds,
                            names,
                            lastUsedInDays,
                            icons,
                            windowAndroid,
                            R.drawable.ic_arrow_back_24dp);
                });
    }

    private static void onRestorableAppsAvailable(
            boolean success,
            @NonNull String[] appIds,
            @NonNull String[] appNames,
            @NonNull int[] lastUsedInDays,
            @NonNull List<Bitmap> icons,
            WindowAndroid windowAndroid,
            int arrowResourceId) {
        BottomSheetController controller = BottomSheetControllerProvider.from(windowAndroid);
        if (controller == null) {
            success = false;
        } else {
            Activity activity = windowAndroid.getActivity().get();
            PwaRestoreBottomSheetCoordinator pwaRestoreBottomSheetCoordinator =
                    new PwaRestoreBottomSheetCoordinator(
                            appIds,
                            appNames,
                            icons,
                            lastUsedInDays,
                            activity,
                            controller,
                            arrowResourceId);
            if (pwaRestoreBottomSheetCoordinator == null
                    || !pwaRestoreBottomSheetCoordinator.show()) {
                success = false;
            }
        }

        SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
        if (success) {
            preferenceManager.writeInt(
                    ChromePreferenceKeys.PWA_RESTORE_PROMO_STAGE, DisplayStage.ALREADY_LAUNCHED);
        } else {
            preferenceManager.writeInt(
                    ChromePreferenceKeys.PWA_RESTORE_PROMO_STAGE,
                    DisplayStage.ERROR_LAUNCHING_PROMO);
        }
      }
}