From c77a6e9c43f35413ecf40586c3f849765ecf5290 Mon Sep 17 00:00:00 2001 From: ohall-m Date: Wed, 14 Feb 2024 10:41:40 -0500 Subject: [PATCH] Bug 1877278 - AC Translations Check for if the Engine is Supported This patch supports the workflow for checking if the device architecture supports translations. This patch adds: * New `TranslationsBrowserState` to hold global translations engine state * New `SetEngineSupportedAction` to set the isEngineSupported value on `TranslationsBrowserState` * New `EngineExceptionAction` to set errors on `TranslationsBrowserState` --- .../browser/state/action/BrowserAction.kt | 20 ++++++++ .../middleware/TranslationsMiddleware.kt | 35 +++++++++++++ .../state/reducer/TranslationsStateReducer.kt | 17 +++++++ .../browser/state/state/BrowserState.kt | 2 + .../state/state/TranslationsBrowserState.kt | 19 +++++++ .../state/action/TranslationsActionTest.kt | 32 ++++++++++++ .../middleware/TranslationsMiddlewareTest.kt | 49 +++++++++++++++++++ .../engine/translate/TranslationError.kt | 8 +++ .../telemetry/TelemetryMiddlewareTest.kt | 1 + 9 files changed, 183 insertions(+) create mode 100644 android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt diff --git a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt index cea4dddd5d54..2594fb6840d6 100644 --- a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt +++ b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt @@ -938,6 +938,16 @@ sealed class TranslationsAction : BrowserAction() { val translationError: TranslationError, ) : TranslationsAction(), ActionWithTab + /** + * Indicates an app level translations error occurred and to set the [TranslationError] on + * [BrowserState.translationEngine]. + * + * @property error The [TranslationError] that occurred. + */ + data class EngineExceptionAction( + val error: TranslationError, + ) : TranslationsAction() + /** * Indicates that the given [operation] data should be fetched for the given [tabId]. * @@ -949,6 +959,16 @@ sealed class TranslationsAction : BrowserAction() { val operation: TranslationOperation, ) : TranslationsAction(), ActionWithTab + /** + * Sets whether the device architecture supports translations or not on + * [BrowserState.translationEngine]. + * + * @property isEngineSupported If the engine supports translations on this device. + */ + data class SetEngineSupportedAction( + val isEngineSupported: Boolean, + ) : TranslationsAction() + /** * Sets the languages that are supported by the translations engine. * diff --git a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt index 0ee5e36edd68..f3e95c065f55 100644 --- a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt +++ b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt @@ -7,6 +7,7 @@ package mozilla.components.browser.state.engine.middleware import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.InitAction import mozilla.components.browser.state.action.TranslationsAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.BrowserState @@ -40,6 +41,11 @@ class TranslationsMiddleware( ) { // Pre process actions when (action) { + is InitAction -> + scope.launch { + requestEngineSupport(context) + } + is TranslationsAction.OperationRequestedAction -> { when (action.operation) { TranslationOperation.FETCH_SUPPORTED_LANGUAGES -> { @@ -117,6 +123,35 @@ class TranslationsMiddleware( next(action) } + /** + * Checks if the translations engine supports the device architecture and updates the state. + * + * @param context Context to use to dispatch to the store. + */ + private fun requestEngineSupport( + context: MiddlewareContext, + ) { + engine.isTranslationsEngineSupported( + onSuccess = { isEngineSupported -> + context.store.dispatch( + TranslationsAction.SetEngineSupportedAction( + isEngineSupported = isEngineSupported, + ), + ) + logger.info("Success requesting engine support.") + }, + + onError = { error -> + context.store.dispatch( + TranslationsAction.EngineExceptionAction( + error = TranslationError.UnknownEngineSupportError(error), + ), + ) + logger.error("Error requesting engine support: ", error) + }, + ) + } + /** * Retrieves the list of supported languages using [scope] and dispatches the result to the * store via [TranslationsAction.SetSupportedLanguagesAction] or else dispatches the failure diff --git a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt index df014ec1289a..107af97ec90e 100644 --- a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt +++ b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt @@ -7,6 +7,7 @@ package mozilla.components.browser.state.reducer import mozilla.components.browser.state.action.TranslationsAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TranslationsState import mozilla.components.concept.engine.translate.TranslationOperation import mozilla.components.concept.engine.translate.TranslationPageSettingOperation @@ -14,6 +15,9 @@ import mozilla.components.concept.engine.translate.TranslationPageSettings internal object TranslationsStateReducer { + /** + * Reducer for [BrowserState.translationEngine] and [SessionState.translationsState] + */ @Suppress("LongMethod") fun reduce(state: BrowserState, action: TranslationsAction): BrowserState = when (action) { is TranslationsAction.TranslateExpectedAction -> { @@ -170,6 +174,10 @@ internal object TranslationsStateReducer { } } + is TranslationsAction.EngineExceptionAction -> { + state.copy(translationEngine = state.translationEngine.copy(engineError = action.error)) + } + is TranslationsAction.SetSupportedLanguagesAction -> state.copyWithTranslationsState(action.tabId) { it.copy( @@ -278,6 +286,15 @@ internal object TranslationsStateReducer { } } } + + is TranslationsAction.SetEngineSupportedAction -> { + state.copy( + translationEngine = state.translationEngine.copy( + isEngineSupported = action.isEngineSupported, + engineError = null, + ), + ) + } } private inline fun BrowserState.copyWithTranslationsState( diff --git a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt index 4e2d2a2fda2a..94ded431dd64 100644 --- a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt +++ b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt @@ -35,6 +35,7 @@ import java.util.Locale * on application startup e.g. as an indicator that tabs have been restored. * @property locale The current locale of the app. Will be null when following the system default. * @property awesomeBarState Holds state for interactions with the [AwesomeBar]. + * @property translationEngine Holds translation state that applies to the browser. */ data class BrowserState( val tabs: List = emptyList(), @@ -54,4 +55,5 @@ data class BrowserState( val showExtensionsProcessDisabledPrompt: Boolean = false, val extensionsProcessDisabled: Boolean = false, val awesomeBarState: AwesomeBarState = AwesomeBarState(), + val translationEngine: TranslationsBrowserState = TranslationsBrowserState(), ) : State diff --git a/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt new file mode 100644 index 000000000000..2b0dc89b5b02 --- /dev/null +++ b/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt @@ -0,0 +1,19 @@ +/* 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 mozilla.components.browser.state.state + +import mozilla.components.concept.engine.translate.TranslationError + +/** + * Value type that represents the state of the translations engine within a [BrowserState]. + * + * @property isEngineSupported Whether the translations engine supports the device architecture. + * @property engineError Holds the error state of the translations engine. + * See [TranslationsState.translationError] for session level errors. + */ +data class TranslationsBrowserState( + val isEngineSupported: Boolean? = null, + val engineError: TranslationError? = null, +) diff --git a/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt b/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt index 4aa5c8bc7fff..f770ca80b665 100644 --- a/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt +++ b/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt @@ -518,4 +518,36 @@ class TranslationsActionTest { assertTrue(tabState().translationsState.pageSettings?.neverTranslateLanguage!!) assertTrue(tabState().translationsState.pageSettings?.neverTranslateSite!!) } + + fun `WHEN a SetEngineSupportAction is dispatched THEN the browser store is updated to match`() { + // Initial state + assertNull(store.state.translationEngine.isEngineSupported) + + // Dispatch + store.dispatch( + TranslationsAction.SetEngineSupportedAction( + isEngineSupported = true, + ), + ).joinBlocking() + + // Final state + assertTrue(store.state.translationEngine.isEngineSupported!!) + } + + @Test + fun `WHEN an EngineExceptionAction is dispatched THEN the browser store is updated to match`() { + // Initial state + assertNull(store.state.translationEngine.engineError) + + // Dispatch + val error = TranslationError.UnknownError(Throwable()) + store.dispatch( + TranslationsAction.EngineExceptionAction( + error = error, + ), + ).joinBlocking() + + // Final state + assertEquals(store.state.translationEngine.engineError!!, error) + } } diff --git a/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt b/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt index 226724f5f47f..0c970f8dddd0 100644 --- a/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt +++ b/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt @@ -6,6 +6,7 @@ package mozilla.components.browser.state.engine.middleware import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.InitAction import mozilla.components.browser.state.action.TranslationsAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.BrowserState @@ -36,6 +37,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.spy import org.mockito.Mockito.verify @@ -439,4 +441,51 @@ class TranslationsMiddlewareTest { ), ) } + + @Test + fun `WHEN InitAction is dispatched THEN SetEngineSupportAction is dispatched`() = runTest { + // Send Action + // Note: Will cause a double InitAction + translationsMiddleware.invoke(context, {}, InitAction) + waitForIdle() + + // Check expectations + val engineSupportedCallback = argumentCaptor<((Boolean) -> Unit)>() + verify(engine, atLeastOnce()).isTranslationsEngineSupported( + onSuccess = engineSupportedCallback.capture(), + onError = any(), + ) + engineSupportedCallback.value.invoke(true) + waitForIdle() + + verify(store, atLeastOnce()).dispatch( + TranslationsAction.SetEngineSupportedAction( + isEngineSupported = true, + ), + ) + waitForIdle() + } + + @Test + fun `WHEN InitAction is dispatched AND has an issue THEN TranslateExceptionAction is dispatched`() = runTest() { + // Send Action + // Note: Will cause a double InitAction + translationsMiddleware.invoke(context, {}, InitAction) + waitForIdle() + + // Check expectations + val errorCallback = argumentCaptor<((Throwable) -> Unit)>() + verify(engine, atLeastOnce()).isTranslationsEngineSupported( + onSuccess = any(), + onError = errorCallback.capture(), + ) + errorCallback.value.invoke(IllegalStateException()) + waitForIdle() + + verify(store, atLeastOnce()).dispatch( + TranslationsAction.EngineExceptionAction( + error = TranslationError.UnknownEngineSupportError(any()), + ), + ) + } } diff --git a/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt b/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt index 812ffe88f563..a12017fa2aca 100644 --- a/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt +++ b/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/translate/TranslationError.kt @@ -48,6 +48,14 @@ sealed class TranslationError( class EngineNotSupportedError(override val cause: Throwable?) : TranslationError(errorName = "engine-not-supported", displayError = false, cause = cause) + /** + * Could not determine if the translations engine works on the device architecture. + * + * @param cause The original [Throwable] before it was converted into this error state. + */ + class UnknownEngineSupportError(override val cause: Throwable?) : + TranslationError(errorName = "unknown-engine-support", displayError = false, cause = cause) + /** * Generic could not compete a translation error. * diff --git a/fenix/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt b/fenix/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt index 4c8eace865f2..dfbb1ba68291 100644 --- a/fenix/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt +++ b/fenix/app/src/test/java/org/mozilla/fenix/telemetry/TelemetryMiddlewareTest.kt @@ -86,6 +86,7 @@ class TelemetryMiddlewareTest { val engine: Engine = mockk() every { engine.enableExtensionProcessSpawning() } just runs every { engine.disableExtensionProcessSpawning() } just runs + every { engine.isTranslationsEngineSupported(any(), any()) } just runs store = BrowserStore( middleware = listOf(telemetryMiddleware) + EngineMiddleware.create(engine), initialState = BrowserState(),