chromium/components/webapps/browser/android/java/src/org/chromium/components/webapps/WebappsUtils.java

// Copyright 2020 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.components.webapps;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;

import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;

import org.jni_zero.CalledByNative;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.components.webapk.lib.client.WebApkValidator;
import org.chromium.ui.widget.Toast;

import java.util.List;

/** Contains utilities for Web Apps and homescreen shortcuts. */
public class WebappsUtils {
    private static final String TAG = "WebappsUtils";

    private static final String INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT";

    // True when Android O's ShortcutManager.requestPinShortcut() is supported.
    private static volatile boolean sIsRequestPinShortcutSupported;

    // True when it is already checked if ShortcutManager.requestPinShortcut() is supported.
    private static volatile boolean sCheckedIfRequestPinShortcutSupported;

    // Synchronization locks for thread-safe access to variables
    // sCheckedIfRequestPinShortcutSupported and sIsRequestPinShortcutSupported.
    private static final Object sLock = new Object();

    /**
     * Creates an intent that will add a shortcut to the home screen.
     *
     * @param title Title of the shortcut.
     * @param icon Image that represents the shortcut.
     * @param shortcutIntent Intent to fire when the shortcut is activated.
     * @return Intent for the shortcut.
     */
    public static Intent createAddToHomeIntent(String title, Bitmap icon, Intent shortcutIntent) {
        Intent i = new Intent(INSTALL_SHORTCUT);
        i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
        i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
        i.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
        return i;
    }

    /**
     * Request Android to add a shortcut to the home screen.
     *
     * @param id The generated GUID of the shortcut.
     * @param title Title of the shortcut.
     * @param icon Image that represents the shortcut.
     * @param isIconAdaptive Whether to create an Android Adaptive icon.
     * @param shortcutIntent Intent to fire when the shortcut is activated.
     */
    public static void addShortcutToHomescreen(
            String id, String title, Bitmap icon, boolean isIconAdaptive, Intent shortcutIntent) {
        if (isRequestPinShortcutSupported()) {
            addShortcutWithShortcutManager(id, title, icon, isIconAdaptive, shortcutIntent);
            return;
        }

        Intent intent = createAddToHomeIntent(title, icon, shortcutIntent);
        ContextUtils.getApplicationContext().sendBroadcast(intent);
        showAddedToHomescreenToast(title);
    }

    @RequiresApi(Build.VERSION_CODES.O)
    public static void addShortcutWithShortcutManager(
            String id, String title, Bitmap bitmap, boolean isMaskableIcon, Intent shortcutIntent) {
        Context context = ContextUtils.getApplicationContext();

        if (bitmap == null) {
            Log.e(TAG, "Failed to find an icon for " + title + ", not adding.");
            return;
        }
        Icon icon =
                isMaskableIcon
                        ? Icon.createWithAdaptiveBitmap(bitmap)
                        : Icon.createWithBitmap(bitmap);

        ShortcutInfo shortcutInfo =
                new ShortcutInfo.Builder(context, id)
                        .setShortLabel(title)
                        .setLongLabel(title)
                        .setIcon(icon)
                        .setIntent(shortcutIntent)
                        .build();
        try {
            ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
            shortcutManager.requestPinShortcut(shortcutInfo, null);
        } catch (IllegalStateException e) {
            Log.d(
                    TAG,
                    "Could not create pinned shortcut: device is locked, or "
                            + "activity is backgrounded.");
        }
    }

    /** Show toast to alert user that the shortcut was added to the home screen. */
    private static void showAddedToHomescreenToast(final String title) {
        Context applicationContext = ContextUtils.getApplicationContext();
        String toastText = applicationContext.getString(R.string.added_to_homescreen, title);
        showToast(toastText);
    }

    public static void showToast(String text) {
        assert ThreadUtils.runningOnUiThread();
        Toast toast =
                Toast.makeText(ContextUtils.getApplicationContext(), text, Toast.LENGTH_SHORT);
        toast.show();
    }

    /**
     * Shows toast notifying user of the result of a WebAPK install if the installation was not
     * successful.
     */
    @SuppressWarnings("unused")
    @CalledByNative
    private static void showWebApkInstallResultToast(@WebApkInstallResult int result) {
        Context applicationContext = ContextUtils.getApplicationContext();
        if (result == WebApkInstallResult.INSTALL_ALREADY_IN_PROGRESS) {
            showToast(applicationContext.getString(R.string.webapk_install_in_progress));
        } else if (result != WebApkInstallResult.SUCCESS) {
            showToast(applicationContext.getString(R.string.webapk_install_failed));
        }
    }

    /**
     * Utility method to check if a shortcut can be added to the home screen.
     *
     * @return if a shortcut can be added to the home screen under the current profile.
     */
    @SuppressLint("WrongConstant")
    public static boolean isAddToHomeIntentSupported() {
        if (isRequestPinShortcutSupported()) return true;
        PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
        Intent i = new Intent(INSTALL_SHORTCUT);
        List<ResolveInfo> receivers =
                pm.queryBroadcastReceivers(i, PackageManager.GET_INTENT_FILTERS);
        return !receivers.isEmpty();
    }

    /** Prepares whether Android O's ShortcutManager.requestPinShortcut() is supported. */
    @WorkerThread
    public static void prepareIsRequestPinShortcutSupported() {
        isRequestPinShortcutSupported();
    }

    /** Returns whether Android O's ShortcutManager.requestPinShortcut() is supported. */
    public static boolean isRequestPinShortcutSupported() {
        if (!sCheckedIfRequestPinShortcutSupported) {
            synchronized (sLock) {
                if (!sCheckedIfRequestPinShortcutSupported) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        ShortcutManager shortcutManager =
                                ContextUtils.getApplicationContext()
                                        .getSystemService(ShortcutManager.class);
                        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
                            sIsRequestPinShortcutSupported =
                                    shortcutManager != null
                                            && shortcutManager.isRequestPinShortcutSupported();
                        }
                    }
                    sCheckedIfRequestPinShortcutSupported = true;
                }
            }
        }
        return sIsRequestPinShortcutSupported;
    }

    /**
     * Returns the package name of one of the WebAPKs which can handle {@link url}. Returns null if
     * there are no matches.
     */
    @CalledByNative
    private static String queryFirstWebApkPackage(String url) {
        return WebApkValidator.queryFirstWebApkPackage(ContextUtils.getApplicationContext(), url);
    }

    /**
     * Override whether shortcuts are considered supported for testing.
     *
     * @param supported Whether shortcuts are supported. Pass null to reset.
     */
    public static void setAddToHomeIntentSupportedForTesting(Boolean supported) {
        if (supported == null) {
            sCheckedIfRequestPinShortcutSupported = false;
            sIsRequestPinShortcutSupported = false;
        } else {
            sCheckedIfRequestPinShortcutSupported = true;
            sIsRequestPinShortcutSupported = supported.booleanValue();
        }
    }
}