chromium/chrome/android/java/src/org/chromium/chrome/browser/quickactionsearchwidget/QuickActionSearchWidgetProvider.java

// Copyright 2021 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.quickactionsearchwidget;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.SizeF;
import android.widget.RemoteViews;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.browserservices.intents.WebappConstants;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.searchwidget.SearchActivityClientImpl;
import org.chromium.chrome.browser.ui.quickactionsearchwidget.QuickActionSearchWidgetProviderDelegate;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityPreferencesManager;
import org.chromium.chrome.browser.ui.searchactivityutils.SearchActivityPreferencesManager.SearchActivityPreferences;
import org.chromium.components.embedder_support.util.UrlConstants;

import java.util.ArrayList;
import java.util.Map;

/**
 * {@link AppWidgetProvider} for a widget that provides an entry point for users to quickly perform
 * actions in Chrome.
 */
public abstract class QuickActionSearchWidgetProvider extends AppWidgetProvider {
    /**
     * A sub class of {@link QuickActionSearchWidgetProvider} that provides the widget that can
     * resize.
     */
    public static class QuickActionSearchWidgetProviderSearch
            extends QuickActionSearchWidgetProvider {
        @Override
        @NonNull
        RemoteViews createWidget(
                @NonNull Context context,
                @NonNull SearchActivityPreferences prefs,
                int areaWidthDp,
                int areaHeightDp) {
            return getDelegate()
                    .createSearchWidgetRemoteViews(
                            context,
                            new SearchActivityClientImpl(),
                            prefs,
                            areaWidthDp,
                            areaHeightDp);
        }
    }

    /** Returns the widget area width in portrait orientation (dp). */
    private static int getPortraitModeTargetAreaWidth(Bundle options) {
        return options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
    }

    /** Returns the widget area height in portrait orientation (dp). */
    private static int getPortraitModeTargetAreaHeight(Bundle options) {
        return options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT);
    }

    /** Returns the widget area width in landscape orientation (dp). */
    private static int getLandscapeModeTargetAreaWidth(Bundle options) {
        return options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH);
    }

    /** Returns the widget area height in landscape orientation (dp). */
    private static int getLandscapeModeTargetAreaHeight(Bundle options) {
        return options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
    }

    /**
     * A sub class of {@link QuickActionSearchWidgetProvider} that provides the widget that only
     * contains a touch surface for launching the Dino game.
     */
    public static class QuickActionSearchWidgetProviderDino
            extends QuickActionSearchWidgetProvider {
        @Override
        @NonNull
        RemoteViews createWidget(
                @NonNull Context context,
                @NonNull SearchActivityPreferences prefs,
                int areaWidthDp,
                int areaHeightDp) {
            return getDelegate()
                    .createDinoWidgetRemoteViews(
                            context,
                            new SearchActivityClientImpl(),
                            prefs,
                            areaWidthDp,
                            areaHeightDp);
        }
    }

    private static @Nullable QuickActionSearchWidgetProviderDelegate sDelegate;

    @Override
    public void onUpdate(
            @NonNull Context context,
            @NonNull AppWidgetManager manager,
            @Nullable int[] widgetIds) {
        updateWidgets(context, manager, SearchActivityPreferencesManager.getCurrent(), widgetIds);
    }

    @Override
    public void onAppWidgetOptionsChanged(
            Context context, AppWidgetManager manager, int widgetId, Bundle newOptions) {
        super.onAppWidgetOptionsChanged(context, manager, widgetId, newOptions);
        onUpdate(context, manager, new int[] {widgetId});
    }

    /**
     * Apply update to widgets, reflecting feature availability on the widget surface.
     *
     * @param context Current context.
     * @param manager Widget manager.
     * @param preferences Search Activity preferences.
     * @param widgetIds List of Widget IDs that should be updated.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    void updateWidgets(
            @NonNull Context context,
            @NonNull AppWidgetManager manager,
            @NonNull SearchActivityPreferences preferences,
            @NonNull int[] widgetIds) {
        if (widgetIds == null) {
            // Query all widgets associated with this component.
            widgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass().getName()));
        }

        for (int index = 0; index < widgetIds.length; index++) {
            int widgetId = widgetIds[index];
            Bundle options = manager.getAppWidgetOptions(widgetId);
            manager.updateAppWidget(widgetId, getRemoteViews(context, preferences, options));
        }
    }

    /** Get (create if necessary) an instance of QuickActionSearchWidgetProviderDelegate. */
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    protected @NonNull QuickActionSearchWidgetProviderDelegate getDelegate() {
        if (sDelegate != null) return sDelegate;

        Context context = ContextUtils.getApplicationContext();
        Intent trustedIncognitoIntent =
                IntentHandler.createTrustedOpenNewTabIntent(context, /* incognito= */ true);
        trustedIncognitoIntent.putExtra(IntentHandler.EXTRA_INVOKED_FROM_APP_WIDGET, true);
        trustedIncognitoIntent.addFlags(
                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
        Intent dinoIntent = createDinoIntent(context);

        sDelegate =
                new QuickActionSearchWidgetProviderDelegate(
                        context, trustedIncognitoIntent, dinoIntent);
        return sDelegate;
    }

    /**
     * Creates an intent to launch a new tab with chrome://dino/ URL.
     *
     * @param context The context from which the intent is being created.
     * @return An intent to launch a tab with a new tab with chrome://dino/ URL.
     */
    private static Intent createDinoIntent(final Context context) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(UrlConstants.CHROME_DINO_URL));
        intent.setComponent(new ComponentName(context, ChromeLauncherActivity.class));
        intent.putExtra(WebappConstants.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, true);
        intent.putExtra(IntentHandler.EXTRA_INVOKED_FROM_APP_WIDGET, true);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
        IntentUtils.addTrustedIntentExtras(intent);

        return intent;
    }

    /**
     * Construct the widget for specific dimensions.
     *
     * @param context Current context.
     * @param prefs Widget settings and feature availability.
     * @param areaWidthDp The width of the widget area, expressed in Dp.
     * @param areaHeightDp The height of the widget area, expressed in Dp.
     * @return RemoteViews description for a single widget layout.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    abstract @NonNull RemoteViews createWidget(
            @NonNull Context context,
            @NonNull SearchActivityPreferences prefs,
            int areaWidthDp,
            int areaHeightDp);

    /**
     * Acquire the RemoteViews that represent the widget.
     *
     * @param context Current context.
     * @param prefs Widget settings and feature availability.
     * @param options Options bundle passed by AppWidgetManager.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    @NonNull
    RemoteViews getRemoteViews(
            @NonNull Context context,
            @NonNull SearchActivityPreferences prefs,
            @NonNull Bundle options) {
        var views = getSizeMappedRemoteViews(context, prefs, options);
        if (views != null) {
            return views;
        }
        return getOrientationSpecificRemoteViews(context, prefs, options);
    }

    /**
     * Acquire screen orientation specific layouts that will be applied to the widget.
     *
     * @param context Current context.
     * @param prefs Widget settings and feature availability.
     * @param options Widget parameters passed by the AppWidgetManager.
     * @return RemoteViews describing widget for landscape and portrait screen orientations.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    @NonNull
    RemoteViews getOrientationSpecificRemoteViews(
            @NonNull Context context,
            @NonNull SearchActivityPreferences prefs,
            @NonNull Bundle options) {
        var portraitViews =
                createWidget(
                        context,
                        prefs,
                        getPortraitModeTargetAreaWidth(options),
                        getPortraitModeTargetAreaHeight(options));

        var landscapeViews =
                createWidget(
                        context,
                        prefs,
                        getLandscapeModeTargetAreaWidth(options),
                        getLandscapeModeTargetAreaHeight(options));

        return new RemoteViews(landscapeViews, portraitViews);
    }

    /**
     * Acquire size-specific layouts that will be applied to the widget.
     *
     * @param context Current context.
     * @param prefs Widget settings and feature availability.
     * @param options Widget parameters passed by the AppWidgetManager.
     * @return RemoteViews describing widget for all sizes requested by the AppWidgetManager, or
     *     null, if the AppWidgetManager did not specify the sizes.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    @Nullable
    RemoteViews getSizeMappedRemoteViews(
            @NonNull Context context,
            @NonNull SearchActivityPreferences prefs,
            @NonNull Bundle options) {
        // On Android S and above, attempt to build widget from supplied array of sizes.
        // This is reserved to Android S because appropriate RemoteViews constructor may not be
        // available.
        // Note that the creation may still fail, if the launcher is unable to offer appropriate
        // details.
        // Check for supported system version.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            return null;
        }

        ArrayList<SizeF> sizes =
                options.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES);
        if (sizes == null || sizes.isEmpty()) {
            return null;
        }
        Map<SizeF, RemoteViews> mappings = new ArrayMap<>();

        for (var size : sizes) {
            mappings.put(
                    size,
                    createWidget(context, prefs, (int) size.getWidth(), (int) size.getHeight()));
        }
        return new RemoteViews(mappings);
    }

    /**
     * This function initializes the QuickActionSearchWidgetProvider component. Namely, this
     * function enables the component for users who have the QUICK_ACTION_SEARCH_WIDGET flag
     * enabled.
     *
     * <p>Note that due to b/189087746, the widget cannot be disabled be default, as a result, we
     * must enable/disable the widget programmatically here.
     *
     * <p>This function is expected to be called exactly once after native libraries are
     * initialized.
     */
    public static void initialize() {
        QuickActionSearchWidgetProvider dinoWidget = new QuickActionSearchWidgetProviderDino();
        QuickActionSearchWidgetProvider smallWidget = new QuickActionSearchWidgetProviderSearch();

        PostTask.postTask(
                TaskTraits.BEST_EFFORT,
                () -> {
                    // Make the Widget available to all Chrome users who participated in an
                    // experiment in the past. This can trigger disk access. Unfortunately,
                    // we need to keep it for a little bit longer -- see:
                    // https://crbug.com/1309116
                    setWidgetEnabled(true, true);
                });

        SearchActivityPreferencesManager.addObserver(
                prefs -> {
                    Context context = ContextUtils.getApplicationContext();
                    if (context == null) return;
                    AppWidgetManager manager = AppWidgetManager.getInstance(context);
                    if (manager == null) {
                        // The device does not support widgets. Abort.
                        return;
                    }
                    dinoWidget.updateWidgets(context, manager, prefs, null);
                    smallWidget.updateWidgets(context, manager, prefs, null);
                });
    }

    /**
     * Enables/Disables the widget component. If the widget is disabled, it will not appear in the
     * widget picker, and users will not be able to add the widget.
     *
     * @param shouldEnableQuickActionSearchWidget a boolean indicating whether the widget component
     *     should be enabled or not.
     * @param shouldEnableDinoVariant a boolean indicating whether the widget component of the Dino
     *     variant should be enabled.
     */
    private static void setWidgetEnabled(
            boolean shouldEnableQuickActionSearchWidget, boolean shouldEnableDinoVariant) {
        setWidgetComponentEnabled(
                QuickActionSearchWidgetProviderSearch.class, shouldEnableQuickActionSearchWidget);
        setWidgetComponentEnabled(
                QuickActionSearchWidgetProviderDino.class, shouldEnableDinoVariant);
    }

    /**
     * Enables/Disables the given widget component for a variation of the Quick Action Search
     * Widget.
     *
     * @param component The {@link QuickActionSearchWidgetProvider} subclass corresponding to the
     *     widget that is to be disabled.
     * @param shouldEnableWidgetComponent a boolean indicating whether the widget component should
     *     be enabled or not.
     */
    private static void setWidgetComponentEnabled(
            @NonNull Class<? extends QuickActionSearchWidgetProvider> component,
            boolean shouldEnableWidgetComponent) {
        // The initialization must be performed on a background thread because the following logic
        // can trigger disk access. The PostTask in ProcessInitializationHandler can be removed once
        // the experimentation phase is over.
        ThreadUtils.assertOnBackgroundThread();
        Context context = ContextUtils.getApplicationContext();

        int componentEnabledState =
                shouldEnableWidgetComponent
                        ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;

        ComponentName componentName = new ComponentName(context, component);
        context.getPackageManager()
                .setComponentEnabledSetting(
                        componentName, componentEnabledState, PackageManager.DONT_KILL_APP);
    }
}