diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 94f08c450..8dd8ae226 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -86,7 +86,8 @@ import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.SplashScreen import org.mozilla.fenix.GleanMetrics.StartOnHome -import org.mozilla.fenix.addons.ExtensionsProcessDisabledController +import org.mozilla.fenix.addons.ExtensionsProcessDisabledBackgroundController +import org.mozilla.fenix.addons.ExtensionsProcessDisabledForegroundController import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.bindings.BrowserStoreBinding @@ -181,8 +182,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { ) } - private val extensionsProcessDisabledPromptObserver by lazy { - ExtensionsProcessDisabledController(this@HomeActivity) + private val extensionsProcessDisabledForegroundController by lazy { + ExtensionsProcessDisabledForegroundController(this@HomeActivity) + } + + private val extensionsProcessDisabledBackgroundController by lazy { + ExtensionsProcessDisabledBackgroundController( + browserStore = components.core.store, + appStore = components.appStore, + ) } private val serviceWorkerSupport by lazy { @@ -363,7 +371,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { lifecycle.addObservers( webExtensionPopupObserver, - extensionsProcessDisabledPromptObserver, + extensionsProcessDisabledForegroundController, + extensionsProcessDisabledBackgroundController, serviceWorkerSupport, webExtensionPromptFeature, BrowserStoreBinding(components.core.store, components.appStore), diff --git a/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundController.kt b/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundController.kt new file mode 100644 index 000000000..d3cf9234f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundController.kt @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.addons + +import android.os.Handler +import android.os.Looper +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptObserver +import org.mozilla.fenix.components.AppStore +import kotlin.system.exitProcess + +/** + * Controller for handling extensions process spawning disabled events. This is for when the app is + * in background, the app is killed to prevent extensions from being disabled and network requests + * continuing. + * + * @param browserStore The [BrowserStore] which holds the state for showing the dialog. + * @param appStore The [AppStore] containing the application state. + * @param onExtensionsProcessDisabled Invoked when the app is in background and extensions process + * is disabled. + */ +class ExtensionsProcessDisabledBackgroundController( + browserStore: BrowserStore, + appStore: AppStore, + onExtensionsProcessDisabled: () -> Unit = { killApp() }, +) : ExtensionsProcessDisabledPromptObserver( + store = browserStore, + shouldCancelOnStop = false, + onShowExtensionsProcessDisabledPrompt = { + if (!appStore.state.isForeground) { + onExtensionsProcessDisabled() + } + }, +) { + + companion object { + /** + * When a dialog can't be shown because the app is in the background, instead the app will + * be killed to prevent leaking network data without extensions enabled. + */ + private fun killApp() { + Handler(Looper.getMainLooper()).post { + exitProcess(0) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledController.kt b/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundController.kt similarity index 84% rename from app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledController.kt rename to app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundController.kt index 83317400c..6432ef735 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledController.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundController.kt @@ -5,8 +5,6 @@ package org.mozilla.fenix.addons import android.content.Context -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.widget.Button import android.widget.TextView @@ -20,35 +18,30 @@ import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptO import org.mozilla.fenix.R import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.ext.components -import kotlin.system.exitProcess /** * Controller for handling extensions process spawning disabled events. When the app is in * foreground this will call for a dialog to decide on correct action to take (retry enabling - * process spawning or disable extensions). When in background, we kill the app to prevent - * extensions from being disabled and network requests continuing. + * process spawning or disable extensions). * * @param context to show the AlertDialog * @param browserStore The [BrowserStore] which holds the state for showing the dialog * @param appStore The [AppStore] containing the application state * @param builder to use for creating the dialog which can be styled as needed * @param appName to be added to the message. Optional and mainly relevant for testing - * @param onKillApp called when the app is backgrounded and extensions process is disabled */ -class ExtensionsProcessDisabledController( +class ExtensionsProcessDisabledForegroundController( @UiContext context: Context, browserStore: BrowserStore = context.components.core.store, appStore: AppStore = context.components.appStore, builder: AlertDialog.Builder = AlertDialog.Builder(context), appName: String = context.appName, - onKillApp: () -> Unit = { killApp() }, ) : ExtensionsProcessDisabledPromptObserver( - browserStore, + store = browserStore, + shouldCancelOnStop = true, { if (appStore.state.isForeground) { presentDialog(context, browserStore, builder, appName) - } else { - onKillApp.invoke() } }, ) { @@ -61,16 +54,6 @@ class ExtensionsProcessDisabledController( companion object { private var shouldCreateDialog: Boolean = true - /** - * When a dialog can't be shown because the app is in the background, instead the app will - * be killed to prevent leaking network data without extensions enabled. - */ - private fun killApp() { - Handler(Looper.getMainLooper()).post { - exitProcess(0) - } - } - /** * Present a dialog to the user notifying of extensions process spawning disabled and also asking * whether they would like to continue trying or disable extensions. If the user chooses to retry, diff --git a/app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundControllerTest.kt b/app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundControllerTest.kt new file mode 100644 index 000000000..e64196394 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledBackgroundControllerTest.kt @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.addons + +import mozilla.components.browser.state.action.ExtensionsProcessAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppState + +class ExtensionsProcessDisabledBackgroundControllerTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + private val dispatcher = coroutinesTestRule.testDispatcher + + @Test + fun `WHEN app is backgrounded AND extension process spawning threshold is exceeded THEN onExtensionsProcessDisabled is invoked`() { + val browserStore = BrowserStore(BrowserState()) + val appStore = AppStore(AppState(isForeground = false)) + var invoked = false + + val controller = ExtensionsProcessDisabledBackgroundController( + browserStore, + appStore, + onExtensionsProcessDisabled = { + invoked = true + }, + ) + + controller.start() + + browserStore.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true)) + dispatcher.scheduler.advanceUntilIdle() + browserStore.waitUntilIdle() + + assertTrue(invoked) + } + + @Test + fun `WHEN app is in foreground AND extension process spawning threshold is exceeded THEN onExtensionsProcessDisabled is not invoked`() { + val browserStore = BrowserStore(BrowserState()) + val appStore = AppStore(AppState(isForeground = true)) + var invoked = false + + val controller = ExtensionsProcessDisabledBackgroundController( + browserStore, + appStore, + onExtensionsProcessDisabled = { + invoked = true + }, + ) + + controller.start() + + browserStore.dispatch(ExtensionsProcessAction.ShowPromptAction(show = true)) + dispatcher.scheduler.advanceUntilIdle() + browserStore.waitUntilIdle() + + assertFalse(invoked) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledControllerTest.kt b/app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundControllerTest.kt similarity index 85% rename from app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledControllerTest.kt rename to app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundControllerTest.kt index 5e90601fd..7170ec97f 100644 --- a/app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/addons/ExtensionsProcessDisabledForegroundControllerTest.kt @@ -8,7 +8,6 @@ import android.view.View import android.widget.Button import androidx.appcompat.app.AlertDialog import mozilla.components.browser.state.action.ExtensionsProcessAction -import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.libstate.ext.waitUntilIdle @@ -29,7 +28,7 @@ import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @RunWith(FenixRobolectricTestRunner::class) -class ExtensionsProcessDisabledControllerTest { +class ExtensionsProcessDisabledForegroundControllerTest { @get:Rule val coroutinesTestRule = MainCoroutineRule() @@ -40,7 +39,7 @@ class ExtensionsProcessDisabledControllerTest { val browserStore = BrowserStore() val dialog: AlertDialog = mock() val builder: AlertDialog.Builder = mock() - val controller = ExtensionsProcessDisabledController( + val controller = ExtensionsProcessDisabledForegroundController( context = testContext, appStore = AppStore(AppState(isForeground = true)), browserStore = browserStore, @@ -81,7 +80,7 @@ class ExtensionsProcessDisabledControllerTest { val browserStore = BrowserStore() val dialog: AlertDialog = mock() val builder: AlertDialog.Builder = mock() - val controller = ExtensionsProcessDisabledController( + val controller = ExtensionsProcessDisabledForegroundController( context = testContext, appStore = AppStore(AppState(isForeground = true)), browserStore = browserStore, @@ -122,7 +121,7 @@ class ExtensionsProcessDisabledControllerTest { val browserStore = BrowserStore() val dialog: AlertDialog = mock() val builder: AlertDialog.Builder = mock() - val controller = ExtensionsProcessDisabledController( + val controller = ExtensionsProcessDisabledForegroundController( context = testContext, appStore = AppStore(AppState(isForeground = true)), browserStore = browserStore, @@ -152,23 +151,4 @@ class ExtensionsProcessDisabledControllerTest { buttonsContainerCaptor.value.findViewById