// Copyright 2016 The Chromium Authors. All rights reserved. // 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.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Looper; import org.chromium.base.ApplicationState; import org.chromium.base.ApplicationStatus; import org.chromium.base.ContentUriUtils; import org.chromium.base.ContextUtils; import org.chromium.base.annotations.CalledByNative; import org.chromium.chrome.browser.ShortcutHelper; import org.chromium.chrome.browser.banners.InstallerDelegate; import org.chromium.chrome.browser.util.IntentUtils; import java.io.File; /** * 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 { private static final String TAG = "WebApkInstaller"; /** The WebAPK's package name. */ private String mWebApkPackageName; /** Monitors for application state changes. */ private ApplicationStatus.ApplicationStateListener mListener; /** Monitors installation progress. */ private InstallerDelegate mInstallTask; /** Indicates whether to install or update a WebAPK. */ private boolean mIsInstall; /** Weak pointer to the native WebApkInstaller. */ private long mNativePointer; private WebApkInstaller(long nativePtr) { mNativePointer = nativePtr; } @CalledByNative private static WebApkInstaller create(long nativePtr) { return new WebApkInstaller(nativePtr); } @CalledByNative private void destroy() { ApplicationStatus.unregisterApplicationStateListener(mListener); mNativePointer = 0; } /** * Installs a WebAPK and monitors the installation process. * @param filePath File to install. * @param packageName Package name to install WebAPK at. * @return True if the install was started. A "true" return value does not guarantee that the * install succeeds. */ @CalledByNative private boolean installAsyncAndMonitorInstallationFromNative( String filePath, String packageName) { mIsInstall = true; mWebApkPackageName = packageName; // Start monitoring the installation. PackageManager packageManager = ContextUtils.getApplicationContext().getPackageManager(); mInstallTask = new InstallerDelegate(Looper.getMainLooper(), packageManager, createInstallerDelegateObserver(), mWebApkPackageName); mInstallTask.start(); // Start monitoring the application state changes. mListener = createApplicationStateListener(); ApplicationStatus.registerApplicationStateListener(mListener); return installDownloadedWebApk(filePath); } /** * Send intent to Android to show prompt and install downloaded WebAPK. * @param filePath File to install. */ private boolean installDownloadedWebApk(String filePath) { // TODO(pkotwicz|hanxi): For Chrome Stable figure out a different way of installing // WebAPKs which does not involve enabling "installation from Unsigned Sources". Context context = ContextUtils.getApplicationContext(); Intent intent; File pathToInstall = new File(filePath); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { intent = new Intent(Intent.ACTION_VIEW); Uri fileUri = Uri.fromFile(pathToInstall); intent.setDataAndType(fileUri, "application/vnd.android.package-archive"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } else { Uri source = ContentUriUtils.getContentUriFromFile(context, pathToInstall); intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setData(source); } return IntentUtils.safeStartActivity(context, intent); } private InstallerDelegate.Observer createInstallerDelegateObserver() { return new InstallerDelegate.Observer() { @Override public void onInstallFinished(InstallerDelegate task, boolean success) { if (mInstallTask != task) return; onInstallFinishedInternal(success); } }; } private void onInstallFinishedInternal(boolean success) { ApplicationStatus.unregisterApplicationStateListener(mListener); mInstallTask = null; if (mNativePointer != 0) { nativeOnInstallFinished(mNativePointer, success); } if (success && mIsInstall) { ShortcutHelper.addWebApkShortcut(ContextUtils.getApplicationContext(), mWebApkPackageName); } } /** * Updates a WebAPK. * @param filePath File to update. * @return True if the update was started. A "true" return value does not guarantee that the * update succeeds. */ @CalledByNative private boolean updateAsyncFromNative(String filePath) { mIsInstall = false; return installDownloadedWebApk(filePath); } private ApplicationStatus.ApplicationStateListener createApplicationStateListener() { return new ApplicationStatus.ApplicationStateListener() { @Override public void onApplicationStateChange(int newState) { if (!ApplicationStatus.hasVisibleActivities()) return; /** * Currently WebAPKs aren't installed by Play. A user can cancel the installation * when the Android native installation dialog shows. The only way to detect the * user cancelling the installation is to check whether the WebAPK is installed * when Chrome is resumed. The monitoring of application state changes will be * removed once WebAPKs are installed by Play. */ if (newState == ApplicationState.HAS_RUNNING_ACTIVITIES && !isWebApkInstalled(mWebApkPackageName)) { onInstallFinishedInternal(false); return; } } }; } private boolean isWebApkInstalled(String packageName) { PackageManager packageManager = ContextUtils.getApplicationContext().getPackageManager(); return InstallerDelegate.isInstalled(packageManager, packageName); } private native void nativeOnInstallFinished(long nativeWebApkInstaller, boolean success); }