From 6b4cad0f9476f8f281d46b45a2a962c11514bf0c Mon Sep 17 00:00:00 2001 From: ohall-m Date: Tue, 13 Feb 2024 13:29:52 -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 | 33 +++++++++++++ .../middleware/TranslationsMiddlewareTest.kt | 49 +++++++++++++++++++ .../engine/translate/TranslationError.kt | 8 +++ .../telemetry/TelemetryMiddlewareTest.kt | 1 + 9 files changed, 184 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 53b72ac3b5ff..c560995e017e 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 @@ -937,6 +937,16 @@ sealed class TranslationsAction : BrowserAction() { val translationError: TranslationError, ) : TranslationsAction(), ActionWithTab + /** + * Indicates an app level translations error occurred and to set the error on + * [BrowserState.translationEngine]. + * + * @property error The error that occurred. + */ + data class EngineExceptionAction( + val error: TranslationError, + ) : TranslationsAction() + /** * Indicates that the given [operation] data should be fetched for the given [tabId]. * @@ -948,6 +958,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 ab984747ef92..f27b0ef62b54 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 @@ -39,6 +40,11 @@ class TranslationsMiddleware( ) { // Pre process actions when (action) { + is InitAction -> + scope.launch { + requestEngineSupport(context) + } + is TranslationsAction.OperationRequestedAction -> { when (action.operation) { TranslationOperation.FETCH_SUPPORTED_LANGUAGES -> { @@ -70,6 +76,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.TranslateSetLanguagesAction] 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 154ae8f4a99b..0a93814ffe1d 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 @@ -6,11 +6,15 @@ package mozilla.components.browser.state.reducer import mozilla.components.browser.state.action.TranslationsAction 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 internal object TranslationsStateReducer { + /** + * Reducer for [BrowserState.translationEngine] and [SessionState.translationsState] + */ @Suppress("LongMethod") fun reduce(state: BrowserState, action: TranslationsAction): BrowserState = when (action) { is TranslationsAction.TranslateExpectedAction -> { @@ -167,6 +171,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( @@ -218,6 +226,15 @@ internal object TranslationsStateReducer { state } } + + 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..377239a14140 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 state for translations that apply to the entire 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..a64af6a43d29 --- /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 The translations engine supports the device architecture. + * @property engineError If the translations engine has a global error state. + * 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 0eed569f0604..373522471e45 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 @@ -387,4 +387,37 @@ class TranslationsActionTest { // Action success assertNull(tabState().translationsState.supportedLanguages) } + + @Test + 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 237d69a5a85a..1973f0c1c9af 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 @@ -34,6 +35,7 @@ import mozilla.components.support.test.whenever import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.spy import org.mockito.Mockito.verify @@ -281,4 +283,51 @@ class TranslationsMiddlewareTest { ) waitForIdle() } + + @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..4ecca6b174e8 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(),