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

// Copyright 2016 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.content.Intent;

import org.jni_zero.CalledByNative;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.Callback;
import org.chromium.base.PackageUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.browserservices.intents.WebappIntentUtils;
import org.chromium.chrome.browser.browserservices.metrics.WebApkUmaRecorder;
import org.chromium.components.webapps.WebApkInstallResult;

/**
 * Java counterpart to webapk_installer.h
 * Contains functionality to install / update WebAPKs.
 * This Java object is created by and owned by the native WebApkInstaller.
 */
public class WebApkInstaller {

    /** Weak pointer to the native WebApkInstaller. */
    private long mNativePointer;

    /** Talks to Google Play to install WebAPKs. */
    private final GooglePlayWebApkInstallDelegate mInstallDelegate;

    private final String mWebApkServerUrl;

    private WebApkInstaller(long nativePtr) {
        mNativePointer = nativePtr;
        mInstallDelegate = AppHooks.get().getGooglePlayWebApkInstallDelegate();
        mWebApkServerUrl = AppHooks.get().getWebApkServerUrl();
    }

    @CalledByNative
    private static WebApkInstaller create(long nativePtr) {
        return new WebApkInstaller(nativePtr);
    }

    @CalledByNative
    private void destroy() {
        mNativePointer = 0;
    }

    /**
     * Installs a WebAPK and monitors the installation.
     *
     * @param packageName The package name of the WebAPK to install.
     * @param version The version of WebAPK to install.
     * @param title The title of the WebAPK to display during installation.
     * @param token The token from WebAPK Server.
     * @param source The source (either app banner or menu) that the install of a WebAPK was
     *     triggered.
     */
    @CalledByNative
    private void installWebApkAsync(
            @JniType("std::string") final String packageName,
            int version,
            @JniType("std::u16string") final String title,
            @JniType("std::string") String token,
            final int source) {
        // Check whether the WebAPK package is already installed. The WebAPK may have been installed
        // by another Chrome version (e.g. Chrome Dev). We have to do this check because the Play
        // install API fails silently if the package is already installed.
        if (isWebApkInstalled(packageName)) {
            notify(WebApkInstallResult.SUCCESS);
            return;
        }

        if (mInstallDelegate == null) {
            notify(WebApkInstallResult.NO_INSTALLER);
            WebApkUmaRecorder.recordGooglePlayInstallResult(
                    WebApkUmaRecorder.GooglePlayInstallResult.FAILED_NO_DELEGATE);
            return;
        }

        Callback<Integer> callback =
                (Integer result) -> {
                    WebApkInstaller.this.notify(result);
                    if (result == WebApkInstallResult.FAILURE) return;
                    var intentDataProvider =
                            WebApkIntentDataProviderFactory.create(
                                    new Intent(),
                                    packageName,
                                    null,
                                    source,
                                    /* forceNavigation= */ false,
                                    /* canUseSplashFromContentProvider= */ false,
                                    /* shareData= */ null,
                                    /* shareDataActivityClassName= */ null);

                    // Stores the source info of WebAPK in WebappDataStorage.
                    WebappRegistry.FetchWebappDataStorageCallback fetchCallback =
                            (WebappDataStorage storage) -> {
                                storage.updateFromWebappIntentDataProvider(intentDataProvider);
                                storage.updateSource(source);
                                storage.updateTimeOfLastCheckForUpdatedWebManifest();
                                WebApkSyncService.onWebApkUsed(
                                        intentDataProvider, storage, true /* isInstall */);
                            };
                    WebappRegistry.getInstance()
                            .register(
                                    WebappIntentUtils.getIdForWebApkPackage(packageName),
                                    fetchCallback);
                };
        mInstallDelegate.installAsync(packageName, version, title, token, callback);
    }

    private void notify(@WebApkInstallResult int result) {
        if (mNativePointer != 0) {
            WebApkInstallerJni.get().onInstallFinished(mNativePointer, result);
        }
    }

    /**
     * Updates a WebAPK installation.
     *
     * @param packageName The package name of the WebAPK to install.
     * @param version The version of WebAPK to install.
     * @param title The title of the WebAPK to display during installation.
     * @param token The token from WebAPK Server.
     */
    @CalledByNative
    private void updateAsync(
            @JniType("std::string") String packageName,
            int version,
            @JniType("std::u16string") String title,
            @JniType("std::string") String token) {
        if (mInstallDelegate == null) {
            notify(WebApkInstallResult.NO_INSTALLER);
            return;
        }

        Callback<Integer> callback =
                new Callback<Integer>() {
                    @Override
                    public void onResult(Integer result) {
                        WebApkInstaller.this.notify(result);
                    }
                };
        mInstallDelegate.updateAsync(packageName, version, title, token, callback);
    }

    @CalledByNative
    private void checkFreeSpace() {
        new AsyncTask<Integer>() {
            @Override
            protected Integer doInBackground() {
                long availableSpaceInBytes =
                        WebApkUmaRecorder.getAvailableSpaceAboveLowSpaceLimit();

                if (availableSpaceInBytes > 0) return SpaceStatus.ENOUGH_SPACE;

                long cacheSizeInBytes = WebApkUmaRecorder.getCacheDirSize();
                if (cacheSizeInBytes + availableSpaceInBytes > 0) {
                    return SpaceStatus.ENOUGH_SPACE_AFTER_FREE_UP_CACHE;
                }
                return SpaceStatus.NOT_ENOUGH_SPACE;
            }

            @Override
            protected void onPostExecute(Integer result) {
                WebApkInstallerJni.get().onGotSpaceStatus(mNativePointer, result);
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    @CalledByNative
    private @JniType("std::string") String getWebApkServerUrl() {
        return mWebApkServerUrl;
    }

    private boolean isWebApkInstalled(String packageName) {
        return PackageUtils.isPackageInstalled(packageName);
    }

    @NativeMethods
    interface Natives {
        void onInstallFinished(long nativeWebApkInstaller, @WebApkInstallResult int result);

        void onGotSpaceStatus(long nativeWebApkInstaller, int status);
    }
}