[go: up one dir, main page]

blob: 8b7a346afeb34e683126fc61ba1595955614ca04 [file] [log] [blame]
// Copyright 2018 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.components.module_installer;
import android.content.SharedPreferences;
import android.util.SparseLongArray;
import com.google.android.play.core.splitinstall.SplitInstallException;
import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
import com.google.android.play.core.splitinstall.SplitInstallRequest;
import com.google.android.play.core.splitinstall.SplitInstallSessionState;
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode;
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.CachedMetrics.EnumeratedHistogramSample;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.components.module_installer.ModuleInstallerBackend.OnFinishedListener;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Backend that uses the Play Core SDK to download a module from Play and install it subsequently.
*/
/* package */ class PlayCoreModuleInstallerBackend
extends ModuleInstallerBackend implements SplitInstallStateUpdatedListener {
private static class InstallTimes {
public final boolean mIsCached;
public final SparseLongArray mInstallTimes = new SparseLongArray();
public InstallTimes(boolean isCached) {
mIsCached = isCached;
mInstallTimes.put(SplitInstallSessionStatus.UNKNOWN, System.currentTimeMillis());
}
}
private static final String TAG = "PlayCoreModInBackend";
private static final String KEY_MODULES_ONDEMAND_REQUESTED_PREVIOUSLY =
"key_modules_requested_previously";
private static final String KEY_MODULES_DEFERRED_REQUESTED_PREVIOUSLY =
"key_modules_deferred_requested_previously";
private final Map<String, InstallTimes> mInstallTimesMap = new HashMap<>();
private final SplitInstallManager mManager;
private boolean mIsClosed;
// FeatureModuleInstallStatus defined in //tools/metrics/histograms/enums.xml.
// These values are persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
private static final int INSTALL_STATUS_SUCCESS = 0;
// private static final int INSTALL_STATUS_FAILURE = 1; [DEPRECATED]
// private static final int INSTALL_STATUS_REQUEST_ERROR = 2; [DEPRECATED]
private static final int INSTALL_STATUS_CANCELLATION = 3;
private static final int INSTALL_STATUS_ACCESS_DENIED = 4;
private static final int INSTALL_STATUS_ACTIVE_SESSIONS_LIMIT_EXCEEDED = 5;
private static final int INSTALL_STATUS_API_NOT_AVAILABLE = 6;
private static final int INSTALL_STATUS_INCOMPATIBLE_WITH_EXISTING_SESSION = 7;
private static final int INSTALL_STATUS_INSUFFICIENT_STORAGE = 8;
private static final int INSTALL_STATUS_INVALID_REQUEST = 9;
private static final int INSTALL_STATUS_MODULE_UNAVAILABLE = 10;
private static final int INSTALL_STATUS_NETWORK_ERROR = 11;
private static final int INSTALL_STATUS_NO_ERROR = 12;
private static final int INSTALL_STATUS_SERVICE_DIED = 13;
private static final int INSTALL_STATUS_SESSION_NOT_FOUND = 14;
private static final int INSTALL_STATUS_SPLITCOMPAT_COPY_ERROR = 15;
private static final int INSTALL_STATUS_SPLITCOMPAT_EMULATION_ERROR = 16;
private static final int INSTALL_STATUS_SPLITCOMPAT_VERIFICATION_ERROR = 17;
private static final int INSTALL_STATUS_INTERNAL_ERROR = 18;
private static final int INSTALL_STATUS_UNKNOWN_SPLITINSTALL_ERROR = 19;
private static final int INSTALL_STATUS_UNKNOWN_REQUEST_ERROR = 20;
// Keep this one at the end and increment appropriately when adding new status.
private static final int INSTALL_STATUS_COUNT = 21;
// FeatureModuleAvailabilityStatus defined in //tools/metrics/histograms/enums.xml.
// These values are persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
private static final int AVAILABILITY_STATUS_REQUESTED = 0;
private static final int AVAILABILITY_STATUS_INSTALLED_REQUESTED = 1;
private static final int AVAILABILITY_STATUS_INSTALLED_UNREQUESTED = 2;
// Keep this one at the end and increment appropriately when adding new status.
private static final int AVAILABILITY_STATUS_COUNT = 3;
/** Records via UMA all modules that have been requested and are currently installed. */
public static void recordModuleAvailability() {
// MUST call init before creating a SplitInstallManager.
ModuleInstaller.init();
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
Set<String> requestedModules = new HashSet<>();
requestedModules.addAll(
prefs.getStringSet(KEY_MODULES_ONDEMAND_REQUESTED_PREVIOUSLY, new HashSet<>()));
requestedModules.addAll(
prefs.getStringSet(KEY_MODULES_DEFERRED_REQUESTED_PREVIOUSLY, new HashSet<>()));
Set<String> installedModules =
SplitInstallManagerFactory.create(ContextUtils.getApplicationContext())
.getInstalledModules();
for (String name : requestedModules) {
EnumeratedHistogramSample sample = new EnumeratedHistogramSample(
"Android.FeatureModules.AvailabilityStatus." + name, AVAILABILITY_STATUS_COUNT);
if (installedModules.contains(name)) {
sample.record(AVAILABILITY_STATUS_INSTALLED_REQUESTED);
} else {
sample.record(AVAILABILITY_STATUS_REQUESTED);
}
}
for (String name : installedModules) {
if (!requestedModules.contains(name)) {
// Module appeared without being requested. Weird.
EnumeratedHistogramSample sample = new EnumeratedHistogramSample(
"Android.FeatureModules.AvailabilityStatus." + name,
AVAILABILITY_STATUS_COUNT);
sample.record(AVAILABILITY_STATUS_INSTALLED_UNREQUESTED);
}
}
}
public PlayCoreModuleInstallerBackend(OnFinishedListener listener) {
super(listener);
// MUST call init before creating a SplitInstallManager.
ModuleInstaller.init();
mManager = SplitInstallManagerFactory.create(ContextUtils.getApplicationContext());
mManager.registerListener(this);
}
@Override
public void install(String moduleName) {
assert !mIsClosed;
// Record start time in order to later report the install duration via UMA. We want to make
// a difference between modules that have been requested first before and after the last
// Chrome start. Modules that have been requested before may install quicker as they may be
// installed form cache. To do this, we use shared prefs to track modules previously
// requested. Additionally, storing requested modules helps us to record module install
// status at next Chrome start.
assert !mInstallTimesMap.containsKey(moduleName);
mInstallTimesMap.put(moduleName,
new InstallTimes(storeModuleRequested(
moduleName, KEY_MODULES_ONDEMAND_REQUESTED_PREVIOUSLY)));
SplitInstallRequest request =
SplitInstallRequest.newBuilder().addModule(moduleName).build();
mManager.startInstall(request).addOnFailureListener(exception -> {
int status = exception instanceof SplitInstallException
? getHistogramCode(((SplitInstallException) exception).getErrorCode())
: INSTALL_STATUS_UNKNOWN_REQUEST_ERROR;
Log.e(TAG, "Failed to request module '%s': error code %s", moduleName, status);
// If we reach this error condition |onStateUpdate| won't be called. Thus, call
// |onFinished| here.
finish(false, Collections.singletonList(moduleName), status);
});
}
@Override
public void installDeferred(String moduleName) {
assert !mIsClosed;
mManager.deferredInstall(Collections.singletonList(moduleName));
storeModuleRequested(moduleName, KEY_MODULES_DEFERRED_REQUESTED_PREVIOUSLY);
}
@Override
public void close() {
assert !mIsClosed;
mManager.unregisterListener(this);
mIsClosed = true;
}
@Override
public void onStateUpdate(SplitInstallSessionState state) {
assert !mIsClosed;
switch (state.status()) {
case SplitInstallSessionStatus.DOWNLOADING:
case SplitInstallSessionStatus.DOWNLOADED:
case SplitInstallSessionStatus.INSTALLING:
case SplitInstallSessionStatus.INSTALLED:
for (String name : state.moduleNames()) {
mInstallTimesMap.get(name).mInstallTimes.put(
state.status(), System.currentTimeMillis());
}
if (state.status() == SplitInstallSessionStatus.INSTALLED) {
finish(true, state.moduleNames(), INSTALL_STATUS_SUCCESS);
}
break;
case SplitInstallSessionStatus.CANCELED:
case SplitInstallSessionStatus.FAILED:
int status = state.status() == SplitInstallSessionStatus.CANCELED
? INSTALL_STATUS_CANCELLATION
: getHistogramCode(state.errorCode());
Log.e(TAG, "Failed to install modules '%s': error code %s", state.moduleNames(),
status);
finish(false, state.moduleNames(), status);
break;
}
}
private void finish(boolean success, List<String> moduleNames, int eventId) {
for (String name : moduleNames) {
RecordHistogram.recordEnumeratedHistogram(
"Android.FeatureModules.InstallStatus." + name, eventId, INSTALL_STATUS_COUNT);
if (success) {
recordInstallTimes(name);
}
}
onFinished(success, moduleNames);
}
/**
* Gets the UMA code based on a SplitInstall error code
* @param errorCode The error code
* @return int The User Metric Analysis code
*/
private int getHistogramCode(@SplitInstallErrorCode int errorCode) {
switch (errorCode) {
case SplitInstallErrorCode.ACCESS_DENIED:
return INSTALL_STATUS_ACCESS_DENIED;
case SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED:
return INSTALL_STATUS_ACTIVE_SESSIONS_LIMIT_EXCEEDED;
case SplitInstallErrorCode.API_NOT_AVAILABLE:
return INSTALL_STATUS_API_NOT_AVAILABLE;
case SplitInstallErrorCode.INCOMPATIBLE_WITH_EXISTING_SESSION:
return INSTALL_STATUS_INCOMPATIBLE_WITH_EXISTING_SESSION;
case SplitInstallErrorCode.INSUFFICIENT_STORAGE:
return INSTALL_STATUS_INSUFFICIENT_STORAGE;
case SplitInstallErrorCode.INVALID_REQUEST:
return INSTALL_STATUS_INVALID_REQUEST;
case SplitInstallErrorCode.MODULE_UNAVAILABLE:
return INSTALL_STATUS_MODULE_UNAVAILABLE;
case SplitInstallErrorCode.NETWORK_ERROR:
return INSTALL_STATUS_NETWORK_ERROR;
case SplitInstallErrorCode.NO_ERROR:
return INSTALL_STATUS_NO_ERROR;
case SplitInstallErrorCode.SERVICE_DIED:
return INSTALL_STATUS_SERVICE_DIED;
case SplitInstallErrorCode.SESSION_NOT_FOUND:
return INSTALL_STATUS_SESSION_NOT_FOUND;
case SplitInstallErrorCode.SPLITCOMPAT_COPY_ERROR:
return INSTALL_STATUS_SPLITCOMPAT_COPY_ERROR;
case SplitInstallErrorCode.SPLITCOMPAT_EMULATION_ERROR:
return INSTALL_STATUS_SPLITCOMPAT_EMULATION_ERROR;
case SplitInstallErrorCode.SPLITCOMPAT_VERIFICATION_ERROR:
return INSTALL_STATUS_SPLITCOMPAT_VERIFICATION_ERROR;
case SplitInstallErrorCode.INTERNAL_ERROR:
return INSTALL_STATUS_INTERNAL_ERROR;
default:
return INSTALL_STATUS_UNKNOWN_SPLITINSTALL_ERROR;
}
}
/**
* Stores to shared prevs that a module has been requested.
*
* @param moduleName Module that has been requested.
* @param prefKey Pref key pointing to a string set to which the requested module will be added.
* @return Whether the module has been requested previously.
*/
private static boolean storeModuleRequested(String moduleName, String prefKey) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
Set<String> modulesRequestedPreviously = prefs.getStringSet(prefKey, new HashSet<String>());
Set<String> newModulesRequestedPreviously = new HashSet<>(modulesRequestedPreviously);
newModulesRequestedPreviously.add(moduleName);
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet(prefKey, newModulesRequestedPreviously);
editor.apply();
return modulesRequestedPreviously.contains(moduleName);
}
/** Records via UMA module install times divided into install steps. */
private void recordInstallTimes(String moduleName) {
recordInstallTime(moduleName, "", SplitInstallSessionStatus.UNKNOWN,
SplitInstallSessionStatus.INSTALLED);
recordInstallTime(moduleName, ".PendingDownload", SplitInstallSessionStatus.UNKNOWN,
SplitInstallSessionStatus.DOWNLOADING);
recordInstallTime(moduleName, ".Downloading", SplitInstallSessionStatus.DOWNLOADING,
SplitInstallSessionStatus.DOWNLOADED);
recordInstallTime(moduleName, ".PendingInstall", SplitInstallSessionStatus.DOWNLOADED,
SplitInstallSessionStatus.INSTALLING);
recordInstallTime(moduleName, ".Installing", SplitInstallSessionStatus.INSTALLING,
SplitInstallSessionStatus.INSTALLED);
}
private void recordInstallTime(
String moduleName, String histogramSubname, int startKey, int endKey) {
assert mInstallTimesMap.containsKey(moduleName);
InstallTimes installTimes = mInstallTimesMap.get(moduleName);
if (installTimes.mInstallTimes.get(startKey) == 0
|| installTimes.mInstallTimes.get(endKey) == 0) {
// Time stamps for install times have not been stored. Don't record anything to not skew
// data.
return;
}
RecordHistogram.recordLongTimesHistogram(
String.format("Android.FeatureModules.%sInstallDuration%s.%s",
installTimes.mIsCached ? "Cached" : "Uncached", histogramSubname,
moduleName),
installTimes.mInstallTimes.get(endKey) - installTimes.mInstallTimes.get(startKey));
}
}