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"/> +