From 2623e05c253c4490e7aa2832fd27352fc3e3496b Mon Sep 17 00:00:00 2001 From: Fred Mello Date: Wed, 2 Oct 2019 20:18:04 +0000 Subject: [PATCH] Android: Refactor Module Installer for Testability The intrinsic dependencies encountered along the way made it difficult (less effective) to break up this CL into multiple smaller CLs (apologies to the reviewers in advance). To simplify its understanding, please refer to the following doc: https://docs.google.com/document/d/1ClZglFQroV53zSYEGIZ7yca8KIsPHGBmzMCbtreu5lg The following are the major points to review: - refactoring of the infra into builder, installer, logger, observer, util - new design (IoC driven) for installers to enable for easy testability - new design for emitted modules (cross-package communication) Out-of-scope (following CLs): - removal of bytecode processing for third-party activities (no longer needed) - move code from ModuleInstallerConfig.java into BuildConfig.java - unit tests for the remaining module_installer classes Testing: this change was verified with vr, ar, autofill_assistant, dev_ui, and test_dummy modules. It was tested with both -m and -f command line args. Bug: 1005802 Change-Id: Icf357c06c4b71a96ed9fa0584f6322d6dc6143d7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1813520 Reviewed-by: Ilya Sherman Reviewed-by: Tibor Goldschwendt Reviewed-by: Andrew Grieve Reviewed-by: Peter Wen Commit-Queue: Fred Mello Cr-Commit-Position: refs/heads/master@{#702165} --- chrome/android/BUILD.gn | 16 +- .../AutofillAssistantModuleEntry.java | 2 +- .../AutofillAssistantModuleEntryProvider.java | 3 +- .../chrome/features/dev_ui/DevUi.java | 2 +- .../tab_management/TabManagementDelegate.java | 2 +- .../chrome/browser/vr/VrDelegateProvider.java | 2 +- .../chrome/browser/vr/VrModuleProvider.java | 6 +- .../chrome/browser/ChromeActivity.java | 2 +- .../chrome/browser/ChromeApplication.java | 6 +- .../init/ChromeBrowserInitializer.java | 6 +- .../chrome/browser/vr/ArCoreInstallUtils.java | 11 +- .../vr/VrDaydreamReadyModuleInstallTest.java | 44 ++- .../browser/vr/util/VrTestRuleUtils.java | 6 - .../chrome/modules/extra_icu/ExtraIcu.java | 2 +- .../modules/test_dummy/TestDummyProvider.java | 2 +- components/module_installer/android/BUILD.gn | 66 ++-- .../build/ModuleInstallerConfig.template | 2 +- .../module_installer/ApkModuleInstaller.java | 20 -- .../module_installer/ModuleInstaller.java | 75 ---- .../ModuleInstallerBackend.java | 57 ---- .../module_installer/ModuleInstallerImpl.java | 208 ------------ .../OnModuleInstallFinishedListener.java | 15 - .../PlayCoreModuleInstallerBackend.java | 319 ------------------ .../{ => builder}/Module.java | 118 ++++--- .../builder/ModuleEngine.java | 59 ++++ .../{ => builder}/ModuleInterface.java | 2 +- .../ModuleInterfaceProcessor.java | 38 ++- .../module_installer/engine/ApkEngine.java | 20 ++ .../engine/EngineFactory.java | 23 ++ .../FakeEngine.java} | 28 +- .../engine/InstallEngine.java | 44 +++ .../engine/InstallListener.java | 17 + .../engine/SplitCompatEngine.java | 147 ++++++++ .../engine/SplitCompatEngineFacade.java | 72 ++++ .../module_installer/logger/Logger.java | 57 ++++ .../logger/PlayCoreLogger.java | 75 ++++ .../logger/SplitAvailabilityLogger.java | 175 ++++++++++ .../logger/SplitInstallFailureLogger.java | 104 ++++++ .../logger/SplitInstallStatusLogger.java | 72 ++++ .../ActivityObserver.java} | 27 +- .../ActivityObserverFacade.java} | 19 +- .../observer/InstallerObserver.java | 10 + .../observers/ObserverStrategy.java | 18 - .../util/CrashKeyRecorder.java | 72 ++++ .../module_installer/util/ModuleUtil.java | 49 +++ .../util/SplitCompatInitializer.java | 36 ++ .../module_installer/{ => util}/Timer.java | 14 +- .../module_installer/ModuleInstallerRule.java | 33 -- .../engine/SplitCompatEngineTest.java | 308 +++++++++++++++++ .../ActivityObserverTest.java} | 49 ++- docs/android_dynamic_feature_modules.md | 2 +- tools/metrics/histograms/enums.xml | 17 + tools/metrics/histograms/histograms.xml | 15 + 53 files changed, 1621 insertions(+), 973 deletions(-) delete mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/ApkModuleInstaller.java delete mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstaller.java delete mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerBackend.java delete mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerImpl.java delete mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/OnModuleInstallFinishedListener.java delete mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/PlayCoreModuleInstallerBackend.java rename components/module_installer/android/java/src/org/chromium/components/module_installer/{ => builder}/Module.java (65%) create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleEngine.java rename components/module_installer/android/java/src/org/chromium/components/module_installer/{ => builder}/ModuleInterface.java (93%) rename components/module_installer/android/java/src/org/chromium/components/module_installer/{ => builder}/ModuleInterfaceProcessor.java (75%) create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/engine/ApkEngine.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/engine/EngineFactory.java rename components/module_installer/android/java/src/org/chromium/components/module_installer/{FakeModuleInstallerBackend.java => engine/FakeEngine.java} (88%) create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallEngine.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallListener.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngine.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngineFacade.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/logger/Logger.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/logger/PlayCoreLogger.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitAvailabilityLogger.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallFailureLogger.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallStatusLogger.java rename components/module_installer/android/java/src/org/chromium/components/module_installer/{observers/ModuleActivityObserver.java => observer/ActivityObserver.java} (66%) rename components/module_installer/android/java/src/org/chromium/components/module_installer/{observers/ObserverStrategyImpl.java => observer/ActivityObserverFacade.java} (55%) create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/observer/InstallerObserver.java delete mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ObserverStrategy.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/util/CrashKeyRecorder.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/util/ModuleUtil.java create mode 100644 components/module_installer/android/java/src/org/chromium/components/module_installer/util/SplitCompatInitializer.java rename components/module_installer/android/java/src/org/chromium/components/module_installer/{ => util}/Timer.java (70%) delete mode 100644 components/module_installer/android/javatests/src/org/chromium/components/module_installer/ModuleInstallerRule.java create mode 100644 components/module_installer/android/junit/src/org/chromium/components/module_installer/engine/SplitCompatEngineTest.java rename components/module_installer/android/junit/src/org/chromium/components/module_installer/{observers/ModuleActivityObserverTest.java => observer/ActivityObserverTest.java} (68%) diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn index 28eee097b60a4b..fe409ec1a85ecb 100644 --- a/chrome/android/BUILD.gn +++ b/chrome/android/BUILD.gn @@ -1064,15 +1064,13 @@ if (enable_vr || enable_arcore) { "javatests/src/org/chromium/chrome/browser/vr/rules/VrModuleNotInstalled.java", ] - deps = - chrome_test_xr_java_deps + [ - "//chrome/android:chrome_test_xr_java", - "//third_party/gvr-android-sdk:controller_test_api_java", - "//third_party/gvr-android-sdk:gvr_common_java", - ":chrome_test_util_java", - "//components/module_installer/android:module_installer_java", - "//components/module_installer/android:module_installer_test_java", - ] + deps = chrome_test_xr_java_deps + [ + "//chrome/android:chrome_test_xr_java", + "//third_party/gvr-android-sdk:controller_test_api_java", + "//third_party/gvr-android-sdk:gvr_common_java", + ":chrome_test_util_java", + "//components/module_installer/android:module_installer_java", + ] data = [ "//chrome/android/shared_preference_files/test/", diff --git a/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntry.java b/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntry.java index 183bef834d4674..cac90ed64520d5 100644 --- a/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntry.java +++ b/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntry.java @@ -12,7 +12,7 @@ import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.widget.ScrimView; import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController; -import org.chromium.components.module_installer.ModuleInterface; +import org.chromium.components.module_installer.builder.ModuleInterface; import org.chromium.content_public.browser.WebContents; import java.util.Map; diff --git a/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntryProvider.java b/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntryProvider.java index ef0dc28a1eaea1..81b0804a591f19 100644 --- a/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntryProvider.java +++ b/chrome/android/features/autofill_assistant/public/java/src/org/chromium/chrome/browser/autofill_assistant/AutofillAssistantModuleEntryProvider.java @@ -14,7 +14,6 @@ import org.chromium.chrome.browser.autofill_assistant.metrics.FeatureModuleInstallation; import org.chromium.chrome.browser.modules.ModuleInstallUi; import org.chromium.chrome.browser.tab.Tab; -import org.chromium.components.module_installer.ModuleInstaller; /** * Manages the loading of autofill assistant DFM, and provides implementation of @@ -104,7 +103,7 @@ public void onFailureUiResponse(boolean retry) { }); // Shows toast informing user about install start. ui.showInstallStartUi(); - ModuleInstaller.getInstance().install("autofill_assistant", (success) -> { + AutofillAssistantModule.install((success) -> { if (success) { // Don't show success UI from DFM, transition to autobot UI directly. AutofillAssistantMetrics.recordFeatureModuleInstallation( diff --git a/chrome/android/features/dev_ui/public/java/src/org/chromium/chrome/features/dev_ui/DevUi.java b/chrome/android/features/dev_ui/public/java/src/org/chromium/chrome/features/dev_ui/DevUi.java index 9f6750756cdd82..9221ed0dfdd812 100644 --- a/chrome/android/features/dev_ui/public/java/src/org/chromium/chrome/features/dev_ui/DevUi.java +++ b/chrome/android/features/dev_ui/public/java/src/org/chromium/chrome/features/dev_ui/DevUi.java @@ -4,7 +4,7 @@ package org.chromium.chrome.features.dev_ui; -import org.chromium.components.module_installer.ModuleInterface; +import org.chromium.components.module_installer.builder.ModuleInterface; /** Interface to call into DevUI feature. */ @ModuleInterface(module = "dev_ui", impl = "org.chromium.chrome.features.dev_ui.DevUiImpl") diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegate.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegate.java index b3946500d9aa26..bfdbb993a04c68 100644 --- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegate.java +++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementDelegate.java @@ -16,7 +16,7 @@ import org.chromium.chrome.browser.tasks.TasksSurface; import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter; import org.chromium.chrome.features.start_surface.StartSurface; -import org.chromium.components.module_installer.ModuleInterface; +import org.chromium.components.module_installer.builder.ModuleInterface; import org.chromium.ui.modelutil.PropertyModel; /** diff --git a/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrDelegateProvider.java b/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrDelegateProvider.java index eaaae99ecd3290..99c4acd3c6ed7c 100644 --- a/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrDelegateProvider.java +++ b/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrDelegateProvider.java @@ -4,7 +4,7 @@ package org.chromium.chrome.browser.vr; -import org.chromium.components.module_installer.ModuleInterface; +import org.chromium.components.module_installer.builder.ModuleInterface; /** Provides delegate interfaces that can be used to call into VR. */ @ModuleInterface(module = "vr", impl = "org.chromium.chrome.browser.vr.VrDelegateProviderImpl") diff --git a/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrModuleProvider.java b/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrModuleProvider.java index 2ee06ec75c43f9..cfcd2615336b3c 100644 --- a/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrModuleProvider.java +++ b/chrome/android/features/vr/java/src/org/chromium/chrome/browser/vr/VrModuleProvider.java @@ -11,7 +11,7 @@ import org.chromium.chrome.R; import org.chromium.chrome.browser.modules.ModuleInstallUi; import org.chromium.chrome.browser.tab.Tab; -import org.chromium.components.module_installer.OnModuleInstallFinishedListener; +import org.chromium.components.module_installer.engine.InstallListener; import java.util.ArrayList; import java.util.List; @@ -88,7 +88,7 @@ public static void onExitVr() { for (VrModeObserver observer : sVrModeObservers) observer.onExitVr(); } - /* package */ static void installModule(OnModuleInstallFinishedListener onFinishedListener) { + /* package */ static void installModule(InstallListener listener) { VrModule.install((success) -> { if (success) { // Re-create delegate provider. @@ -97,7 +97,7 @@ public static void onExitVr() { assert !(delegate instanceof VrDelegateFallback); delegate.initAfterModuleInstall(); } - onFinishedListener.onFinished(success); + listener.onComplete(success); }); } diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java index 214fe8d8c334f6..88aadc31ee4cde 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java @@ -162,7 +162,7 @@ import org.chromium.components.bookmarks.BookmarkId; import org.chromium.components.feature_engagement.EventConstants; import org.chromium.components.feature_engagement.Tracker; -import org.chromium.components.module_installer.Module; +import org.chromium.components.module_installer.builder.Module; import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.content_public.browser.SelectionPopupController; import org.chromium.content_public.browser.WebContents; diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeApplication.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeApplication.java index 5de260b3e2f0e3..14797634011c19 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeApplication.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeApplication.java @@ -49,7 +49,7 @@ import org.chromium.chrome.browser.vr.OnExitVrRequestListener; import org.chromium.chrome.browser.vr.VrModuleProvider; import org.chromium.components.embedder_support.application.FontPreloadingWorkaround; -import org.chromium.components.module_installer.ModuleInstaller; +import org.chromium.components.module_installer.util.ModuleUtil; import org.chromium.ui.base.ResourceBundle; /** @@ -121,7 +121,7 @@ protected void attachBaseContext(Context context) { // Record via UMA all modules that have been requested and are currently installed. This // will tell us the install penetration of each module over time. - ModuleInstaller.getInstance().recordModuleAvailability(); + ModuleUtil.recordModuleAvailability(); // Set Chrome factory for mapping BackgroundTask classes to TaskIds. ChromeBackgroundTaskFactory.setAsDefault(); @@ -129,7 +129,7 @@ protected void attachBaseContext(Context context) { // Write installed modules to crash keys. This needs to be done as early as possible so that // these values are set before any crashes are reported. - ModuleInstaller.getInstance().updateCrashKeys(); + ModuleUtil.updateCrashKeys(); BuildInfo.setFirebaseAppId(FirebaseConfig.getFirebaseAppId()); diff --git a/chrome/android/java/src/org/chromium/chrome/browser/init/ChromeBrowserInitializer.java b/chrome/android/java/src/org/chromium/chrome/browser/init/ChromeBrowserInitializer.java index aeae7787cffa3b..e2ca44c8034a01 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/init/ChromeBrowserInitializer.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/init/ChromeBrowserInitializer.java @@ -43,8 +43,7 @@ import org.chromium.components.background_task_scheduler.BackgroundTaskSchedulerPrefs; import org.chromium.components.crash.browser.ChildProcessCrashObserver; import org.chromium.components.minidump_uploader.CrashFileManager; -import org.chromium.components.module_installer.ModuleInstaller; -import org.chromium.components.module_installer.observers.ModuleActivityObserver; +import org.chromium.components.module_installer.util.ModuleUtil; import org.chromium.content_public.browser.BrowserStartupController; import org.chromium.content_public.browser.DeviceUtils; import org.chromium.content_public.browser.SpeechRecognition; @@ -237,7 +236,6 @@ private void preInflationStartup() { DeviceUtils.addDeviceSpecificUserAgentSwitch(); ApplicationStatus.registerStateListenerForAllActivities(createActivityStateListener()); - ApplicationStatus.registerStateListenerForAllActivities(new ModuleActivityObserver()); mPreInflationStartupComplete = true; } @@ -440,7 +438,7 @@ public void childCrashed(int pid) { // Needed for field trial metrics to be properly collected in ServiceManager only mode. FeatureUtilities.cacheNativeFlagsForServiceManagerOnlyMode(); - ModuleInstaller.getInstance().recordStartupTime(); + ModuleUtil.recordStartupTime(); } private ActivityStateListener createActivityStateListener() { diff --git a/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreInstallUtils.java b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreInstallUtils.java index 2c3aaf3c77a6c1..26537ada1bd119 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreInstallUtils.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreInstallUtils.java @@ -17,7 +17,8 @@ import org.chromium.chrome.browser.infobar.SimpleConfirmInfoBarBuilder; import org.chromium.chrome.browser.modules.ModuleInstallUi; import org.chromium.chrome.browser.tab.Tab; -import org.chromium.components.module_installer.ModuleInstaller; +import org.chromium.components.module_installer.engine.EngineFactory; +import org.chromium.components.module_installer.engine.InstallEngine; /** * Installs AR DFM and ArCore runtimes. @@ -64,8 +65,6 @@ private static ArCoreInstallUtils create(long nativeArCoreInstallUtils) { private ArCoreInstallUtils(long nativeArCoreInstallUtils) { mNativeArCoreInstallUtils = nativeArCoreInstallUtils; - // Need to be called before trying to access the AR module. - ModuleInstaller.getInstance().init(); } @Override @@ -99,9 +98,13 @@ private boolean shouldRequestInstallArModule() { @CalledByNative private void requestInstallArModule(Tab tab) { mTab = tab; + ModuleInstallUi ui = new ModuleInstallUi(mTab, R.string.ar_module_title, this); + InstallEngine installEngine = new EngineFactory().getEngine(); + ui.showInstallStartUi(); - ModuleInstaller.getInstance().install("ar", success -> { + + installEngine.install("ar", success -> { assert shouldRequestInstallArModule() != success; if (success) { diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/VrDaydreamReadyModuleInstallTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/VrDaydreamReadyModuleInstallTest.java index 175f5dd53d74c4..0e30f8639707ba 100644 --- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/VrDaydreamReadyModuleInstallTest.java +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/VrDaydreamReadyModuleInstallTest.java @@ -12,6 +12,7 @@ import org.junit.Assert; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExternalResource; import org.junit.rules.RuleChain; import org.junit.runner.RunWith; @@ -28,8 +29,7 @@ import org.chromium.chrome.browser.vr.util.VrTestRuleUtils; import org.chromium.chrome.test.ChromeActivityTestRule; import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate; -import org.chromium.components.module_installer.ModuleInstaller; -import org.chromium.components.module_installer.ModuleInstallerRule; +import org.chromium.components.module_installer.engine.InstallEngine; import java.util.HashSet; import java.util.List; @@ -53,24 +53,13 @@ public class VrDaydreamReadyModuleInstallTest { @Rule public RuleChain mRuleChain; - private ModuleInstallerRule mModuleInstallerRule; - - private ChromeActivityTestRule mVrTestRule; - private final Set mModulesRequestedDeferred = new HashSet<>(); public VrDaydreamReadyModuleInstallTest(Callable callable) throws Exception { - mVrTestRule = callable.call(); - mModuleInstallerRule = new ModuleInstallerRule(new ModuleInstaller() { - @Override - public void installDeferred(String moduleName) { - mModulesRequestedDeferred.add(moduleName); - } - }); mRuleChain = - RuleChain.outerRule(mModuleInstallerRule) - .around(VrTestRuleUtils.wrapRuleInActivityRestrictionRule(mVrTestRule)); + RuleChain.outerRule(new VrModuleInstallerRule()) + .around(VrTestRuleUtils.wrapRuleInActivityRestrictionRule(callable.call())); } /** Tests that the install is requested deferred. */ @@ -83,4 +72,29 @@ public void testDeferredRequestOnStartup() { Assert.assertTrue("VR module should have been deferred installed at startup", mModulesRequestedDeferred.contains("vr")); } + + private class VrModuleInstallerRule extends ExternalResource { + private final InstallEngine mOldModuleInstaller; + private final InstallEngine mStubModuleInstaller; + + public VrModuleInstallerRule() { + mStubModuleInstaller = new InstallEngine() { + @Override + public void installDeferred(String moduleName) { + mModulesRequestedDeferred.add(moduleName); + } + }; + mOldModuleInstaller = VrModule.getInstallEngine(); + } + + @Override + protected void before() { + VrModule.setInstallEngine(mStubModuleInstaller); + } + + @Override + protected void after() { + VrModule.setInstallEngine(mOldModuleInstaller); + } + } } diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/VrTestRuleUtils.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/VrTestRuleUtils.java index e8c47c14d69278..1c2163470147b8 100644 --- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/VrTestRuleUtils.java +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/VrTestRuleUtils.java @@ -22,10 +22,8 @@ import org.chromium.chrome.browser.vr.rules.ChromeTabbedActivityVrTestRule; import org.chromium.chrome.browser.vr.rules.CustomTabActivityVrTestRule; import org.chromium.chrome.browser.vr.rules.VrActivityRestrictionRule; -import org.chromium.chrome.browser.vr.rules.VrModuleNotInstalled; import org.chromium.chrome.browser.vr.rules.VrTestRule; import org.chromium.chrome.browser.vr.rules.WebappActivityVrTestRule; -import org.chromium.components.module_installer.Module; import java.util.ArrayList; import java.util.concurrent.Callable; @@ -59,10 +57,6 @@ public interface ChromeLaunchMethod { public void launch() throws Throwable; } */ public static void evaluateVrTestRuleImpl(final Statement base, final Description desc, final VrTestRule rule, final ChromeLaunchMethod launcher) throws Throwable { - // Should be called before any other VR methods get called. - if (desc.getAnnotation(VrModuleNotInstalled.class) != null) { - Module.setForceUninstalled("vr"); - } TestVrShellDelegate.setDescription(desc); VrTestRuleUtils.ensureNoVrActivitiesDisplayed(); diff --git a/chrome/android/modules/extra_icu/public/java/src/org/chromium/chrome/modules/extra_icu/ExtraIcu.java b/chrome/android/modules/extra_icu/public/java/src/org/chromium/chrome/modules/extra_icu/ExtraIcu.java index 13beb9a99ba0a0..9d56e27a941c80 100644 --- a/chrome/android/modules/extra_icu/public/java/src/org/chromium/chrome/modules/extra_icu/ExtraIcu.java +++ b/chrome/android/modules/extra_icu/public/java/src/org/chromium/chrome/modules/extra_icu/ExtraIcu.java @@ -4,7 +4,7 @@ package org.chromium.chrome.modules.extra_icu; -import org.chromium.components.module_installer.ModuleInterface; +import org.chromium.components.module_installer.builder.ModuleInterface; /** Interface into the extra ICU module. Only used to check whether module is installed. */ @ModuleInterface(module = "extra_icu", impl = "org.chromium.chrome.modules.extra_icu.ExtraIcuImpl") diff --git a/chrome/android/modules/test_dummy/public/java/src/org/chromium/chrome/modules/test_dummy/TestDummyProvider.java b/chrome/android/modules/test_dummy/public/java/src/org/chromium/chrome/modules/test_dummy/TestDummyProvider.java index 94743e65d155cb..04ea09eb4de4e3 100644 --- a/chrome/android/modules/test_dummy/public/java/src/org/chromium/chrome/modules/test_dummy/TestDummyProvider.java +++ b/chrome/android/modules/test_dummy/public/java/src/org/chromium/chrome/modules/test_dummy/TestDummyProvider.java @@ -5,7 +5,7 @@ package org.chromium.chrome.modules.test_dummy; import org.chromium.chrome.features.test_dummy.TestDummy; -import org.chromium.components.module_installer.ModuleInterface; +import org.chromium.components.module_installer.builder.ModuleInterface; /** Provides the test dummy implementation. */ @ModuleInterface(module = "test_dummy", diff --git a/components/module_installer/android/BUILD.gn b/components/module_installer/android/BUILD.gn index 4cd636ee347a33..7f173c96f2b288 100644 --- a/components/module_installer/android/BUILD.gn +++ b/components/module_installer/android/BUILD.gn @@ -7,18 +7,27 @@ import("//chrome/android/modules/buildflags.gni") android_library("module_installer_java") { java_files = [ - "java/src/org/chromium/components/module_installer/ModuleInstallerImpl.java", - "java/src/org/chromium/components/module_installer/ModuleInstallerBackend.java", - "java/src/org/chromium/components/module_installer/FakeModuleInstallerBackend.java", - "java/src/org/chromium/components/module_installer/PlayCoreModuleInstallerBackend.java", - "java/src/org/chromium/components/module_installer/ApkModuleInstaller.java", - "java/src/org/chromium/components/module_installer/ModuleInstaller.java", - "java/src/org/chromium/components/module_installer/OnModuleInstallFinishedListener.java", - "java/src/org/chromium/components/module_installer/Module.java", - "java/src/org/chromium/components/module_installer/Timer.java", - "java/src/org/chromium/components/module_installer/observers/ModuleActivityObserver.java", - "java/src/org/chromium/components/module_installer/observers/ObserverStrategy.java", - "java/src/org/chromium/components/module_installer/observers/ObserverStrategyImpl.java", + "java/src/org/chromium/components/module_installer/builder/Module.java", + "java/src/org/chromium/components/module_installer/builder/ModuleEngine.java", + "java/src/org/chromium/components/module_installer/engine/ApkEngine.java", + "java/src/org/chromium/components/module_installer/engine/FakeEngine.java", + "java/src/org/chromium/components/module_installer/engine/InstallEngine.java", + "java/src/org/chromium/components/module_installer/engine/EngineFactory.java", + "java/src/org/chromium/components/module_installer/engine/InstallListener.java", + "java/src/org/chromium/components/module_installer/engine/SplitCompatEngine.java", + "java/src/org/chromium/components/module_installer/engine/SplitCompatEngineFacade.java", + "java/src/org/chromium/components/module_installer/logger/Logger.java", + "java/src/org/chromium/components/module_installer/logger/PlayCoreLogger.java", + "java/src/org/chromium/components/module_installer/logger/SplitAvailabilityLogger.java", + "java/src/org/chromium/components/module_installer/logger/SplitInstallFailureLogger.java", + "java/src/org/chromium/components/module_installer/logger/SplitInstallStatusLogger.java", + "java/src/org/chromium/components/module_installer/observer/ActivityObserver.java", + "java/src/org/chromium/components/module_installer/observer/ActivityObserverFacade.java", + "java/src/org/chromium/components/module_installer/observer/InstallerObserver.java", + "java/src/org/chromium/components/module_installer/util/CrashKeyRecorder.java", + "java/src/org/chromium/components/module_installer/util/ModuleUtil.java", + "java/src/org/chromium/components/module_installer/util/SplitCompatInitializer.java", + "java/src/org/chromium/components/module_installer/util/Timer.java", ] deps = [ @@ -35,19 +44,11 @@ android_library("module_installer_java") { annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] } -android_library("module_installer_test_java") { - testonly = true - java_files = [ "javatests/src/org/chromium/components/module_installer/ModuleInstallerRule.java" ] - deps = [ - ":module_installer_java", - "//base:base_java", - "//third_party/junit", - ] - jacoco_never_instrument = true -} - junit_binary("module_installer_junit_tests") { - java_files = [ "junit/src/org/chromium/components/module_installer/observers/ModuleActivityObserverTest.java" ] + java_files = [ + "junit/src/org/chromium/components/module_installer/engine/SplitCompatEngineTest.java", + "junit/src/org/chromium/components/module_installer/observer/ActivityObserverTest.java", + ] deps = [ ":module_installer_java", "//base:base_java", @@ -57,15 +58,12 @@ junit_binary("module_installer_junit_tests") { java_library("module_interface_java") { supports_android = true - java_files = [ - "java/src/org/chromium/components/module_installer/ModuleInterface.java", - ] + java_files = [ "java/src/org/chromium/components/module_installer/builder/ModuleInterface.java" ] } java_annotation_processor("module_interface_processor") { - java_files = [ "java/src/org/chromium/components/module_installer/ModuleInterfaceProcessor.java" ] - main_class = - "org.chromium.components.module_installer.ModuleInterfaceProcessor" + java_files = [ "java/src/org/chromium/components/module_installer/builder/ModuleInterfaceProcessor.java" ] + main_class = "org.chromium.components.module_installer.builder.ModuleInterfaceProcessor" annotation_processor_deps = [ "//third_party/auto:auto_service_processor" ] deps = [ ":module_interface_java", @@ -77,14 +75,14 @@ java_annotation_processor("module_interface_processor") { # Use this one if your target needs to depend on ModuleInstallerConfig. The # other two targets are automatically added to build targets. java_cpp_template("module_installer_build_config") { - package_path = "org/chromium/components/module_installer" + package_path = "org/chromium/components/module_installer/builder" sources = [ "build/ModuleInstallerConfig.template", ] } java_cpp_template("module_installer_bundle_build_config") { - package_path = "org/chromium/components/module_installer" + package_path = "org/chromium/components/module_installer/builder" sources = [ "build/ModuleInstallerConfig.template", ] @@ -92,7 +90,7 @@ java_cpp_template("module_installer_bundle_build_config") { } java_cpp_template("module_installer_apk_build_config") { - package_path = "org/chromium/components/module_installer" + package_path = "org/chromium/components/module_installer/builder" sources = [ "build/ModuleInstallerConfig.template", ] @@ -121,6 +119,6 @@ source_set("native") { generate_jni("jni_headers") { sources = [ - "java/src/org/chromium/components/module_installer/Module.java", + "java/src/org/chromium/components/module_installer/builder/Module.java", ] } diff --git a/components/module_installer/android/build/ModuleInstallerConfig.template b/components/module_installer/android/build/ModuleInstallerConfig.template index 745e6be4437174..b23b3472f0299c 100644 --- a/components/module_installer/android/build/ModuleInstallerConfig.template +++ b/components/module_installer/android/build/ModuleInstallerConfig.template @@ -2,7 +2,7 @@ // 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; +package org.chromium.components.module_installer.builder; /** Build config for DFMs. */ public class ModuleInstallerConfig { diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/ApkModuleInstaller.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/ApkModuleInstaller.java deleted file mode 100644 index bc565d6765de0f..00000000000000 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/ApkModuleInstaller.java +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2019 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; - -/** Module Installer for Apks. */ -public class ApkModuleInstaller implements ModuleInstaller { - /** A valid singleton instance is necessary for tests to swap it out. */ - private static ModuleInstaller sInstance = new ApkModuleInstaller(); - - /** Returns the singleton instance. */ - public static ModuleInstaller getInstance() { - return sInstance; - } - - public static void setInstanceForTesting(ModuleInstaller moduleInstaller) { - sInstance = moduleInstaller; - } -} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstaller.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstaller.java deleted file mode 100644 index 1b5e8ed17cfde4..00000000000000 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstaller.java +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2019 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.app.Activity; - -import org.chromium.base.annotations.MainDex; - -/** - * This interface contains all the necessary methods to orchestrate the installation of dynamic - * feature modules (DFMs). - */ -public interface ModuleInstaller { - /** Returns the singleton instance from the correct implementation. */ - @MainDex - static ModuleInstaller getInstance() { - if (ModuleInstallerConfig.IS_BUNDLE) { - return ModuleInstallerImpl.getInstance(); - } else { - return ApkModuleInstaller.getInstance(); - } - } - - static void setInstanceForTesting(ModuleInstaller moduleInstaller) { - if (ModuleInstallerConfig.IS_BUNDLE) { - ModuleInstallerImpl.setInstanceForTesting(moduleInstaller); - } else { - ApkModuleInstaller.setInstanceForTesting(moduleInstaller); - } - } - - /** Needs to be called before trying to access a module. */ - default void init() {} - - /** - * Needs to be called in attachBaseContext of the activities that want to have access to - * splits prior to application restart. - * - * For details, see: - * https://developer.android.com/reference/com/google/android/play/core/splitcompat/SplitCompat.html#install(android.content.Context) - * @param activity The Activity for which SplitCompat will be run. - */ - default void initActivity(Activity activity) {} - - /** - * Records via UMA all modules that have been requested and are currently installed. The intent - * is to measure the install penetration of each module. - */ - default void recordModuleAvailability() {} - - /** Writes fully installed and emulated modules to crash keys. */ - default void updateCrashKeys() {} - - /** - * Requests the install of a module. The install will be performed asynchronously. - * - * @param moduleName Name of the module as defined in GN. - * @param onFinishedListener Listener to be called once installation is finished. - */ - default void install(String moduleName, OnModuleInstallFinishedListener onFinishedListener) {} - - /** - * Asynchronously installs module in the background when on unmetered connection and charging. - * Install is best effort and may fail silently. Upon success, the module will only be available - * after Chrome restarts. - * - * @param moduleName Name of the module. - */ - default void installDeferred(String moduleName) {} - - /** Called when startup completes to record module overhead during startup. */ - default void recordStartupTime() {} -} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerBackend.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerBackend.java deleted file mode 100644 index c6e9dc71d4f7ad..00000000000000 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerBackend.java +++ /dev/null @@ -1,57 +0,0 @@ -// 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 org.chromium.base.ThreadUtils; - -import java.util.List; - -/** A backend for installing dynamic feature modules that contain the actual install logic. */ -/* package */ abstract class ModuleInstallerBackend { - private final OnFinishedListener mListener; - - /** Listener for when a module install has finished. */ - interface OnFinishedListener { - /** - * Called when the module install has finished. - * @param success True if the install was successful. - * @param moduleNames Names of modules whose install is finished. - */ - void onFinished(boolean success, List moduleNames); - } - - public ModuleInstallerBackend(OnFinishedListener listener) { - ThreadUtils.assertOnUiThread(); - mListener = listener; - } - - /** - * Asynchronously installs module. - * @param moduleName Name of the module. - */ - public abstract void install(String moduleName); - - /** - * Asynchronously installs module in the background. - * @param moduleName Name of the module. - */ - public abstract void installDeferred(String moduleName); - - /** - * Releases resources of this backend. Calling this method an install is in progress results in - * undefined behavior. Calling any other method on this backend after closing results in - * undefined behavior, too. - */ - public abstract void close(); - - /* package */ void recordModuleAvailability() {} - - /* package */ void recordStartupTime(long durationMs) {} - - /** To be called when module install has finished. */ - protected void onFinished(boolean success, List moduleNames) { - mListener.onFinished(success, moduleNames); - } -} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerImpl.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerImpl.java deleted file mode 100644 index 5bd89d2db4d1a4..00000000000000 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInstallerImpl.java +++ /dev/null @@ -1,208 +0,0 @@ -// 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.app.Activity; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Build; -import android.text.TextUtils; - -import com.google.android.play.core.splitcompat.SplitCompat; -import com.google.android.play.core.splitinstall.SplitInstallManagerFactory; - -import org.chromium.base.BuildInfo; -import org.chromium.base.CommandLine; -import org.chromium.base.ContextUtils; -import org.chromium.base.StrictModeContext; -import org.chromium.base.ThreadUtils; -import org.chromium.components.crash.CrashKeyIndex; -import org.chromium.components.crash.CrashKeys; -import org.chromium.components.module_installer.observers.ModuleActivityObserver; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; - -/** - * Installs dynamic feature modules (DFMs). - */ -/* package */ class ModuleInstallerImpl implements ModuleInstaller { - /** - * Command line switch for activating the fake backend. - */ - private static final String FAKE_FEATURE_MODULE_INSTALL = "fake-feature-module-install"; - private static ModuleInstaller sInstance = new ModuleInstallerImpl(); - private static boolean sAppContextSplitCompatted; - private final Map> mModuleNameListenerMap = - new HashMap<>(); - private ModuleInstallerBackend mBackend; - private ModuleActivityObserver mActivityObserver = new ModuleActivityObserver(); - - /** - * Returns the singleton instance. - */ - public static ModuleInstaller getInstance() { - return sInstance; - } - - public static void setInstanceForTesting(ModuleInstaller moduleInstaller) { - sInstance = moduleInstaller; - } - - @Override - public void init() { - try (Timer ignored1 = new Timer()) { - if (sAppContextSplitCompatted) return; - // SplitCompat.install may copy modules into Chrome's internal folder or clean them up. - try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { - SplitCompat.install(ContextUtils.getApplicationContext()); - sAppContextSplitCompatted = true; - } - // SplitCompat.install may add emulated modules. Thus, update crash keys. - updateCrashKeys(); - } - } - - @Override - public void initActivity(Activity activity) { - try (Timer ignored = new Timer()) { - // SplitCompat#install should always be run for the application first before it is run - // for any activities. - init(); - SplitCompat.installActivity(activity); - } - } - - @Override - public void recordModuleAvailability() { - try (Timer ignored = new Timer()) { - getBackend().recordModuleAvailability(); - } - } - - @Override - public void recordStartupTime() { - getBackend().recordStartupTime(Timer.getTotalTime()); - } - - @Override - public void updateCrashKeys() { - try (Timer ignored = new Timer()) { - Context context = ContextUtils.getApplicationContext(); - - // Get modules that are fully installed as split APKs (excluding base which is always - // installed). Tree set to have ordered and, thus, deterministic results. - Set fullyInstalledModules = new TreeSet<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // Split APKs are only supported on Android L+. - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo( - BuildInfo.getInstance().packageName, 0); - if (packageInfo.splitNames != null) { - fullyInstalledModules.addAll(Arrays.asList(packageInfo.splitNames)); - } - } catch (NameNotFoundException e) { - throw new RuntimeException(e); - } - } - - // Create temporary split install manager to retrieve both fully installed and emulated - // modules. Then remove fully installed ones to get emulated ones only. Querying the - // installed modules can only be done if splitcompat has already been called. Otherwise, - // emulation of later modules won't work. If splitcompat has not been called no modules - // are emulated. Therefore, use an empty set in that case. - Set emulatedModules = new TreeSet<>(); - if (sAppContextSplitCompatted) { - emulatedModules.addAll( - SplitInstallManagerFactory.create(context).getInstalledModules()); - emulatedModules.removeAll(fullyInstalledModules); - } - - CrashKeys.getInstance().set( - CrashKeyIndex.INSTALLED_MODULES, encodeCrashKeyValue(fullyInstalledModules)); - CrashKeys.getInstance().set( - CrashKeyIndex.EMULATED_MODULES, encodeCrashKeyValue(emulatedModules)); - } - } - - @Override - public void install(String moduleName, OnModuleInstallFinishedListener onFinishedListener) { - try (Timer ignored = new Timer()) { - ThreadUtils.assertOnUiThread(); - - if (!mModuleNameListenerMap.containsKey(moduleName)) { - mModuleNameListenerMap.put(moduleName, new LinkedList<>()); - } - List onFinishedListeners = - mModuleNameListenerMap.get(moduleName); - onFinishedListeners.add(onFinishedListener); - if (onFinishedListeners.size() > 1) { - // Request is already running. - return; - } - getBackend().install(moduleName); - } - } - - @Override - public void installDeferred(String moduleName) { - try (Timer ignored = new Timer()) { - ThreadUtils.assertOnUiThread(); - getBackend().installDeferred(moduleName); - } - } - - private void onFinished(boolean success, List moduleNames) { - // Add timer to this private method since it is passed as a callback. - try (Timer ignored = new Timer()) { - ThreadUtils.assertOnUiThread(); - - mActivityObserver.onModuleInstalled(); - - for (String moduleName : moduleNames) { - List onFinishedListeners = - mModuleNameListenerMap.get(moduleName); - if (onFinishedListeners == null) continue; - - for (OnModuleInstallFinishedListener listener : onFinishedListeners) { - listener.onFinished(success); - } - mModuleNameListenerMap.remove(moduleName); - } - - if (mModuleNameListenerMap.isEmpty()) { - mBackend.close(); - mBackend = null; - } - - updateCrashKeys(); - } - } - - private ModuleInstallerBackend getBackend() { - if (mBackend == null) { - ModuleInstallerBackend.OnFinishedListener listener = this::onFinished; - mBackend = CommandLine.getInstance().hasSwitch(FAKE_FEATURE_MODULE_INSTALL) - ? new FakeModuleInstallerBackend(listener) - : new PlayCoreModuleInstallerBackend(listener); - } - return mBackend; - } - - private String encodeCrashKeyValue(Set moduleNames) { - if (moduleNames.isEmpty()) return ""; - // Values with dots are interpreted as URLs. Some module names have dots in them. Make sure - // they don't get sanitized. - return TextUtils.join(",", moduleNames).replace('.', '$'); - } - - private ModuleInstallerImpl() {} -} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/OnModuleInstallFinishedListener.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/OnModuleInstallFinishedListener.java deleted file mode 100644 index e6d8d0e26a5477..00000000000000 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/OnModuleInstallFinishedListener.java +++ /dev/null @@ -1,15 +0,0 @@ -// 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; - -/** Listener for when a module install has finished. */ -public interface OnModuleInstallFinishedListener { - /** - * Called when the install has finished. - * - * @param success True if the module was installed successfully. - */ - void onFinished(boolean success); -} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/PlayCoreModuleInstallerBackend.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/PlayCoreModuleInstallerBackend.java deleted file mode 100644 index 501314c88ccb65..00000000000000 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/PlayCoreModuleInstallerBackend.java +++ /dev/null @@ -1,319 +0,0 @@ -// 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.os.SystemClock; -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.CachedMetrics.TimesHistogramSample; -import org.chromium.base.metrics.RecordHistogram; - -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, SystemClock.uptimeMillis()); - } - } - - 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 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; - private static final int INSTALL_STATUS_NO_SPLITCOMPAT = 21; - // Keep this one at the end and increment appropriately when adding new status. - private static final int INSTALL_STATUS_COUNT = 22; - - // 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. */ - @Override - /* package */ void recordModuleAvailability() { - SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); - Set 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 installedModules = mManager.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); - } - } - } - - @Override - /* package */ void recordStartupTime(long durationMs) { - TimesHistogramSample sample = - new TimesHistogramSample("Android.FeatureModules.StartupTime"); - sample.record(durationMs); - } - - /* package */ PlayCoreModuleInstallerBackend(OnFinishedListener listener) { - super(listener); - // MUST call init before creating a SplitInstallManager. - ModuleInstaller.getInstance().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; - Log.i(TAG, "Status for modules '%s' updated to %d", state.moduleNames(), state.status()); - switch (state.status()) { - case SplitInstallSessionStatus.DOWNLOADING: - case SplitInstallSessionStatus.INSTALLING: - case SplitInstallSessionStatus.INSTALLED: - for (String name : state.moduleNames()) { - mInstallTimesMap.get(name).mInstallTimes.put( - state.status(), SystemClock.uptimeMillis()); - } - if (state.status() == SplitInstallSessionStatus.INSTALLED) { - finish(true, state.moduleNames(), INSTALL_STATUS_SUCCESS); - } - break; - // DOWNLOADED only gets sent if SplitCompat is not enabled. That's an error. - // SplitCompat should always be enabled. - case SplitInstallSessionStatus.DOWNLOADED: - case SplitInstallSessionStatus.CANCELED: - case SplitInstallSessionStatus.FAILED: - int status; - if (state.status() == SplitInstallSessionStatus.DOWNLOADED) { - status = INSTALL_STATUS_NO_SPLITCOMPAT; - } else if (state.status() == SplitInstallSessionStatus.CANCELED) { - status = INSTALL_STATUS_CANCELLATION; - } else { - status = 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 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 boolean storeModuleRequested(String moduleName, String prefKey) { - SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); - Set modulesRequestedPreviously = prefs.getStringSet(prefKey, new HashSet<>()); - Set 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, ".Download", SplitInstallSessionStatus.DOWNLOADING, - 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.%sAwakeInstallDuration%s.%s", - installTimes.mIsCached ? "Cached" : "Uncached", histogramSubname, - moduleName), - installTimes.mInstallTimes.get(endKey) - installTimes.mInstallTimes.get(startKey)); - } -} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/Module.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/Module.java similarity index 65% rename from components/module_installer/android/java/src/org/chromium/components/module_installer/Module.java rename to components/module_installer/android/java/src/org/chromium/components/module_installer/builder/Module.java index fe8947168c1029..5a25b1c20169bc 100644 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/Module.java +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/Module.java @@ -2,12 +2,15 @@ // 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; +package org.chromium.components.module_installer.builder; import org.chromium.base.StrictModeContext; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.NativeMethods; +import org.chromium.components.module_installer.engine.InstallEngine; +import org.chromium.components.module_installer.engine.InstallListener; +import org.chromium.components.module_installer.util.Timer; import java.util.HashSet; import java.util.Set; @@ -17,45 +20,20 @@ * {@link ModuleInterface} for how to conveniently create an instance of the module class for a * specific feature module. * - * @param The interface of the module/ + * @param The interface of the module */ @JNINamespace("module_installer") public class Module { - private static final Set sInstantiatedModuleNames = new HashSet<>(); - private static final Set sModulesUninstalledForTesting = new HashSet<>(); - private static final Set sPendingNativeRegistrations = new HashSet<>(); - private static boolean sNativeInitialized; private final String mName; private final Class mInterfaceClass; private final String mImplClassName; + private T mImpl; - /** Forces a module to appear uninstalled. */ - @VisibleForTesting - public static void setForceUninstalled(String moduleName) { - // We should not be uninstalling anything after the module API has been used for a - // particular module. - assert !sInstantiatedModuleNames.contains(moduleName); - sModulesUninstalledForTesting.add(moduleName); - } + private InstallEngine mInstaller; - @NativeMethods - interface Natives { - void loadNativeLibrary(String name); - } - - /** - * To be called after the main native library has been loaded. Any module instances - * created before the native library is loaded have their native component queued - * for loading and registration. Calling this methed completes that process. - **/ - public static void doDeferredNativeRegistrations() { - for (String name : sPendingNativeRegistrations) { - loadNativeLibrary(name); - } - sPendingNativeRegistrations.clear(); - sNativeInitialized = true; - } + private static boolean sNativeInitialized; + private static final Set sPendingNativeRegistrations = new HashSet<>(); /** * Instantiates a module. @@ -63,44 +41,55 @@ public static void doDeferredNativeRegistrations() { * @param name The module's name as used with {@link ModuleInstaller}. * @param interfaceClass {@link Class} object of the module interface. * @param implClassName fully qualified class name of the implementation of the module's - * interface. - **/ + * interface. + */ public Module(String name, Class interfaceClass, String implClassName) { mName = name; mInterfaceClass = interfaceClass; mImplClassName = implClassName; - sInstantiatedModuleNames.add(name); } - /** Returns true if the module is currently installed and can be accessed. */ - public boolean isInstalled() { - try (Timer ignored1 = new Timer()) { - if (sModulesUninstalledForTesting.contains(mName)) return false; - if (mImpl != null) return true; - // Accessing classes in the module may cause its DEX file to be loaded. And on some - // devices that causes a read mode violation. - try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { - ModuleInstaller.getInstance().init(); - Class.forName(mImplClassName); - return true; - } catch (ClassNotFoundException e) { - return false; + @VisibleForTesting + public InstallEngine getInstallEngine() { + if (mInstaller == null) { + try (Timer timer = new Timer()) { + mInstaller = new ModuleEngine(mImplClassName); } } + return mInstaller; } - /** Requests install of the module. See {@link ModuleInstallerImpl#install} for more details. */ - public void install(OnModuleInstallFinishedListener onFinishedListener) { - assert !isInstalled(); - ModuleInstaller.getInstance().install(mName, onFinishedListener); + @VisibleForTesting + public void setInstallEngine(InstallEngine engine) { + mInstaller = engine; } /** - * Requests deferred install of the module. See {@link ModuleInstallerImpl#installDeferred} for - * more details. + * Returns true if the module is currently installed and can be accessed. + */ + public boolean isInstalled() { + try (Timer timer = new Timer()) { + return getInstallEngine().isInstalled(mName); + } + } + + /** + * Requests install of the module. + */ + public void install(InstallListener listener) { + try (Timer timer = new Timer()) { + assert !isInstalled(); + getInstallEngine().install(mName, listener); + } + } + + /** + * Requests deferred install of the module. */ public void installDeferred() { - ModuleInstaller.getInstance().installDeferred(mName); + try (Timer timer = new Timer()) { + getInstallEngine().installDeferred(mName); + } } /** @@ -108,10 +97,9 @@ public void installDeferred() { * installed. */ public T getImpl() { - try (Timer ignored1 = new Timer()) { + try (Timer timer = new Timer()) { assert isInstalled(); if (mImpl == null) { - ModuleInstaller.getInstance().init(); // Accessing classes in the module may cause its DEX file to be loaded. And on some // devices that causes a read mode violation. try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { @@ -140,4 +128,22 @@ private static void loadNativeLibrary(String name) { ModuleJni.get().loadNativeLibrary(name); } + + /** + * To be called after the main native library has been loaded. Any module instances + * created before the native library is loaded have their native component queued + * for loading and registration. Calling this methed completes that process. + **/ + public static void doDeferredNativeRegistrations() { + for (String name : sPendingNativeRegistrations) { + loadNativeLibrary(name); + } + sPendingNativeRegistrations.clear(); + sNativeInitialized = true; + } + + @NativeMethods + interface Natives { + void loadNativeLibrary(String name); + } } diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleEngine.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleEngine.java new file mode 100644 index 00000000000000..a8cf103740dc61 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleEngine.java @@ -0,0 +1,59 @@ +// Copyright 2019 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.builder; + +import android.app.Activity; + +import org.chromium.base.StrictModeContext; +import org.chromium.components.module_installer.engine.EngineFactory; +import org.chromium.components.module_installer.engine.InstallEngine; +import org.chromium.components.module_installer.engine.InstallListener; + +/** + * Proxy engine used by {@link Module}. + * This engine's main purpose is to change the behaviour of isInstalled(...) so that + * modules can be moved in and out from the base more easily. + */ +class ModuleEngine implements InstallEngine { + private static InstallEngine sInternalEngine; + + private final String mImplClassName; + + public ModuleEngine(String implClassName) { + this(implClassName, new EngineFactory()); + } + + public ModuleEngine(String implClassName, EngineFactory engineFactory) { + mImplClassName = implClassName; + sInternalEngine = engineFactory.getEngine(); + } + + @Override + public void initActivity(Activity activity) { + sInternalEngine.initActivity(activity); + } + + @Override + public boolean isInstalled(String moduleName) { + // Accessing classes in the module may cause its DEX file to be loaded. And on some + // devices that causes a read mode violation. + try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { + Class.forName(mImplClassName); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + @Override + public void installDeferred(String moduleName) { + sInternalEngine.installDeferred(moduleName); + } + + @Override + public void install(String moduleName, InstallListener listener) { + sInternalEngine.install(moduleName, listener); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInterface.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleInterface.java similarity index 93% rename from components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInterface.java rename to components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleInterface.java index 1fc6fd5523d3c7..f831f0153451f5 100644 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInterface.java +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleInterface.java @@ -2,7 +2,7 @@ // 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; +package org.chromium.components.module_installer.builder; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInterfaceProcessor.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleInterfaceProcessor.java similarity index 75% rename from components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInterfaceProcessor.java rename to components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleInterfaceProcessor.java index 4eecb3a014f56d..50145c4eb8ab9a 100644 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/ModuleInterfaceProcessor.java +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/builder/ModuleInterfaceProcessor.java @@ -2,7 +2,7 @@ // 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; +package org.chromium.components.module_installer.builder; import com.google.auto.service.AutoService; import com.google.common.base.CaseFormat; @@ -75,10 +75,12 @@ private TypeSpec createModuleClassSpec( CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, moduleName) + "Module"); TypeName interfaceClassName = ClassName.get(moduleInterface); TypeName moduleClassName = ParameterizedTypeName.get( - ClassName.get("org.chromium.components.module_installer", "Module"), + ClassName.get("org.chromium.components.module_installer.builder", "Module"), interfaceClassName); - TypeName onFinishedListenerClassName = ClassName.get( - "org.chromium.components.module_installer", "OnModuleInstallFinishedListener"); + TypeName listenerInterface = + ClassName.get("org.chromium.components.module_installer.engine", "InstallListener"); + TypeName installEngineInterface = + ClassName.get("org.chromium.components.module_installer.engine", "InstallEngine"); FieldSpec module = FieldSpec.builder(moduleClassName, "sModule") .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) @@ -92,13 +94,12 @@ private TypeSpec createModuleClassSpec( .addStatement("return sModule.isInstalled()") .build(); - MethodSpec install = - MethodSpec.methodBuilder("install") - .returns(TypeName.VOID) - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addParameter(onFinishedListenerClassName, "onFinishedListener") - .addStatement("sModule.install(onFinishedListener)") - .build(); + MethodSpec install = MethodSpec.methodBuilder("install") + .returns(TypeName.VOID) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(listenerInterface, "listener") + .addStatement("sModule.install(listener)") + .build(); MethodSpec installDeferred = MethodSpec.methodBuilder("installDeferred") .returns(TypeName.VOID) @@ -112,6 +113,19 @@ private TypeSpec createModuleClassSpec( .addStatement("return sModule.getImpl()") .build(); + MethodSpec getInstallEngine = MethodSpec.methodBuilder("getInstallEngine") + .returns(installEngineInterface) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addStatement("return sModule.getInstallEngine()") + .build(); + + MethodSpec setInstallEngine = MethodSpec.methodBuilder("setInstallEngine") + .returns(TypeName.VOID) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(installEngineInterface, "engine") + .addStatement("sModule.setInstallEngine(engine)") + .build(); + MethodSpec constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build(); @@ -123,6 +137,8 @@ private TypeSpec createModuleClassSpec( .addMethod(install) .addMethod(installDeferred) .addMethod(getImpl) + .addMethod(getInstallEngine) + .addMethod(setInstallEngine) .build(); } diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/ApkEngine.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/ApkEngine.java new file mode 100644 index 00000000000000..5841d391666bfe --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/ApkEngine.java @@ -0,0 +1,20 @@ +// Copyright 2019 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.engine; + +/** + * Engine used by APK builds. + * + * This class exposes no behavior - it's main purpose is to help our compile-time optimizers + * to exclude libraries that should not be included in APK builds (for example, SplitCompat). + */ +class ApkEngine implements InstallEngine { + @Override + public void install(String moduleName, InstallListener listener) { + // This method should never be called in APK builds. + // Adding a fallback call for completeness. + listener.onComplete(false); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/EngineFactory.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/EngineFactory.java new file mode 100644 index 00000000000000..4afcf5fdf793a0 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/EngineFactory.java @@ -0,0 +1,23 @@ +// Copyright 2019 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.engine; + +import org.chromium.base.CommandLine; +import org.chromium.components.module_installer.builder.ModuleInstallerConfig; + +/** + * Factory used to build concrete engines. + */ +public class EngineFactory { + public InstallEngine getEngine() { + if (!ModuleInstallerConfig.IS_BUNDLE) { + return new ApkEngine(); + } + if (CommandLine.getInstance().hasSwitch("fake-feature-module-install")) { + return new FakeEngine(); + } + return new SplitCompatEngine(); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/FakeModuleInstallerBackend.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/FakeEngine.java similarity index 88% rename from components/module_installer/android/java/src/org/chromium/components/module_installer/FakeModuleInstallerBackend.java rename to components/module_installer/android/java/src/org/chromium/components/module_installer/engine/FakeEngine.java index e293021bfff9a5..52c3dae21b82b8 100644 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/FakeModuleInstallerBackend.java +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/FakeEngine.java @@ -1,8 +1,8 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2019 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; +package org.chromium.components.module_installer.engine; import android.content.Context; import android.content.pm.PackageManager; @@ -20,19 +20,19 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.util.Arrays; /** - * Backend that looks for module APKs on the device's disk instead of invoking the Play core API to + * Engine that looks for module APKs on the device's disk instead of invoking the Play core API to * install a feature module. This backend is used for testing purposes where the module is not * uploaded to the Play server. */ -class FakeModuleInstallerBackend extends ModuleInstallerBackend { - private static final String TAG = "FakeModInBackend"; +class FakeEngine extends SplitCompatEngine { + private static final String TAG = "FakeEngine"; private static final String MODULES_SRC_DIRECTORY_PATH = "/data/local/tmp/modules"; - public FakeModuleInstallerBackend(OnFinishedListener listener) { - super(listener); + @Override + public void installDeferred(String moduleName) { + // This is currently not supported by fake installs. } /** @@ -43,7 +43,7 @@ public FakeModuleInstallerBackend(OnFinishedListener listener) { * module to be is not accessible without rooting. */ @Override - public void install(String moduleName) { + public void install(String moduleName, InstallListener listener) { ThreadUtils.assertOnUiThread(); new AsyncTask() { @@ -54,19 +54,11 @@ protected Boolean doInBackground() { @Override protected void onPostExecute(Boolean success) { - onFinished(success, Arrays.asList(moduleName)); + notifyListener(listener, success); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - @Override - public void installDeferred(String moduleName) {} - - @Override - public void close() { - // No open resources. Nothing to be done here. - } - private boolean installInternal(String moduleName) { Context context = ContextUtils.getApplicationContext(); long versionCode = BuildInfo.getInstance().versionCode; diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallEngine.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallEngine.java new file mode 100644 index 00000000000000..2b3b0a3507c251 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallEngine.java @@ -0,0 +1,44 @@ +// Copyright 2019 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.engine; + +import android.app.Activity; + +/** + * Engine definition for installing dynamic feature modules. + */ +public interface InstallEngine { + /** + * Initializes an Activity so that dynamic feature modules are available to be used. + * + * @param activity The activity that wants to use a module. + */ + default void initActivity(Activity activity) {} + + /** + * Checks whether or not a dynamic feature module is installed. + * + * @param moduleName The module name. + * @return Module installed or not. + */ + default boolean isInstalled(String moduleName) { + return false; + } + + /** + * Installs a dynamic feature module deferred. + * + * @param moduleName The module name. + */ + default void installDeferred(String moduleName) {} + + /** + * Installs a dynamic feature module on-demand. + * + * @param moduleName The module name. + * @param listener The listener to install updates. + */ + default void install(String moduleName, InstallListener listener) {} +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallListener.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallListener.java new file mode 100644 index 00000000000000..a553e8168dc04c --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/InstallListener.java @@ -0,0 +1,17 @@ +// Copyright 2019 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.engine; + +/** + * Broadcast listener for dynamic feature module installs. + */ +public interface InstallListener { + /** + * Called when the install has completed. + * + * @param success True if the module was installed successfully. + */ + void onComplete(boolean success); +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngine.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngine.java new file mode 100644 index 00000000000000..fd32b41ba2be08 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngine.java @@ -0,0 +1,147 @@ +// Copyright 2019 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.engine; + +import android.app.Activity; + +import com.google.android.play.core.splitinstall.SplitInstallException; +import com.google.android.play.core.splitinstall.SplitInstallRequest; +import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener; +import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus; + +import org.chromium.base.ThreadUtils; +import org.chromium.base.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Install engine that uses Play Core and SplitCompat to install modules. + */ +class SplitCompatEngine implements InstallEngine { + private final SplitCompatEngineFacade mFacade; + private final SplitInstallStateUpdatedListener mUpdateListener = getStatusUpdateListener(); + private static final Map> sSessions = new HashMap<>(); + + public SplitCompatEngine() { + this(new SplitCompatEngineFacade()); + } + + public SplitCompatEngine(SplitCompatEngineFacade facade) { + mFacade = facade; + mFacade.initApplicationContext(this); + } + + @Override + public void initActivity(Activity activity) { + mFacade.installActivity(activity); + } + + @Override + public boolean isInstalled(String moduleName) { + Set installedModules = mFacade.getSplitManager().getInstalledModules(); + return installedModules.contains(moduleName); + } + + @Override + public void installDeferred(String moduleName) { + mFacade.getSplitManager().deferredInstall(Collections.singletonList(moduleName)); + mFacade.getLogger().logRequestDeferredStart(moduleName); + } + + @Override + public void install(String moduleName, InstallListener listener) { + ThreadUtils.assertOnUiThread(); + + if (sSessions.containsKey(moduleName)) { + sSessions.get(moduleName).add(listener); + return; + } + + registerUpdateListener(); + + sSessions.put(moduleName, new ArrayList() { + { add(listener); } + }); + + SplitInstallRequest request = mFacade.createSplitInstallRequest(moduleName); + + mFacade.getSplitManager().startInstall(request).addOnFailureListener(ex -> { + // TODO(fredmello): look into potential issues with mixing split error code + // with our logger codes - fix accordingly. + mFacade.getLogger().logRequestFailure(moduleName, + ex instanceof SplitInstallException + ? ((SplitInstallException) ex).getErrorCode() + : mFacade.getLogger().getUnknownRequestErrorCode()); + + String message = String.format(Locale.US, "Request Exception: %s", ex.getMessage()); + notifyListeners(moduleName, false); + }); + + mFacade.getLogger().logRequestStart(moduleName); + } + + private SplitInstallStateUpdatedListener getStatusUpdateListener() { + return state -> { + if (state.moduleNames().size() != 1) { + throw new UnsupportedOperationException("Only one module supported."); + } + + int status = state.status(); + String moduleName = state.moduleNames().get(0); + + switch (status) { + case SplitInstallSessionStatus.INSTALLED: + notifyListeners(moduleName, true); + break; + case SplitInstallSessionStatus.FAILED: + notifyListeners(moduleName, false); + mFacade.getLogger().logStatusFailure(moduleName, state.errorCode()); + break; + } + + mFacade.getLogger().logStatus(moduleName, status); + }; + } + + private void notifyListeners(String moduleName, Boolean success) { + for (InstallListener listener : sSessions.get(moduleName)) { + notifyListener(listener, success); + } + + sSessions.remove(moduleName); + unregisterUpdateListener(); + } + + protected void notifyListener(InstallListener listener, Boolean success) { + if (success) { + mFacade.notifyObservers(); + } + + listener.onComplete(success); + } + + private void registerUpdateListener() { + if (sSessions.size() == 0) { + mFacade.getSplitManager().registerListener(mUpdateListener); + } + } + + private void unregisterUpdateListener() { + if (sSessions.size() == 0) { + mFacade.getSplitManager().unregisterListener(mUpdateListener); + } + } + + @VisibleForTesting + public void resetSessionQueue() { + sSessions.clear(); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngineFacade.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngineFacade.java new file mode 100644 index 00000000000000..4fda2139dae1da --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/engine/SplitCompatEngineFacade.java @@ -0,0 +1,72 @@ +// Copyright 2019 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.engine; + +import android.app.Activity; + +import com.google.android.play.core.splitcompat.SplitCompat; +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 org.chromium.base.ApplicationStatus; +import org.chromium.base.ContextUtils; +import org.chromium.components.module_installer.logger.Logger; +import org.chromium.components.module_installer.logger.PlayCoreLogger; +import org.chromium.components.module_installer.observer.ActivityObserver; +import org.chromium.components.module_installer.observer.InstallerObserver; +import org.chromium.components.module_installer.util.ModuleUtil; + +/** + * PlayCore SplitCompatEngine Context. Class used to segregate external dependencies that + * cannot be easily mocked and simplify the engine's design. + */ +class SplitCompatEngineFacade { + private InstallerObserver mObserver; + + private final SplitInstallManager mSplitManager; + private final Logger mLogger; + + public SplitCompatEngineFacade() { + this(SplitInstallManagerFactory.create(ContextUtils.getApplicationContext()), + new PlayCoreLogger()); + } + + public SplitCompatEngineFacade(SplitInstallManager manager, Logger umaLogger) { + mSplitManager = manager; + mLogger = umaLogger; + } + + public Logger getLogger() { + return mLogger; + } + + public SplitInstallManager getSplitManager() { + return mSplitManager; + } + + public void initApplicationContext(InstallEngine engine) { + ModuleUtil.initApplication(); + + // Initializes the ActivityObserver. + ActivityObserver observer = new ActivityObserver(engine); + ApplicationStatus.registerStateListenerForAllActivities(observer); + mObserver = observer; + } + + public void installActivity(Activity activity) { + // Note that SplitCompat (install) needs to be called on the Application Context prior + // to calling this method - this is guaranteed by the behavior of SplitCompatEngine. + SplitCompat.installActivity(activity); + } + + public void notifyObservers() { + mObserver.onModuleInstalled(); + } + + public SplitInstallRequest createSplitInstallRequest(String moduleName) { + return SplitInstallRequest.newBuilder().addModule(moduleName).build(); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/Logger.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/Logger.java new file mode 100644 index 00000000000000..835ee8aa827625 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/Logger.java @@ -0,0 +1,57 @@ +// Copyright 2019 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.logger; + +import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode; + +/** + * Logger for SplitCompat Engine. + */ +public interface Logger { + /** + * Logs exceptions that happen during module request. + * + * @param moduleName The module name. + * @param status The error code. + */ + void logRequestFailure(String moduleName, @SplitInstallErrorCode int status); + + /** + * Logs exceptions that happen during the installation process. + * + * @param moduleName The module name. + * @param status The error code. + */ + void logStatusFailure(String moduleName, @SplitInstallErrorCode int status); + + /** + * Logs the status count and duration during a module installation process. + * + * @param moduleName The module name + * @param status The status code + */ + void logStatus(String moduleName, @SplitInstallErrorCode int status); + + /** + * Logs the request start time. + * + * @param moduleName The module name. + */ + void logRequestStart(String moduleName); + + /** + * Logs when a module has its install deferred. + * + * @param moduleName The module name. + */ + void logRequestDeferredStart(String moduleName); + + /** + * Gets the error code for an unknown error thrown at module request time. + * + * @return The error code. + */ + int getUnknownRequestErrorCode(); +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/PlayCoreLogger.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/PlayCoreLogger.java new file mode 100644 index 00000000000000..7d742d7e93445b --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/PlayCoreLogger.java @@ -0,0 +1,75 @@ +// Copyright 2019 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.logger; + +import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode; +import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus; + +/** + * Concrete Logger for SplitCompat Installers (proxy to specific loggers). + */ +public class PlayCoreLogger implements Logger { + private final SplitInstallFailureLogger mFailureLogger; + private final SplitInstallStatusLogger mStatusLogger; + private final SplitAvailabilityLogger mAvailabilityLogger; + + public PlayCoreLogger() { + this(new SplitInstallFailureLogger(), new SplitInstallStatusLogger(), + new SplitAvailabilityLogger()); + } + + public PlayCoreLogger(SplitInstallFailureLogger failureLogger, + SplitInstallStatusLogger statusLogger, SplitAvailabilityLogger availabilityLogger) { + mFailureLogger = failureLogger; + mStatusLogger = statusLogger; + mAvailabilityLogger = availabilityLogger; + } + + @Override + public void logRequestFailure(String moduleName, @SplitInstallErrorCode int status) { + mFailureLogger.logRequestFailure(moduleName, status); + } + + @Override + public void logStatusFailure(String moduleName, @SplitInstallErrorCode int status) { + mFailureLogger.logStatusFailure(moduleName, status); + } + + @Override + public void logStatus(String moduleName, @SplitInstallSessionStatus int status) { + mStatusLogger.logStatusChange(moduleName, status); + + if (status == SplitInstallSessionStatus.INSTALLED) { + mAvailabilityLogger.storeModuleInstalled(moduleName, status); + mAvailabilityLogger.logInstallTimes(moduleName); + + // Keep old behavior where we log a 'success' bit with all other failures. + mFailureLogger.logStatusSuccess(moduleName); + } else if (status == SplitInstallSessionStatus.CANCELED) { + // Keep old behavior where we log a 'canceled' bit with all other failures. + mFailureLogger.logStatusCanceled(moduleName); + } else if (status == SplitInstallSessionStatus.DOWNLOADED) { + // Keep old behavior where we log a 'no split compat' bit with all other failures. + mFailureLogger.logStatusNoSplitCompat(moduleName); + } + } + + @Override + public void logRequestStart(String moduleName) { + mStatusLogger.logRequestStart(moduleName); + mAvailabilityLogger.storeRequestStart(moduleName); + } + + @Override + public void logRequestDeferredStart(String moduleName) { + mStatusLogger.logRequestDeferredStart(moduleName); + mAvailabilityLogger.storeRequestDeferredStart(moduleName); + } + + @Override + public int getUnknownRequestErrorCode() { + return SplitInstallFailureLogger.UNKNOWN_REQUEST_ERROR; + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitAvailabilityLogger.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitAvailabilityLogger.java new file mode 100644 index 00000000000000..a638bd7e442158 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitAvailabilityLogger.java @@ -0,0 +1,175 @@ +// Copyright 2019 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.logger; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.SystemClock; +import android.util.SparseLongArray; + +import com.google.android.play.core.splitinstall.SplitInstallManager; +import com.google.android.play.core.splitinstall.SplitInstallManagerFactory; +import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus; + +import org.chromium.base.ContextUtils; +import org.chromium.base.metrics.CachedMetrics.EnumeratedHistogramSample; +import org.chromium.base.metrics.RecordHistogram; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 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. + */ +public class SplitAvailabilityLogger { + // These values are persisted to logs. Entries should not be renumbered and + // numeric values should never be reused. + private static final int REQUESTED = 0; + private static final int INSTALLED_REQUESTED = 1; + private static final int INSTALLED_UNREQUESTED = 2; + + // Keep this one at the end and increment appropriately when adding new status. + private static final int COUNT = 3; + + private static final String ONDEMAND_REQ_PREV = "key_modules_requested_previously"; + private static final String DEFERRED_REQ_PREV = "key_modules_deferred_requested_previously"; + + private final Map mInstallTimesMap = new HashMap<>(); + + /** + * Records via UMA all modules that have been requested and are currently installed. + */ + public static void logModuleAvailability() { + SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); + Set requestedModules = new HashSet<>(); + requestedModules.addAll(prefs.getStringSet(ONDEMAND_REQ_PREV, new HashSet<>())); + requestedModules.addAll(prefs.getStringSet(DEFERRED_REQ_PREV, new HashSet<>())); + + Context context = ContextUtils.getApplicationContext(); + SplitInstallManager manager = SplitInstallManagerFactory.create(context); + Set installedModules = manager.getInstalledModules(); + + for (String name : requestedModules) { + recordAvailabilityStatus( + name, installedModules.contains(name) ? INSTALLED_REQUESTED : REQUESTED); + } + + for (String name : installedModules) { + if (!requestedModules.contains(name)) { + recordAvailabilityStatus(name, INSTALLED_UNREQUESTED); + } + } + } + + private static void recordAvailabilityStatus(String moduleName, int status) { + String key = "Android.FeatureModules.AvailabilityStatus." + moduleName; + EnumeratedHistogramSample sample = new EnumeratedHistogramSample(key, COUNT); + sample.record(status); + } + + /** + * Records via UMA module install times divided into install steps. + * + * @param moduleName The module name. + */ + public void logInstallTimes(String moduleName) { + recordInstallTime(moduleName, "", SplitInstallSessionStatus.UNKNOWN, + SplitInstallSessionStatus.INSTALLED); + recordInstallTime(moduleName, ".PendingDownload", SplitInstallSessionStatus.UNKNOWN, + SplitInstallSessionStatus.DOWNLOADING); + recordInstallTime(moduleName, ".Download", SplitInstallSessionStatus.DOWNLOADING, + SplitInstallSessionStatus.INSTALLING); + recordInstallTime(moduleName, ".Installing", SplitInstallSessionStatus.INSTALLING, + SplitInstallSessionStatus.INSTALLED); + } + + /** + * Records the start time of an on-demand install request. + * + * @param moduleName The module name. + */ + public void storeRequestStart(String moduleName) { + assert !mInstallTimesMap.containsKey(moduleName); + boolean moduleRequested = storeModuleRequested(moduleName, ONDEMAND_REQ_PREV); + mInstallTimesMap.put(moduleName, new InstallTimes(moduleRequested)); + } + + /** + * Records module deferred requested. + * + * @param moduleName The module name. + */ + public void storeRequestDeferredStart(String moduleName) { + storeModuleRequested(moduleName, DEFERRED_REQ_PREV); + } + + /** + * Records that a module has been installed on-demand. + * + * @param moduleName The module name. + * @param status The install status. + */ + public void storeModuleInstalled(String moduleName, int status) { + InstallTimes times = mInstallTimesMap.get(moduleName); + times.mInstallTimes.put(status, SystemClock.uptimeMillis()); + } + + /** + * 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 boolean storeModuleRequested(String moduleName, String prefKey) { + SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); + Set modulesRequestedPreviously = prefs.getStringSet(prefKey, new HashSet<>()); + Set newModulesRequestedPreviously = new HashSet<>(modulesRequestedPreviously); + newModulesRequestedPreviously.add(moduleName); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(prefKey, newModulesRequestedPreviously); + editor.apply(); + return modulesRequestedPreviously.contains(moduleName); + } + + private void recordInstallTime( + String moduleName, String histogramSubname, int startKey, int endKey) { + assert mInstallTimesMap.containsKey(moduleName); + + InstallTimes installTimes = mInstallTimesMap.get(moduleName); + long startTime = installTimes.mInstallTimes.get(startKey); + long endTime = installTimes.mInstallTimes.get(endKey); + + if (startTime == 0 || endTime == 0) { + // Time stamps for install times have not been stored. + // Don't record anything to not skew data. + return; + } + + String cacheKey = installTimes.mIsCached ? "Cached" : "Uncached"; + long timing = endTime - startTime; + String key = String.format("Android.FeatureModules.%sAwakeInstallDuration%s.%s", cacheKey, + histogramSubname, moduleName); + + RecordHistogram.recordLongTimesHistogram(key, timing); + } + + private static class InstallTimes { + public final boolean mIsCached; + public final SparseLongArray mInstallTimes = new SparseLongArray(); + + public InstallTimes(boolean isCached) { + mIsCached = isCached; + mInstallTimes.put(SplitInstallSessionStatus.UNKNOWN, SystemClock.uptimeMillis()); + } + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallFailureLogger.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallFailureLogger.java new file mode 100644 index 00000000000000..7c09d9ba3ead07 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallFailureLogger.java @@ -0,0 +1,104 @@ +// Copyright 2019 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.logger; + +import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode; + +import org.chromium.base.metrics.RecordHistogram; + +class SplitInstallFailureLogger { + // 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 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 CANCELLATION = 3; + private static final int ACCESS_DENIED = 4; + private static final int ACTIVE_SESSIONS_LIMIT_EXCEEDED = 5; + private static final int API_NOT_AVAILABLE = 6; + private static final int INCOMPATIBLE_WITH_EXISTING_SESSION = 7; + private static final int INSUFFICIENT_STORAGE = 8; + private static final int INVALID_REQUEST = 9; + private static final int MODULE_UNAVAILABLE = 10; + private static final int NETWORK_ERROR = 11; + private static final int NO_ERROR = 12; + private static final int SERVICE_DIED = 13; + private static final int SESSION_NOT_FOUND = 14; + private static final int SPLITCOMPAT_COPY_ERROR = 15; + private static final int SPLITCOMPAT_EMULATION_ERROR = 16; + private static final int SPLITCOMPAT_VERIFICATION_ERROR = 17; + private static final int INTERNAL_ERROR = 18; + private static final int UNKNOWN_SPLITINSTALL_ERROR = 19; + public static final int UNKNOWN_REQUEST_ERROR = 20; + private static final int NO_SPLITCOMPAT = 21; + + // Keep this one at the end and increment appropriately when adding new status. + private static final int COUNT = 22; + + private int getHistogramCode(@SplitInstallErrorCode int errorCode) { + switch (errorCode) { + case SplitInstallErrorCode.NO_ERROR: + return NO_ERROR; + case SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED: + return ACTIVE_SESSIONS_LIMIT_EXCEEDED; + case SplitInstallErrorCode.MODULE_UNAVAILABLE: + return MODULE_UNAVAILABLE; + case SplitInstallErrorCode.INVALID_REQUEST: + return INVALID_REQUEST; + case SplitInstallErrorCode.SESSION_NOT_FOUND: + return SESSION_NOT_FOUND; + case SplitInstallErrorCode.API_NOT_AVAILABLE: + return API_NOT_AVAILABLE; + case SplitInstallErrorCode.NETWORK_ERROR: + return NETWORK_ERROR; + case SplitInstallErrorCode.ACCESS_DENIED: + return ACCESS_DENIED; + case SplitInstallErrorCode.INCOMPATIBLE_WITH_EXISTING_SESSION: + return INCOMPATIBLE_WITH_EXISTING_SESSION; + case SplitInstallErrorCode.SERVICE_DIED: + return SERVICE_DIED; + case SplitInstallErrorCode.INSUFFICIENT_STORAGE: + return INSUFFICIENT_STORAGE; + case SplitInstallErrorCode.SPLITCOMPAT_VERIFICATION_ERROR: + return SPLITCOMPAT_VERIFICATION_ERROR; + case SplitInstallErrorCode.SPLITCOMPAT_EMULATION_ERROR: + return SPLITCOMPAT_EMULATION_ERROR; + case SplitInstallErrorCode.SPLITCOMPAT_COPY_ERROR: + return SPLITCOMPAT_COPY_ERROR; + case SplitInstallErrorCode.INTERNAL_ERROR: + return INTERNAL_ERROR; + } + + return -1; + } + + public void logStatusSuccess(String moduleName) { + log(moduleName, SUCCESS); + } + + public void logStatusCanceled(String moduleName) { + log(moduleName, CANCELLATION); + } + + public void logStatusNoSplitCompat(String moduleName) { + log(moduleName, NO_SPLITCOMPAT); + } + + public void logStatusFailure(String moduleName, @SplitInstallErrorCode int status) { + Integer code = getHistogramCode(status); + log(moduleName, code == -1 ? UNKNOWN_SPLITINSTALL_ERROR : code); + } + + public void logRequestFailure(String moduleName, @SplitInstallErrorCode int status) { + Integer code = getHistogramCode(status); + log(moduleName, code == -1 ? UNKNOWN_REQUEST_ERROR : code); + } + + private void log(String moduleName, int code) { + String name = "Android.FeatureModules.InstallStatus." + moduleName; + RecordHistogram.recordEnumeratedHistogram(name, code, COUNT); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallStatusLogger.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallStatusLogger.java new file mode 100644 index 00000000000000..ddc8f7d0b84dbc --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitInstallStatusLogger.java @@ -0,0 +1,72 @@ +// Copyright 2019 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.logger; + +import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus; + +import org.chromium.base.metrics.RecordHistogram; + +class SplitInstallStatusLogger { + // FeatureModuleInstallingStatus 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 UNKNOWN_CODE = 0; + private static final int REQUESTED = 1; + private static final int PENDING = 2; + private static final int DOWNLOADING = 3; + private static final int DOWNLOADED = 4; + private static final int INSTALLING = 5; + private static final int INSTALLED = 6; + private static final int FAILED = 7; + private static final int CANCELING = 8; + private static final int CANCELED = 9; + private static final int REQUIRES_USER_CONFIRMATION = 10; + private static final int REQUESTED_DEFERRED = 11; + + // Keep this one at the end and increment appropriately when adding new status. + private static final int COUNT = 12; + + private int getHistogramCode(@SplitInstallSessionStatus int code) { + switch (code) { + case SplitInstallSessionStatus.PENDING: + return PENDING; + case SplitInstallSessionStatus.DOWNLOADING: + return DOWNLOADING; + case SplitInstallSessionStatus.DOWNLOADED: + return DOWNLOADED; + case SplitInstallSessionStatus.INSTALLING: + return INSTALLING; + case SplitInstallSessionStatus.INSTALLED: + return INSTALLED; + case SplitInstallSessionStatus.FAILED: + return FAILED; + case SplitInstallSessionStatus.CANCELING: + return CANCELING; + case SplitInstallSessionStatus.CANCELED: + return CANCELED; + case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION: + return REQUIRES_USER_CONFIRMATION; + } + + return UNKNOWN_CODE; + } + + public void logStatusChange(String moduleName, @SplitInstallSessionStatus int status) { + recordInstallStatus(moduleName, getHistogramCode(status)); + } + + public void logRequestStart(String moduleName) { + recordInstallStatus(moduleName, REQUESTED); + } + + public void logRequestDeferredStart(String moduleName) { + recordInstallStatus(moduleName, REQUESTED_DEFERRED); + } + + private void recordInstallStatus(String moduleName, int status) { + String name = "Android.FeatureModules.InstallingStatus." + moduleName; + RecordHistogram.recordEnumeratedHistogram(name, status, COUNT); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ModuleActivityObserver.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/observer/ActivityObserver.java similarity index 66% rename from components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ModuleActivityObserver.java rename to components/module_installer/android/java/src/org/chromium/components/module_installer/observer/ActivityObserver.java index 650fe8f56ef931..378dc215e08a3d 100644 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ModuleActivityObserver.java +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/observer/ActivityObserver.java @@ -2,13 +2,14 @@ // 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.observers; +package org.chromium.components.module_installer.observer; import android.app.Activity; import org.chromium.base.ActivityState; import org.chromium.base.ApplicationStatus; import org.chromium.base.ThreadUtils; +import org.chromium.components.module_installer.engine.InstallEngine; import java.util.HashSet; @@ -17,16 +18,19 @@ * Note that ActivityIds are managed globally and therefore any changes to it are to be made * using a single thread (in this case, the UI thread). */ -public class ModuleActivityObserver implements ApplicationStatus.ActivityStateListener { +public class ActivityObserver + implements InstallerObserver, ApplicationStatus.ActivityStateListener { private static HashSet sActivityIds = new HashSet(); - private final ObserverStrategy mStrategy; + private final ActivityObserverFacade mFacade; + private final InstallEngine mInstallEngine; - public ModuleActivityObserver() { - this(new ObserverStrategyImpl()); + public ActivityObserver(InstallEngine installEngine) { + this(new ActivityObserverFacade(), installEngine); } - public ModuleActivityObserver(ObserverStrategy strategy) { - mStrategy = strategy; + public ActivityObserver(ActivityObserverFacade facade, InstallEngine installEngine) { + mFacade = facade; + mInstallEngine = installEngine; } @Override @@ -41,13 +45,14 @@ public void onActivityStateChange(Activity activity, @ActivityState int newState } /** Makes activities aware of a DFM install and prepare them to be able to use new modules. */ + @Override public void onModuleInstalled() { ThreadUtils.assertOnUiThread(); sActivityIds.clear(); - for (Activity activity : mStrategy.getRunningActivities()) { - if (mStrategy.getStateForActivity(activity) == ActivityState.RESUMED) { + for (Activity activity : mFacade.getRunningActivities()) { + if (mFacade.getStateForActivity(activity) == ActivityState.RESUMED) { splitCompatActivity(activity); } } @@ -58,7 +63,7 @@ private void splitCompatActivity(Activity activity) { Integer key = activity.hashCode(); if (!sActivityIds.contains(key)) { sActivityIds.add(key); - mStrategy.getModuleInstaller().initActivity(activity); + mInstallEngine.initActivity(activity); } } -} \ No newline at end of file +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ObserverStrategyImpl.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/observer/ActivityObserverFacade.java similarity index 55% rename from components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ObserverStrategyImpl.java rename to components/module_installer/android/java/src/org/chromium/components/module_installer/observer/ActivityObserverFacade.java index 414d9cccf77f54..52c36f881d5eb4 100644 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ObserverStrategyImpl.java +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/observer/ActivityObserverFacade.java @@ -2,29 +2,24 @@ // 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.observers; +package org.chromium.components.module_installer.observer; import android.app.Activity; import org.chromium.base.ApplicationStatus; -import org.chromium.components.module_installer.ModuleInstaller; import java.util.List; -/** Strategy utilizing ModuleInstaller and ApplicationStatus. */ -/* package */ class ObserverStrategyImpl implements ObserverStrategy { - @Override - public ModuleInstaller getModuleInstaller() { - return ModuleInstaller.getInstance(); - } - - @Override +/** + * ActivityObserver Context. Class used to segregate external dependencies that + * cannot be easily mocked and simplify the observer's design. + */ +class ActivityObserverFacade { public List getRunningActivities() { return ApplicationStatus.getRunningActivities(); } - @Override public int getStateForActivity(Activity activity) { return ApplicationStatus.getStateForActivity(activity); } -} \ No newline at end of file +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/observer/InstallerObserver.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/observer/InstallerObserver.java new file mode 100644 index 00000000000000..c7b41530296a66 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/observer/InstallerObserver.java @@ -0,0 +1,10 @@ +// Copyright 2019 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.observer; + +/** + * Listener for 'module installed' notifications. + */ +public interface InstallerObserver { void onModuleInstalled(); } \ No newline at end of file diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ObserverStrategy.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ObserverStrategy.java deleted file mode 100644 index f50ef963f87c5f..00000000000000 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/observers/ObserverStrategy.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019 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.observers; - -import android.app.Activity; - -import org.chromium.components.module_installer.ModuleInstaller; - -import java.util.List; - -/** Interface outlining the necessary strategy to load activities and install modules. */ -/* package */ interface ObserverStrategy { - ModuleInstaller getModuleInstaller(); - List getRunningActivities(); - int getStateForActivity(Activity activity); -} \ No newline at end of file diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/util/CrashKeyRecorder.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/CrashKeyRecorder.java new file mode 100644 index 00000000000000..23c062ee79869e --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/CrashKeyRecorder.java @@ -0,0 +1,72 @@ +// Copyright 2019 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.util; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.text.TextUtils; + +import com.google.android.play.core.splitinstall.SplitInstallManager; +import com.google.android.play.core.splitinstall.SplitInstallManagerFactory; + +import org.chromium.base.BuildInfo; +import org.chromium.base.ContextUtils; +import org.chromium.components.crash.CrashKeyIndex; +import org.chromium.components.crash.CrashKeys; + +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +/** + * CrashKey Recorder for installed modules. + */ +class CrashKeyRecorder { + public static void updateCrashKeys() { + Context context = ContextUtils.getApplicationContext(); + CrashKeys ck = CrashKeys.getInstance(); + + // Get modules that are fully installed as split APKs (excluding base which is always + // installed). Tree set to have ordered and, thus, deterministic results. + Set fullyInstalledModules = new TreeSet<>(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Split APKs are only supported on Android L+. + try { + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageInfo(BuildInfo.getInstance().packageName, 0); + if (packageInfo.splitNames != null) { + fullyInstalledModules.addAll(Arrays.asList(packageInfo.splitNames)); + } + } catch (NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + // Create temporary split install manager to retrieve both fully installed and emulated + // modules. Then remove fully installed ones to get emulated ones only. Querying the + // installed modules can only be done if splitcompat has already been called. Otherwise, + // emulation of later modules won't work. If splitcompat has not been called no modules + // are emulated. Therefore, use an empty set in that case. + Set emulatedModules = new TreeSet<>(); + if (SplitCompatInitializer.isInitialized()) { + SplitInstallManager manager = SplitInstallManagerFactory.create(context); + emulatedModules.addAll(manager.getInstalledModules()); + emulatedModules.removeAll(fullyInstalledModules); + } + + ck.set(CrashKeyIndex.INSTALLED_MODULES, encodeCrashKeyValue(fullyInstalledModules)); + ck.set(CrashKeyIndex.EMULATED_MODULES, encodeCrashKeyValue(emulatedModules)); + } + + private static String encodeCrashKeyValue(Set moduleNames) { + if (moduleNames.isEmpty()) return ""; + // Values with dots are interpreted as URLs. Some module names have dots in them. Make sure + // they don't get sanitized. + return TextUtils.join(",", moduleNames).replace('.', '$'); + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/util/ModuleUtil.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/ModuleUtil.java new file mode 100644 index 00000000000000..3825d8602ab107 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/ModuleUtil.java @@ -0,0 +1,49 @@ +// Copyright 2019 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.util; + +import org.chromium.base.annotations.MainDex; +import org.chromium.components.module_installer.logger.SplitAvailabilityLogger; + +/** + * Utilitary class (proxy) exposing DFM functionality to the broader application. + */ +@MainDex +public class ModuleUtil { + /** + * Records the execution time (ms) taken by the module installer framework. + */ + public static void recordStartupTime() { + Timer.recordStartupTime(); + } + + /** + * Records the start time in order to later report the install duration via UMA. + */ + public static void recordModuleAvailability() { + try (Timer timer = new Timer()) { + initApplication(); + SplitAvailabilityLogger.logModuleAvailability(); + } + } + + /** + * Updates the CrashKey report containing modules currently present. + */ + public static void updateCrashKeys() { + try (Timer timer = new Timer()) { + CrashKeyRecorder.updateCrashKeys(); + } + } + + /** + * Initializes the PlayCore SplitCompat framework. + */ + public static void initApplication() { + try (Timer timer = new Timer()) { + SplitCompatInitializer.initApplication(); + } + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/util/SplitCompatInitializer.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/SplitCompatInitializer.java new file mode 100644 index 00000000000000..4c73f0b4f18e46 --- /dev/null +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/SplitCompatInitializer.java @@ -0,0 +1,36 @@ +// Copyright 2019 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.util; + +import com.google.android.play.core.splitcompat.SplitCompat; + +import org.chromium.base.ContextUtils; +import org.chromium.base.StrictModeContext; +import org.chromium.base.ThreadUtils; + +/** + * PlayCore SplitCompat initializer for installing modules in the application context. + */ +class SplitCompatInitializer { + private static volatile boolean sIsInitialized; + + public static void initApplication() { + ThreadUtils.assertOnUiThread(); + + if (sIsInitialized) { + return; + } + + // SplitCompat.install may copy modules into Chrome's internal folder or clean them up. + try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { + SplitCompat.install(ContextUtils.getApplicationContext()); + } + sIsInitialized = true; + } + + public static boolean isInitialized() { + return sIsInitialized; + } +} diff --git a/components/module_installer/android/java/src/org/chromium/components/module_installer/Timer.java b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/Timer.java similarity index 70% rename from components/module_installer/android/java/src/org/chromium/components/module_installer/Timer.java rename to components/module_installer/android/java/src/org/chromium/components/module_installer/util/Timer.java index 03627f8d07bd9b..73b7544718a3e5 100644 --- a/components/module_installer/android/java/src/org/chromium/components/module_installer/Timer.java +++ b/components/module_installer/android/java/src/org/chromium/components/module_installer/util/Timer.java @@ -2,10 +2,12 @@ // 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; +package org.chromium.components.module_installer.util; import android.os.SystemClock; +import org.chromium.base.metrics.CachedMetrics.TimesHistogramSample; + import java.io.Closeable; /** @@ -14,13 +16,13 @@ * * This should only be used on the UI thread to avoid race conditions. */ -/* package */ class Timer implements Closeable { +public class Timer implements Closeable { private static Timer sCurrentTimer; private static long sTotalTime; private final long mStartTime; - /* package */ Timer() { + public Timer() { mStartTime = SystemClock.uptimeMillis(); if (sCurrentTimer == null) { sCurrentTimer = this; @@ -35,7 +37,9 @@ public void close() { } } - /* package */ static long getTotalTime() { - return sTotalTime; + public static void recordStartupTime() { + String name = "Android.FeatureModules.StartupTime"; + TimesHistogramSample sample = new TimesHistogramSample(name); + sample.record(sTotalTime); } } diff --git a/components/module_installer/android/javatests/src/org/chromium/components/module_installer/ModuleInstallerRule.java b/components/module_installer/android/javatests/src/org/chromium/components/module_installer/ModuleInstallerRule.java deleted file mode 100644 index f9a9418f09746d..00000000000000 --- a/components/module_installer/android/javatests/src/org/chromium/components/module_installer/ModuleInstallerRule.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019 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 org.junit.rules.ExternalResource; - -/** - * This rule allows the caller to specify their own {@link ModuleInstaller} for the duration of the - * test and resets it back to what it was before. - * - * TODO(wnwen): This should eventually become ModuleConfigRule. - */ -public class ModuleInstallerRule extends ExternalResource { - private ModuleInstaller mOldModuleInstaller; - private final ModuleInstaller mMockModuleInstaller; - - public ModuleInstallerRule(ModuleInstaller mockModuleInstaller) { - mMockModuleInstaller = mockModuleInstaller; - } - - @Override - protected void before() { - mOldModuleInstaller = ModuleInstaller.getInstance(); - ModuleInstaller.setInstanceForTesting(mMockModuleInstaller); - } - - @Override - protected void after() { - ModuleInstaller.setInstanceForTesting(mOldModuleInstaller); - } -} diff --git a/components/module_installer/android/junit/src/org/chromium/components/module_installer/engine/SplitCompatEngineTest.java b/components/module_installer/android/junit/src/org/chromium/components/module_installer/engine/SplitCompatEngineTest.java new file mode 100644 index 00000000000000..f28f694a2856ae --- /dev/null +++ b/components/module_installer/android/junit/src/org/chromium/components/module_installer/engine/SplitCompatEngineTest.java @@ -0,0 +1,308 @@ +// Copyright 2019 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.engine; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Activity; + +import com.google.android.play.core.splitinstall.SplitInstallException; +import com.google.android.play.core.splitinstall.SplitInstallManager; +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 com.google.android.play.core.tasks.OnFailureListener; +import com.google.android.play.core.tasks.Task; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.components.module_installer.logger.Logger; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Test suite for the SplitCompatEngine class. + */ +@RunWith(BaseRobolectricTestRunner.class) +public class SplitCompatEngineTest { + @Mock + private Logger mLogger; + @Mock + private SplitInstallManager mManager; + @Mock + private SplitInstallRequest mInstallRequest; + @Mock + private Task mTask; + + private SplitCompatEngine mInstaller; + private SplitCompatEngineFacade mInstallerFacade; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mInstallerFacade = mock(SplitCompatEngineFacade.class); + + // Mock SplitCompatEngineFacade. + doReturn(mLogger).when(mInstallerFacade).getLogger(); + doReturn(mManager).when(mInstallerFacade).getSplitManager(); + doReturn(mInstallRequest).when(mInstallerFacade).createSplitInstallRequest(any()); + + // Mock SplitInstallManager. + doReturn(mTask).when(mManager).startInstall(any()); + + mInstaller = new SplitCompatEngine(mInstallerFacade); + + mInstaller.resetSessionQueue(); + } + + @Test + public void whenConstructed_verifySplitInitialized() { + // Arrange. + InOrder inOrder = inOrder(mInstallerFacade, mManager); + + // Act & Assert. + inOrder.verify(mInstallerFacade).initApplicationContext(mInstaller); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void whenInitActivity_verifyActivityInstalled() { + // Arrange. + Activity activityMock = mock(Activity.class); + + // Act. + mInstaller.initActivity(activityMock); + + // Assert. + verify(mInstallerFacade, times(1)).installActivity(activityMock); + } + + @Test + public void whenIsInstalled_verifyModuleIsInstalled() { + // Arrange. + String installedModule = "m1"; + String uninstalledModule = "m2"; + Set installedModules = new HashSet() { + { add(installedModule); } + }; + doReturn(installedModules).when(mManager).getInstalledModules(); + + // Act & Assert. + assertTrue(mInstaller.isInstalled(installedModule)); + assertFalse(mInstaller.isInstalled(uninstalledModule)); + } + + @Test + public void whenInstallDeferred_verifyModuleInstalled() { + // Arrange. + String moduleName = "whenInstallDeferred_verifyModuleInstalled"; + List moduleList = Collections.singletonList(moduleName); + + // Act. + mInstaller.installDeferred(moduleName); + + // Assert. + verify(mManager, times(1)).deferredInstall(moduleList); + verify(mLogger, times(1)).logRequestDeferredStart(moduleName); + } + + @Test + public void whenInstalling_verifyInstallSequence() { + // Arrange. + String moduleName = "whenInstalling_verifyInstallSequence"; + InstallListener listener = mock(InstallListener.class); + InOrder inOrder = inOrder(mInstallerFacade, mManager, mLogger, mTask); + + // Act. + mInstaller.install(moduleName, listener); + + // Assert. + inOrder.verify(mManager).registerListener(any()); + inOrder.verify(mInstallerFacade).createSplitInstallRequest(moduleName); + inOrder.verify(mManager).startInstall(mInstallRequest); + inOrder.verify(mTask).addOnFailureListener(any()); + inOrder.verify(mLogger).logRequestStart(moduleName); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void whenInstallingSameModuleConcurrently_verifySingleInstall() { + // Arrange. + String moduleName = "whenInstallingSameModuleConcurrently_verifySingleInstall"; + InstallListener listener = mock(InstallListener.class); + SplitCompatEngine instance1 = new SplitCompatEngine(mInstallerFacade); + SplitCompatEngine instance2 = new SplitCompatEngine(mInstallerFacade); + + // Act. + instance1.install(moduleName, listener); + instance1.install(moduleName, listener); + instance2.install(moduleName, listener); + instance2.install(moduleName, listener); + + // Assert. + verify(mInstallerFacade, times(1)).createSplitInstallRequest(moduleName); + } + + @Test + public void whenInstallingWithException_verifyErrorHandled() { + // Arrange. + String moduleName = "whenInstallingWithException_verifyErrorHandled"; + String exceptionMessage = moduleName + "_ex_msg"; + Integer errorCode = -1; + InstallListener listener = mock(InstallListener.class); + ArgumentCaptor arg = ArgumentCaptor.forClass(OnFailureListener.class); + doReturn(errorCode).when(mLogger).getUnknownRequestErrorCode(); + + // Act. + mInstaller.install(moduleName, listener); + verify(mTask).addOnFailureListener(arg.capture()); + arg.getValue().onFailure(new Exception(exceptionMessage)); + + // Assert. + verify(mLogger, times(1)).logRequestFailure(moduleName, errorCode); + verify(listener, times(1)).onComplete(false); + } + + @Test + public void whenInstallingWithSplitException_verifyErrorHandled() { + // Arrange. + String moduleName = "whenInstallingWithSplitException_verifyErrorHandled"; + InstallListener listener = mock(InstallListener.class); + ArgumentCaptor arg = ArgumentCaptor.forClass(OnFailureListener.class); + + // Act. + mInstaller.install(moduleName, listener); + verify(mTask).addOnFailureListener(arg.capture()); + arg.getValue().onFailure(new SplitInstallException(-1)); + + // Assert. + verify(mLogger, times(1)).logRequestFailure(moduleName, -1); + verify(listener, times(1)).onComplete(false); + } + + @Test + public void whenInstallingWithException_verifyCanTryAgainAfterFailure() { + // Arrange. + String moduleName = "whenInstallingWithException_verifyCanTryAgainAfterFailure"; + ArgumentCaptor arg = ArgumentCaptor.forClass(OnFailureListener.class); + + // Act. + mInstaller.install(moduleName, mock(InstallListener.class)); + verify(mTask).addOnFailureListener(arg.capture()); + arg.getValue().onFailure(new Exception("")); + mInstaller.install(moduleName, mock(InstallListener.class)); // 2nd call. + + // Assert. + verify(mLogger, times(2)).logRequestStart(moduleName); + } + + @Test + public void whenInstalled_verifyListenerAndLogger() { + // Arrange. + String moduleName = "whenInstalled_verifyListenerAndLogger"; + Integer status = SplitInstallSessionStatus.INSTALLED; + String message = String.format("Module '%s' Installed", moduleName); + InstallListener listener = mock(InstallListener.class); + + // Mock SplitInstallSessionState + SplitInstallSessionState state = mock(SplitInstallSessionState.class); + doReturn(status).when(state).status(); + doReturn(Arrays.asList(moduleName)).when(state).moduleNames(); + + InOrder inOrder = inOrder(listener, mManager, mLogger); + ArgumentCaptor arg = + ArgumentCaptor.forClass(SplitInstallStateUpdatedListener.class); + + // Act. + mInstaller.install(moduleName, listener); + verify(mManager).registerListener(arg.capture()); + arg.getValue().onStateUpdate(state); + + // Assert. + inOrder.verify(listener, times(1)).onComplete(true); + inOrder.verify(mManager, times(1)).unregisterListener(any()); + inOrder.verify(mLogger, times(1)).logStatus(moduleName, status); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void whenFailureToInstall_verifyListenerAndLogger() { + // Arrange. + String moduleName = "whenFailureToInstall_verifyListenerAndLogger"; + Integer status = SplitInstallSessionStatus.FAILED; + Integer errorCode = SplitInstallErrorCode.NO_ERROR; + String message = String.format("Failed with code: %d", errorCode); + InstallListener listener = mock(InstallListener.class); + + // Mock SplitInstallSessionState. + SplitInstallSessionState state = mock(SplitInstallSessionState.class); + doReturn(status).when(state).status(); + doReturn(errorCode).when(state).errorCode(); + doReturn(Arrays.asList(moduleName)).when(state).moduleNames(); + + InOrder inOrder = inOrder(listener, mLogger, mManager); + ArgumentCaptor arg = + ArgumentCaptor.forClass(SplitInstallStateUpdatedListener.class); + + // Act. + mInstaller.install(moduleName, listener); + verify(mManager).registerListener(arg.capture()); + arg.getValue().onStateUpdate(state); + + // Assert. + inOrder.verify(listener, times(1)).onComplete(false); + inOrder.verify(mManager, times(1)).unregisterListener(any()); + inOrder.verify(mLogger, times(1)).logStatusFailure(moduleName, errorCode); + inOrder.verify(mLogger, times(1)).logStatus(moduleName, status); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void whenNotInstalledOrFailed_verifyStatusLogged() { + // Arrange. + String moduleName = "whenNotInstalledOrFailed_verifyStatusLogged"; + Integer status = SplitInstallSessionStatus.UNKNOWN; + InstallListener listener = mock(InstallListener.class); + + // Mock SplitInstallSessionState. + SplitInstallSessionState state = mock(SplitInstallSessionState.class); + doReturn(status).when(state).status(); + doReturn(Arrays.asList(moduleName)).when(state).moduleNames(); + + InOrder inOrder = inOrder(listener, mLogger); + ArgumentCaptor arg = + ArgumentCaptor.forClass(SplitInstallStateUpdatedListener.class); + + // Act. + mInstaller.install(moduleName, mock(InstallListener.class)); + verify(mManager).registerListener(arg.capture()); + arg.getValue().onStateUpdate(state); + + // Assert. + inOrder.verify(mLogger, times(1)).logStatus(moduleName, status); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/components/module_installer/android/junit/src/org/chromium/components/module_installer/observers/ModuleActivityObserverTest.java b/components/module_installer/android/junit/src/org/chromium/components/module_installer/observer/ActivityObserverTest.java similarity index 68% rename from components/module_installer/android/junit/src/org/chromium/components/module_installer/observers/ModuleActivityObserverTest.java rename to components/module_installer/android/junit/src/org/chromium/components/module_installer/observer/ActivityObserverTest.java index a43fc420e6fec9..0d8a7c385322ea 100644 --- a/components/module_installer/android/junit/src/org/chromium/components/module_installer/observers/ModuleActivityObserverTest.java +++ b/components/module_installer/android/junit/src/org/chromium/components/module_installer/observer/ActivityObserverTest.java @@ -2,7 +2,7 @@ // 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.observers; +package org.chromium.components.module_installer.observer; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; @@ -21,36 +21,35 @@ import org.chromium.base.ActivityState; import org.chromium.base.test.BaseRobolectricTestRunner; -import org.chromium.components.module_installer.ModuleInstaller; +import org.chromium.components.module_installer.engine.InstallEngine; import java.util.ArrayList; import java.util.List; /** - * Test suite for the ModuleActivityObserver class. + * Test suite for the ActivityObserver class. */ @RunWith(BaseRobolectricTestRunner.class) -public class ModuleActivityObserverTest { +public class ActivityObserverTest { @Mock - private ModuleInstaller mModuleInstallerMock; + private InstallEngine mInstallEngineMock; @Mock private Activity mActivityMock; - @Mock - private ObserverStrategy mStrategy; - - private ModuleActivityObserver mObserver; + private ActivityObserverFacade mFacade; + private ActivityObserver mObserver; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mObserver = new ModuleActivityObserver(mStrategy); + mFacade = mock(ActivityObserverFacade.class); + + mObserver = new ActivityObserver(mFacade, mInstallEngineMock); - doReturn(mModuleInstallerMock).when(mStrategy).getModuleInstaller(); - doReturn(new ArrayList<>()).when(mStrategy).getRunningActivities(); - doReturn(ActivityState.CREATED).when(mStrategy).getStateForActivity(any(Activity.class)); + doReturn(new ArrayList<>()).when(mFacade).getRunningActivities(); + doReturn(ActivityState.CREATED).when(mFacade).getStateForActivity(any(Activity.class)); } @Test @@ -63,7 +62,7 @@ public void whenOnCreate_verifySplitCompatted() { mObserver.onActivityStateChange(mActivityMock, newState); // Assert. - verify(mModuleInstallerMock, times(1)).initActivity(mActivityMock); + verify(mInstallEngineMock, times(1)).initActivity(mActivityMock); } @Test @@ -76,7 +75,7 @@ public void whenOnResume_verifySplitCompatted() { mObserver.onActivityStateChange(mActivityMock, newState); // Assert. - verify(mModuleInstallerMock, times(1)).initActivity(mActivityMock); + verify(mInstallEngineMock, times(1)).initActivity(mActivityMock); } @Test @@ -90,7 +89,7 @@ public void whenOnResumeTwice_verifySplitCompattedOnlyOnce() { mObserver.onActivityStateChange(mActivityMock, newState); // Assert. - verify(mModuleInstallerMock, times(1)).initActivity(mActivityMock); + verify(mInstallEngineMock, times(1)).initActivity(mActivityMock); } @Test @@ -105,7 +104,7 @@ public void whenOnResumeAfterModuleInstall_verifySplitCompatted() { mObserver.onActivityStateChange(mActivityMock, newState); // Assert. - verify(mModuleInstallerMock, times(2)).initActivity(mActivityMock); + verify(mInstallEngineMock, times(2)).initActivity(mActivityMock); } @Test @@ -117,7 +116,7 @@ public void whenNotOnResumeOrNotOnCreate_verifyNotSplitCompatted() { mObserver.onActivityStateChange(mActivityMock, ActivityState.DESTROYED); // Assert. - verify(mModuleInstallerMock, never()).initActivity(mActivityMock); + verify(mInstallEngineMock, never()).initActivity(mActivityMock); } @Test @@ -125,14 +124,14 @@ public void whenMultipleInstances_verifySplitCompatCalledOnlyOnce() { // Arrange. @ActivityState Integer newState = ActivityState.RESUMED; - ModuleActivityObserver newObserver = new ModuleActivityObserver(mStrategy); + ActivityObserver newObserver = new ActivityObserver(mFacade, mInstallEngineMock); // Act. mObserver.onActivityStateChange(mActivityMock, newState); newObserver.onActivityStateChange(mActivityMock, newState); // Assert. - verify(mModuleInstallerMock, times(1)).initActivity(mActivityMock); + verify(mInstallEngineMock, times(1)).initActivity(mActivityMock); } @Test @@ -147,16 +146,16 @@ public void whenOnModuleInstalled_verifyOnlyResumedActivitiesAreSplitCompatted() activitiesList.add(activityMock2); activitiesList.add(activityMock3); - doReturn(activitiesList).when(mStrategy).getRunningActivities(); + doReturn(activitiesList).when(mFacade).getRunningActivities(); - doReturn(ActivityState.RESUMED).when(mStrategy).getStateForActivity(activityMock1); - doReturn(ActivityState.PAUSED).when(mStrategy).getStateForActivity(activityMock2); - doReturn(ActivityState.DESTROYED).when(mStrategy).getStateForActivity(activityMock3); + doReturn(ActivityState.RESUMED).when(mFacade).getStateForActivity(activityMock1); + doReturn(ActivityState.PAUSED).when(mFacade).getStateForActivity(activityMock2); + doReturn(ActivityState.DESTROYED).when(mFacade).getStateForActivity(activityMock3); // Act. mObserver.onModuleInstalled(); // Assert. - verify(mModuleInstallerMock, times(1)).initActivity(any(Activity.class)); + verify(mInstallEngineMock, times(1)).initActivity(any(Activity.class)); } } diff --git a/docs/android_dynamic_feature_modules.md b/docs/android_dynamic_feature_modules.md index dc1879ec48c49c..19a0633a06a15b 100644 --- a/docs/android_dynamic_feature_modules.md +++ b/docs/android_dynamic_feature_modules.md @@ -178,7 +178,7 @@ access the module. To do this, add the following in the new file ```java package org.chromium.chrome.features.foo; -import org.chromium.components.module_installer.ModuleInterface; +import org.chromium.components.module_installer.builder.ModuleInterface; /** Interface to call into Foo feature. */ @ModuleInterface(module = "foo", impl = "org.chromium.chrome.features.FooImpl") diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index 9760e9bb5593d0..7cbbe88684aac7 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -21587,6 +21587,23 @@ Called by update_net_error_codes.py.--> + + Unknown update status. + Module requested (on-demand). + Module pending. + Module downloading. + Module downloaded. + Module installing. + Module installed. + Module request failed. + Module request canceling. + Module request canceled. + + Module request requires user confirmation. + + Module requested (deferred). + + Installation succeeded. diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml index e82f6a5867e22a..9a840523f820ff 100644 --- a/tools/metrics/histograms/histograms.xml +++ b/tools/metrics/histograms/histograms.xml @@ -2669,6 +2669,20 @@ uploading your change for review. + + + + tiborg@chromium.org + agrieve@chromium.org + fredmello@chromium.org + + Install status counter for each dynamic feature module. Recorded during + on-demand and deferred installs. + + + name="Android.FeatureModules.CachedInstallDuration.PendingDownload"/> +