// 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.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Looper;
import android.provider.Settings;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.banners.InstallerDelegate;
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) {
if (!installingFromUnknownSourcesAllowed()) {
Log.e(TAG,
"WebAPK install failed because installation from unknown sources is disabled.");
return false;
}
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) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri fileUri = Uri.fromFile(new File(filePath));
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
ContextUtils.getApplicationContext().startActivity(intent);
} catch (ActivityNotFoundException e) {
return false;
}
return true;
}
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) {
if (!installingFromUnknownSourcesAllowed()) {
Log.e(TAG,
"WebAPK update failed because installation from unknown sources is disabled.");
return false;
}
mIsInstall = false;
return installDownloadedWebApk(filePath);
}
/**
* Returns whether the user has enabled installing apps from sources other than the Google Play
* Store.
*/
private static boolean installingFromUnknownSourcesAllowed() {
Context context = ContextUtils.getApplicationContext();
try {
return Settings.Secure.getInt(
context.getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS)
== 1;
} catch (Settings.SettingNotFoundException e) {
return false;
}
}
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);
}