blob: 6dbc0ea05535a10ec5bd6291ce0b0ec1e8e36ada [file] [log] [blame]
// 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.Callback;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.AppHooks;
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;
/** Whether a homescreen shortcut should be added on success. */
private boolean mAddHomescreenShortcut;
/** Weak pointer to the native WebApkInstaller. */
private long mNativePointer;
/** Talks to Google Play to install WebAPKs. */
private GooglePlayWebApkInstallDelegate mGooglePlayWebApkInstallDelegate;
private WebApkInstaller(long nativePtr) {
mNativePointer = nativePtr;
mGooglePlayWebApkInstallDelegate = AppHooks.get().getGooglePlayWebApkInstallDelegate();
}
@CalledByNative
private static WebApkInstaller create(long nativePtr) {
return new WebApkInstaller(nativePtr);
}
@CalledByNative
private void destroy() {
if (mListener != null) {
ApplicationStatus.unregisterApplicationStateListener(mListener);
}
mListener = null;
mNativePointer = 0;
}
/**
* Installs WebAPK via "unsigned sources" using APK downloaded to {@link filePath}.
* @param filePath File to install.
* @param packageName Package name to install WebAPK at.
*/
@CalledByNative
private void installDownloadedWebApkAsync(String filePath, String packageName) {
mAddHomescreenShortcut = 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);
// Notify native only if the intent could not be delivered. If the intent was delivered
// successfully, notify native once InstallerDelegate has determined whether the install
// was successful.
if (!installOrUpdateDownloadedWebApkImpl(filePath)) {
notify(WebApkInstallResult.FAILURE);
}
}
/**
* Installs a WebAPK from Google Play 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 url The start URL of the WebAPK to install.
*/
@CalledByNative
private void installWebApkFromGooglePlayAsync(
String packageName, int version, String title, String token, String url) {
if (mGooglePlayWebApkInstallDelegate == null) {
notify(WebApkInstallResult.FAILURE);
return;
}
// 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;
}
Callback<Boolean> callback = new Callback<Boolean>() {
@Override
public void onResult(Boolean success) {
// TODO(pkotwicz): Send WebApkInstallResult.PROBABLE_FAILURE result if
// install timed out.
WebApkInstaller.this.notify(
success ? WebApkInstallResult.SUCCESS : WebApkInstallResult.FAILURE);
}
};
mGooglePlayWebApkInstallDelegate.installAsync(
packageName, version, title, token, url, callback);
}
private void notify(@WebApkInstallResult.WebApkInstallResultEnum int result) {
if (mListener != null) {
ApplicationStatus.unregisterApplicationStateListener(mListener);
mListener = null;
}
mInstallTask = null;
if (mNativePointer != 0) {
nativeOnInstallFinished(mNativePointer, result);
}
if (mAddHomescreenShortcut && result == WebApkInstallResult.SUCCESS) {
ShortcutHelper.addWebApkShortcut(
ContextUtils.getApplicationContext(), mWebApkPackageName);
}
}
/**
* Updates WebAPK via "unsigned sources" using APK downloaded to {@link filePath}.
* @param filePath File to update.
*/
@CalledByNative
private void updateUsingDownloadedWebApkAsync(String filePath) {
if (!installOrUpdateDownloadedWebApkImpl(filePath)) {
notify(WebApkInstallResult.FAILURE);
return;
}
// We can't use InstallerDelegate to detect whether updates are successful. If there was no
// error in delivering the intent, assume that the update will be successful.
notify(WebApkInstallResult.SUCCESS);
}
/**
* Updates a WebAPK using Google Play.
* @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 url The start URL of the WebAPK to install.
*/
@CalledByNative
private void updateAsyncFromGooglePlay(
String packageName, int version, String title, String token, String url) {
if (mGooglePlayWebApkInstallDelegate == null) {
notify(WebApkInstallResult.FAILURE);
return;
}
Callback<Boolean> callback = new Callback<Boolean>() {
@Override
public void onResult(Boolean success) {
// TODO(pkotwicz): Send WebApkInstallResult.PROBABLE_FAILURE result if
// update timed out.
WebApkInstaller.this.notify(
success ? WebApkInstallResult.SUCCESS : WebApkInstallResult.FAILURE);
}
};
mGooglePlayWebApkInstallDelegate.installAsync(
packageName, version, title, token, url, callback);
}
/**
* Sends intent to Android to show prompt to install or update downloaded WebAPK.
* @param filePath File to install.
*/
private boolean installOrUpdateDownloadedWebApkImpl(String filePath) {
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;
// TODO(pkotwicz): Return WebApkInstallResult.PROBABLE_FAILURE if the install
// timed out.
WebApkInstaller.this.notify(
success ? WebApkInstallResult.SUCCESS : WebApkInstallResult.FAILURE);
}
};
}
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)) {
WebApkInstaller.this.notify(WebApkInstallResult.PROBABLE_FAILURE);
return;
}
}
};
}
private boolean isWebApkInstalled(String packageName) {
PackageManager packageManager = ContextUtils.getApplicationContext().getPackageManager();
return InstallerDelegate.isInstalled(packageManager, packageName);
}
private native void nativeOnInstallFinished(
long nativeWebApkInstaller, @WebApkInstallResult.WebApkInstallResultEnum int result);
}