diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:42 +0000 |
commit | da4c7e7ed675c3bf405668739c3012d140856109 (patch) | |
tree | cdd868dba063fecba609a1d819de271f0d51b23e /mobile/android/android-components/components/feature/customtabs/src | |
parent | Adding upstream version 125.0.3. (diff) | |
download | firefox-da4c7e7ed675c3bf405668739c3012d140856109.tar.xz firefox-da4c7e7ed675c3bf405668739c3012d140856109.zip |
Adding upstream version 126.0.upstream/126.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/feature/customtabs/src')
140 files changed, 5011 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/customtabs/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e16cda1d34 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<!-- 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/. --> +<manifest /> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/AbstractCustomTabsService.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/AbstractCustomTabsService.kt new file mode 100644 index 0000000000..7a96b72cfc --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/AbstractCustomTabsService.kt @@ -0,0 +1,135 @@ +/* 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.feature.customtabs + +import android.app.Service +import android.net.Uri +import android.os.Binder +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import androidx.browser.customtabs.CustomTabsService +import androidx.browser.customtabs.CustomTabsSessionToken +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.customtabs.feature.OriginVerifierFeature +import mozilla.components.feature.customtabs.store.CustomTabsServiceStore +import mozilla.components.feature.customtabs.store.SaveCreatorPackageNameAction +import mozilla.components.service.digitalassetlinks.RelationChecker +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.utils.ext.getParcelableCompat + +/** + * Maximum number of speculative connections we will open when an app calls into + * [AbstractCustomTabsService.mayLaunchUrl] with a list of URLs. + */ +private const val MAX_SPECULATIVE_URLS = 50 + +/** + * [Service] providing Custom Tabs related functionality. + */ +abstract class AbstractCustomTabsService : CustomTabsService() { + private val logger = Logger("CustomTabsService") + private val scope = MainScope() + + abstract val engine: Engine + abstract val customTabsServiceStore: CustomTabsServiceStore + open val relationChecker: RelationChecker? = null + + @VisibleForTesting + internal val verifier by lazy { + relationChecker?.let { checker -> + OriginVerifierFeature(packageManager, checker) { customTabsServiceStore.dispatch(it) } + } + } + + override fun onDestroy() { + super.onDestroy() + scope.cancel() + } + + override fun warmup(flags: Long): Boolean { + // We need to run this on the main thread since that's where GeckoRuntime expects to get initialized (if needed) + return runBlocking(Main) { + engine.warmUp() + true + } + } + + override fun requestPostMessageChannel(sessionToken: CustomTabsSessionToken, postMessageOrigin: Uri): Boolean { + return false + } + + /** + * Saves the package name of the app creating the custom tab when a new session is started. + */ + override fun newSession(sessionToken: CustomTabsSessionToken): Boolean { + // Extract the process UID of the app creating the custom tab. + val uid = Binder.getCallingUid() + // Only save the package if exactly one package name maps to the process UID. + val packageName = packageManager.getPackagesForUid(uid)?.singleOrNull() + + if (!packageName.isNullOrEmpty()) { + customTabsServiceStore.dispatch(SaveCreatorPackageNameAction(sessionToken, packageName)) + } + return true + } + + override fun extraCommand(commandName: String, args: Bundle?): Bundle? = null + + override fun mayLaunchUrl( + sessionToken: CustomTabsSessionToken, + url: Uri?, + extras: Bundle?, + otherLikelyBundles: List<Bundle>?, + ): Boolean { + logger.debug("Opening speculative connections") + + // Most likely URL for a future navigation: Open a speculative connection. + url?.let { engine.speculativeConnect(it.toString()) } + + // A list of other likely URLs. Let's open a speculative connection for them up to a limit. + otherLikelyBundles?.take(MAX_SPECULATIVE_URLS)?.forEach { bundle -> + bundle.getParcelableCompat(KEY_URL, Uri::class.java)?.let { uri -> + engine.speculativeConnect(uri.toString()) + } + } + + return true + } + + override fun postMessage(sessionToken: CustomTabsSessionToken, message: String, extras: Bundle?) = + RESULT_FAILURE_DISALLOWED + + override fun validateRelationship( + sessionToken: CustomTabsSessionToken, + @Relation relation: Int, + origin: Uri, + extras: Bundle?, + ): Boolean { + val verifier = verifier + val state = customTabsServiceStore.state.tabs[sessionToken] + return if (verifier != null && state != null) { + scope.launch(Main) { + val result = verifier.verify(state, sessionToken, relation, origin) + sessionToken.callback?.onRelationshipValidationResult(relation, origin, result, extras) + } + true + } else { + false + } + } + + override fun updateVisuals(sessionToken: CustomTabsSessionToken, bundle: Bundle?): Boolean { + return false + } + + override fun receiveFile(sessionToken: CustomTabsSessionToken, uri: Uri, purpose: Int, extras: Bundle?): Boolean { + return false + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt new file mode 100644 index 0000000000..2d5fe96e9a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt @@ -0,0 +1,280 @@ +/* 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.feature.customtabs + +import android.app.PendingIntent +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import androidx.annotation.ColorInt +import androidx.annotation.VisibleForTesting +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.browser.customtabs.CustomTabsIntent.ColorScheme +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_CLOSE_BUTTON_ICON +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_COLOR_SCHEME +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_COLOR_SCHEME_PARAMS +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_ENABLE_URLBAR_HIDING +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_MENU_ITEMS +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SHARE_STATE +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TINT_ACTION_BUTTON +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_COLOR +import androidx.browser.customtabs.CustomTabsIntent.KEY_DESCRIPTION +import androidx.browser.customtabs.CustomTabsIntent.KEY_ICON +import androidx.browser.customtabs.CustomTabsIntent.KEY_ID +import androidx.browser.customtabs.CustomTabsIntent.KEY_MENU_ITEM_TITLE +import androidx.browser.customtabs.CustomTabsIntent.KEY_PENDING_INTENT +import androidx.browser.customtabs.CustomTabsIntent.NO_TITLE +import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_DEFAULT +import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON +import androidx.browser.customtabs.CustomTabsIntent.SHOW_PAGE_TITLE +import androidx.browser.customtabs.CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID +import androidx.browser.customtabs.CustomTabsSessionToken +import androidx.browser.customtabs.TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY +import mozilla.components.browser.state.state.ColorSchemeParams +import mozilla.components.browser.state.state.ColorSchemes +import mozilla.components.browser.state.state.CustomTabActionButtonConfig +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.CustomTabMenuItem +import mozilla.components.browser.state.state.ExternalAppType +import mozilla.components.support.utils.SafeIntent +import mozilla.components.support.utils.toSafeBundle +import mozilla.components.support.utils.toSafeIntent +import kotlin.math.max + +/** + * Checks if the provided intent is a custom tab intent. + * + * @param intent the intent to check. + * @return true if the intent is a custom tab intent, otherwise false. + */ +fun isCustomTabIntent(intent: Intent) = isCustomTabIntent(intent.toSafeIntent()) + +/** + * Checks if the provided intent is a custom tab intent. + * + * @param safeIntent the intent to check, wrapped as a SafeIntent. + * @return true if the intent is a custom tab intent, otherwise false. + */ +fun isCustomTabIntent(safeIntent: SafeIntent) = safeIntent.hasExtra(EXTRA_SESSION) + +/** + * Checks if the provided intent is a trusted web activity intent. + * + * @param intent the intent to check. + * @return true if the intent is a trusted web activity intent, otherwise false. + */ +fun isTrustedWebActivityIntent(intent: Intent) = isTrustedWebActivityIntent(intent.toSafeIntent()) + +/** + * Checks if the provided intent is a trusted web activity intent. + * + * @param safeIntent the intent to check, wrapped as a SafeIntent. + * @return true if the intent is a trusted web activity intent, otherwise false. + */ +fun isTrustedWebActivityIntent(safeIntent: SafeIntent) = isCustomTabIntent(safeIntent) && + safeIntent.getBooleanExtra(EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, false) + +/** + * Creates a [CustomTabConfig] instance based on the provided [Intent]. + * + * @param intent The [Intent] wrapped as a [SafeIntent], which is processed to extract configuration data. + * @param resources Optional [Resources] to verify that only icons of a max size are provided. + * + * @return the configured [CustomTabConfig]. + */ +fun createCustomTabConfigFromIntent(intent: Intent, resources: Resources?): CustomTabConfig { + val safeIntent = intent.toSafeIntent() + + return CustomTabConfig( + colorScheme = safeIntent.getColorExtra(EXTRA_COLOR_SCHEME), + colorSchemes = getColorSchemes(safeIntent), + closeButtonIcon = getCloseButtonIcon(safeIntent, resources), + enableUrlbarHiding = safeIntent.getBooleanExtra(EXTRA_ENABLE_URLBAR_HIDING, false), + actionButtonConfig = getActionButtonConfig(safeIntent), + showShareMenuItem = (safeIntent.getIntExtra(EXTRA_SHARE_STATE, SHARE_STATE_DEFAULT) == SHARE_STATE_ON), + menuItems = getMenuItems(safeIntent), + exitAnimations = safeIntent.getBundleExtra(EXTRA_EXIT_ANIMATION_BUNDLE)?.unsafe, + titleVisible = safeIntent.getIntExtra(EXTRA_TITLE_VISIBILITY_STATE, NO_TITLE) == SHOW_PAGE_TITLE, + sessionToken = if (intent.extras != null) { + // getSessionTokenFromIntent throws if extras is null + CustomTabsSessionToken.getSessionTokenFromIntent(intent) + } else { + null + }, + externalAppType = ExternalAppType.CUSTOM_TAB, + ) +} + +@ColorInt +private fun SafeIntent.getColorExtra(name: String): Int? = + if (hasExtra(name)) getIntExtra(name, 0) else null + +private fun getCloseButtonIcon(intent: SafeIntent, resources: Resources?): Bitmap? { + val icon = try { + intent.getParcelableExtra(EXTRA_CLOSE_BUTTON_ICON, Bitmap::class.java) + } catch (e: ClassCastException) { + null + } + val maxSize = resources?.getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size) ?: Float.MAX_VALUE + + return if (icon != null && max(icon.width, icon.height) <= maxSize) { + icon + } else { + null + } +} + +private fun getColorSchemes(safeIntent: SafeIntent): ColorSchemes? { + val defaultColorSchemeParams = getDefaultSchemeColorParams(safeIntent) + val lightColorSchemeParams = getLightColorSchemeParams(safeIntent) + val darkColorSchemeParams = getDarkColorSchemeParams(safeIntent) + + return if (allNull(defaultColorSchemeParams, lightColorSchemeParams, darkColorSchemeParams)) { + null + } else { + ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ) + } +} + +/** + * Processes the given [SafeIntent] to extract possible default [CustomTabColorSchemeParams] + * properties. + * + * @param safeIntent the [SafeIntent] to process. + * + * @return the derived [ColorSchemeParams] or null if the [SafeIntent] had no default + * [CustomTabColorSchemeParams] properties. + * + * @see [CustomTabsIntent.Builder.setDefaultColorSchemeParams]. + */ +private fun getDefaultSchemeColorParams(safeIntent: SafeIntent): ColorSchemeParams? { + val toolbarColor = safeIntent.getColorExtra(EXTRA_TOOLBAR_COLOR) + val secondaryToolbarColor = safeIntent.getColorExtra(EXTRA_SECONDARY_TOOLBAR_COLOR) + val navigationBarColor = safeIntent.getColorExtra(EXTRA_NAVIGATION_BAR_COLOR) + val navigationBarDividerColor = safeIntent.getColorExtra(EXTRA_NAVIGATION_BAR_DIVIDER_COLOR) + + return if (allNull( + toolbarColor, + secondaryToolbarColor, + navigationBarColor, + navigationBarDividerColor, + ) + ) { + null + } else { + ColorSchemeParams( + toolbarColor = toolbarColor, + secondaryToolbarColor = secondaryToolbarColor, + navigationBarColor = navigationBarColor, + navigationBarDividerColor = navigationBarDividerColor, + ) + } +} + +private fun getLightColorSchemeParams(safeIntent: SafeIntent) = + getColorSchemeParams(safeIntent, CustomTabsIntent.COLOR_SCHEME_LIGHT) + +private fun getDarkColorSchemeParams(safeIntent: SafeIntent) = + getColorSchemeParams(safeIntent, CustomTabsIntent.COLOR_SCHEME_DARK) + +/** + * Processes the given [SafeIntent] to extract possible [CustomTabColorSchemeParams] properties for + * the given [colorScheme]. + * + * @param safeIntent The [SafeIntent] to process. + * @param colorScheme The [ColorScheme] to get the [ColorSchemeParams] for. + * + * @return the derived [ColorSchemeParams] for the given [ColorScheme], or null if the [SafeIntent] + * had no [CustomTabColorSchemeParams] properties for the [ColorScheme]. + * + * @see [CustomTabsIntent.Builder.setColorSchemeParams]. + */ +private fun getColorSchemeParams(safeIntent: SafeIntent, @ColorScheme colorScheme: Int): ColorSchemeParams? { + val bundle = safeIntent.getColorSchemeParamsBundle()?.get(colorScheme) + + val toolbarColor = bundle?.getNullableSafeValue(EXTRA_TOOLBAR_COLOR) + val secondaryToolbarColor = bundle?.getNullableSafeValue(EXTRA_SECONDARY_TOOLBAR_COLOR) + val navigationBarColor = bundle?.getNullableSafeValue(EXTRA_NAVIGATION_BAR_COLOR) + val navigationBarDividerColor = bundle?.getNullableSafeValue(EXTRA_NAVIGATION_BAR_DIVIDER_COLOR) + + return if (allNull(toolbarColor, secondaryToolbarColor, navigationBarColor, navigationBarDividerColor)) { + null + } else { + ColorSchemeParams( + toolbarColor = toolbarColor, + secondaryToolbarColor = secondaryToolbarColor, + navigationBarColor = navigationBarColor, + navigationBarDividerColor = navigationBarDividerColor, + ) + } +} + +private fun <T> allNull(vararg value: T?) = value.toList().all { it == null } + +@VisibleForTesting +internal fun SafeIntent.getColorSchemeParamsBundle() = extras?.let { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + @Suppress("DEPRECATION") + it.getSparseParcelableArray(EXTRA_COLOR_SCHEME_PARAMS) + } else { + it.getSparseParcelableArray(EXTRA_COLOR_SCHEME_PARAMS, Bundle::class.java) + } +} + +private fun Bundle.getNullableSafeValue(key: String) = + if (containsKey(key)) toSafeBundle().getInt(key) else null + +private fun getActionButtonConfig(intent: SafeIntent): CustomTabActionButtonConfig? { + val actionButtonBundle = intent.getBundleExtra(EXTRA_ACTION_BUTTON_BUNDLE) ?: return null + val description = actionButtonBundle.getString(KEY_DESCRIPTION) + val icon = actionButtonBundle.getParcelable(KEY_ICON, Bitmap::class.java) + val pendingIntent = actionButtonBundle.getParcelable(KEY_PENDING_INTENT, PendingIntent::class.java) + val id = actionButtonBundle.getInt(KEY_ID, TOOLBAR_ACTION_BUTTON_ID) + val tint = intent.getBooleanExtra(EXTRA_TINT_ACTION_BUTTON, false) + + return if (description != null && icon != null && pendingIntent != null) { + CustomTabActionButtonConfig( + id = id, + description = description, + icon = icon, + pendingIntent = pendingIntent, + tint = tint, + ) + } else { + null + } +} + +private fun getMenuItems(intent: SafeIntent): List<CustomTabMenuItem> = + intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS, Parcelable::class.java).orEmpty() + .mapNotNull { menuItemBundle -> + val bundle = (menuItemBundle as? Bundle)?.toSafeBundle() + val name = bundle?.getString(KEY_MENU_ITEM_TITLE) + val pendingIntent = bundle?.getParcelable(KEY_PENDING_INTENT, PendingIntent::class.java) + + if (name != null && pendingIntent != null) { + CustomTabMenuItem( + name = name, + pendingIntent = pendingIntent, + ) + } else { + null + } + } diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt new file mode 100644 index 0000000000..8bd8ee2179 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt @@ -0,0 +1,74 @@ +/* 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.feature.customtabs + +import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.content.res.Resources +import android.provider.Browser +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.externalPackage +import mozilla.components.feature.intent.ext.putSessionId +import mozilla.components.feature.intent.processing.IntentProcessor +import mozilla.components.feature.tabs.CustomTabsUseCases +import mozilla.components.support.utils.SafeIntent +import mozilla.components.support.utils.toSafeIntent + +/** + * Processor for intents which trigger actions related to custom tabs. + */ +class CustomTabIntentProcessor( + private val addCustomTabUseCase: CustomTabsUseCases.AddCustomTabUseCase, + private val resources: Resources, + private val isPrivate: Boolean = false, +) : IntentProcessor { + + private fun matches(intent: Intent): Boolean { + val safeIntent = intent.toSafeIntent() + return safeIntent.action == ACTION_VIEW && isCustomTabIntent(safeIntent) + } + + @VisibleForTesting + internal fun getAdditionalHeaders(intent: SafeIntent): Map<String, String>? { + val pairs = intent.getBundleExtra(Browser.EXTRA_HEADERS) + val headers = mutableMapOf<String, String>() + pairs?.keySet()?.forEach { key -> + val header = pairs.getString(key) + if (header != null) { + headers[key] = header + } else { + throw IllegalArgumentException("getAdditionalHeaders() intent bundle contains wrong key value pair") + } + } + return if (headers.isEmpty()) { + null + } else { + headers + } + } + + override fun process(intent: Intent): Boolean { + val safeIntent = SafeIntent(intent) + val url = safeIntent.dataString + + return if (!url.isNullOrEmpty() && matches(intent)) { + val config = createCustomTabConfigFromIntent(intent, resources) + val caller = safeIntent.externalPackage() + val customTabId = addCustomTabUseCase( + url, + config, + isPrivate, + getAdditionalHeaders(safeIntent), + source = SessionState.Source.External.CustomTab(caller), + ) + intent.putSessionId(customTabId) + + true + } else { + false + } + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabWindowFeature.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabWindowFeature.kt new file mode 100644 index 0000000000..77aa6f0999 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabWindowFeature.kt @@ -0,0 +1,107 @@ +/* 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.feature.customtabs + +import android.app.Activity +import android.content.ActivityNotFoundException +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.Companion.PRIVATE +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.mapNotNull +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.window.WindowRequest +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature + +const val SHORTCUT_CATEGORY = "mozilla.components.pwa.category.SHORTCUT" + +/** + * Feature implementation for handling window requests by opening custom tabs. + */ +class CustomTabWindowFeature( + private val activity: Activity, + private val store: BrowserStore, + private val sessionId: String, +) : LifecycleAwareFeature { + + private var scope: CoroutineScope? = null + + /** + * Transform a [CustomTabConfig] into a [CustomTabsIntent] that creates a + * new custom tab with the same styling and layout + */ + @Suppress("ComplexMethod") + @VisibleForTesting(otherwise = PRIVATE) + internal fun configToIntent(config: CustomTabConfig?): CustomTabsIntent { + val intent = CustomTabsIntent.Builder().apply { + setInstantAppsEnabled(false) + + val customTabColorSchemeBuilder = CustomTabColorSchemeParams.Builder() + config?.colorSchemes?.defaultColorSchemeParams?.toolbarColor?.let { + customTabColorSchemeBuilder.setToolbarColor(it) + } + config?.colorSchemes?.defaultColorSchemeParams?.navigationBarColor?.let { + customTabColorSchemeBuilder.setNavigationBarColor(it) + } + setDefaultColorSchemeParams(customTabColorSchemeBuilder.build()) + + if (config?.enableUrlbarHiding == true) setUrlBarHidingEnabled(true) + config?.closeButtonIcon?.let { setCloseButtonIcon(it) } + if (config?.showShareMenuItem == true) setShareState(CustomTabsIntent.SHARE_STATE_ON) + config?.titleVisible?.let { setShowTitle(it) } + config?.actionButtonConfig?.apply { setActionButton(icon, description, pendingIntent, tint) } + config?.menuItems?.forEach { addMenuItem(it.name, it.pendingIntent) } + }.build() + + intent.intent.`package` = activity.packageName + intent.intent.addCategory(SHORTCUT_CATEGORY) + + return intent + } + + /** + * Starts observing the configured session to listen for window requests. + */ + override fun start() { + scope = store.flowScoped { flow -> + flow.mapNotNull { state -> state.findCustomTab(sessionId) } + .distinctUntilChangedBy { + it.content.windowRequest + } + .collect { state -> + val windowRequest = state.content.windowRequest + if (windowRequest?.type == WindowRequest.Type.OPEN) { + val intent = configToIntent(state.config) + val uri = windowRequest.url.toUri() + // This could only fail if the above intent is for our application + // and we are not registered to handle its schemes. + try { + intent.launchUrl(activity, uri) + } catch (e: ActivityNotFoundException) { + // Workaround for unsupported schemes + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1878704 + state.engineState.engineSession?.loadUrl(windowRequest.url) + } + store.dispatch(ContentAction.ConsumeWindowRequestAction(sessionId)) + } + } + } + } + + /** + * Stops observing the configured session for incoming window requests. + */ + override fun stop() { + scope?.cancel() + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsFacts.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsFacts.kt new file mode 100644 index 0000000000..6be0cb9610 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsFacts.kt @@ -0,0 +1,37 @@ +/* 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.feature.customtabs + +import mozilla.components.support.base.Component +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.base.facts.collect + +/** + * Facts emitted for telemetry related to [CustomTabsToolbarFeature] + */ +class CustomTabsFacts { + /** + * Items that specify which portion of the [CustomTabsToolbarFeature] was interacted with + */ + object Items { + const val CLOSE = "close" + const val ACTION_BUTTON = "action_button" + } +} + +private fun emitCustomTabsFact( + action: Action, + item: String, +) { + Fact( + Component.FEATURE_CUSTOMTABS, + action, + item, + ).collect() +} + +internal fun emitCloseFact() = emitCustomTabsFact(Action.CLICK, CustomTabsFacts.Items.CLOSE) +internal fun emitActionButtonFact() = emitCustomTabsFact(Action.CLICK, CustomTabsFacts.Items.ACTION_BUTTON) diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeature.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeature.kt new file mode 100644 index 0000000000..f437a6874d --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeature.kt @@ -0,0 +1,425 @@ +/* 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.feature.customtabs + +import android.app.PendingIntent +import android.app.UiModeManager.MODE_NIGHT_YES +import android.content.Context +import android.content.res.Configuration +import android.graphics.Bitmap +import android.util.Size +import android.view.Window +import androidx.annotation.ColorInt +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM +import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO +import androidx.appcompat.app.AppCompatDelegate.NightMode +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_SYSTEM +import androidx.browser.customtabs.CustomTabsIntent.ColorScheme +import androidx.core.content.ContextCompat.getColor +import androidx.core.graphics.drawable.toDrawable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.mapNotNull +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.BrowserMenuItem +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.browser.state.state.ColorSchemeParams +import mozilla.components.browser.state.state.ColorSchemes +import mozilla.components.browser.state.state.CustomTabActionButtonConfig +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.CustomTabMenuItem +import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.feature.customtabs.feature.CustomTabSessionTitleObserver +import mozilla.components.feature.customtabs.menu.sendWithUrl +import mozilla.components.feature.tabs.CustomTabsUseCases +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.feature.UserInteractionHandler +import mozilla.components.support.ktx.android.content.res.resolveAttribute +import mozilla.components.support.ktx.android.content.share +import mozilla.components.support.ktx.android.util.dpToPx +import mozilla.components.support.ktx.android.view.setNavigationBarTheme +import mozilla.components.support.ktx.android.view.setStatusBarTheme +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import mozilla.components.support.utils.ColorUtils.getReadableTextColor +import mozilla.components.support.utils.ext.resizeMaintainingAspectRatio +import mozilla.components.ui.icons.R as iconsR + +/** + * Initializes and resets the [BrowserToolbar] for a Custom Tab based on the [CustomTabConfig]. + * + * @property store The given [BrowserStore] to use. + * @property toolbar Reference to the [BrowserToolbar], so that the color and menu items can be set. + * @property sessionId ID of the custom tab session. No-op if null or invalid. + * @property useCases The given [CustomTabsUseCases] to use. + * @property menuBuilder [BrowserMenuBuilder] reference to pull menu options from. + * @property menuItemIndex Location to insert any custom menu options into the predefined menu list. + * @property window Reference to the [Window] so the navigation bar color can be set. + * @property updateTheme Whether or not the toolbar and system bar colors should be changed. + * @property appNightMode The [NightMode] used in the app. Defaults to [MODE_NIGHT_FOLLOW_SYSTEM]. + * @property forceActionButtonTinting When set to true the [toolbar] action button will always be tinted + * based on the [toolbar] background, ignoring the value of [CustomTabActionButtonConfig.tint]. + * @property isNavBarEnabled Whether or not the navigation bar is enabled. + * @property shareListener Invoked when the share button is pressed. + * @property closeListener Invoked when the close button is pressed. + */ +@Suppress("LargeClass") +class CustomTabsToolbarFeature( + private val store: BrowserStore, + private val toolbar: BrowserToolbar, + private val sessionId: String? = null, + private val useCases: CustomTabsUseCases, + private val menuBuilder: BrowserMenuBuilder? = null, + private val menuItemIndex: Int = menuBuilder?.items?.size ?: 0, + private val window: Window? = null, + private val updateTheme: Boolean = true, + @NightMode private val appNightMode: Int = MODE_NIGHT_FOLLOW_SYSTEM, + private val forceActionButtonTinting: Boolean = false, + private val isNavBarEnabled: Boolean = false, + private val shareListener: (() -> Unit)? = null, + private val closeListener: () -> Unit, +) : LifecycleAwareFeature, UserInteractionHandler { + private var initialized: Boolean = false + private val titleObserver = CustomTabSessionTitleObserver(toolbar) + private val context get() = toolbar.context + private var scope: CoroutineScope? = null + + /** + * Gets the current custom tab session. + */ + private val session: CustomTabSessionState? + get() = sessionId?.let { store.state.findCustomTab(it) } + + /** + * Initializes the feature and registers the [CustomTabSessionTitleObserver]. + */ + override fun start() { + val tabId = sessionId ?: return + val tab = store.state.findCustomTab(tabId) ?: return + + scope = store.flowScoped { flow -> + flow + .mapNotNull { state -> state.findCustomTab(tabId) } + .ifAnyChanged { tab -> arrayOf(tab.content.title, tab.content.url) } + .collect { tab -> titleObserver.onTab(tab) } + } + + if (!initialized) { + initialized = true + init(tab.config) + } + } + + /** + * Unregisters the [CustomTabSessionTitleObserver]. + */ + override fun stop() { + scope?.cancel() + } + + @VisibleForTesting + internal fun init(config: CustomTabConfig) { + // Don't allow clickable toolbar so a custom tab can't switch to edit mode. + toolbar.display.onUrlClicked = { false } + toolbar.display.hidePageActionSeparator() + + // Use the intent provided color scheme or fallback to the app night mode preference. + val nightMode = config.colorScheme?.toNightMode() ?: appNightMode + + val colorSchemeParams = config.colorSchemes?.getConfiguredColorSchemeParams( + nightMode = nightMode, + isDarkMode = context.isDarkMode(), + ) + + val readableColor = if (updateTheme) { + colorSchemeParams?.toolbarColor?.let { getReadableTextColor(it) } + ?: toolbar.display.colors.menu + } else { + // It's private mode, the readable color needs match the app. + // Note: The main app is configuring the private theme, Custom Tabs is adding the + // additional theming for the dynamic UI elements e.g. action & share buttons. + val colorResId = context.theme.resolveAttribute(android.R.attr.textColorPrimary) + getColor(context, colorResId) + } + + if (updateTheme) { + colorSchemeParams.let { + updateTheme( + toolbarColor = it?.toolbarColor, + navigationBarColor = it?.navigationBarColor ?: it?.toolbarColor, + navigationBarDividerColor = it?.navigationBarDividerColor, + readableColor = readableColor, + ) + } + } + + // Add navigation close action + if (config.showCloseButton) { + addCloseButton(readableColor, config.closeButtonIcon) + } + + // Add action button + addActionButton(readableColor, config.actionButtonConfig) + + // Show share button + if (config.showShareMenuItem) { + addShareButton(readableColor) + } + + // Add menu items + if (config.menuItems.isNotEmpty() || menuBuilder?.items?.isNotEmpty() == true) { + addMenuItems(config.menuItems, menuItemIndex) + } + + if (isNavBarEnabled) { + toolbar.display.hideMenuButton() + } + } + + @VisibleForTesting + internal fun updateTheme( + @ColorInt toolbarColor: Int? = null, + @ColorInt navigationBarColor: Int? = null, + @ColorInt navigationBarDividerColor: Int? = null, + @ColorInt readableColor: Int, + ) { + toolbarColor?.let { + toolbar.setBackgroundColor(it) + + toolbar.display.colors = toolbar.display.colors.copy( + text = readableColor, + title = readableColor, + securityIconSecure = readableColor, + securityIconInsecure = readableColor, + trackingProtection = readableColor, + menu = readableColor, + ) + + window?.setStatusBarTheme(it) + } + + if (navigationBarColor != null || navigationBarDividerColor != null) { + window?.setNavigationBarTheme(navigationBarColor, navigationBarDividerColor) + } + } + + /** + * Display a close button at the start of the toolbar. + * When clicked, it calls [closeListener]. + */ + @VisibleForTesting + internal fun addCloseButton(@ColorInt readableColor: Int, bitmap: Bitmap?) { + val drawableIcon = bitmap?.toDrawable(context.resources) + ?: getDrawable(context, iconsR.drawable.mozac_ic_cross_24)!!.mutate() + + drawableIcon.setTint(readableColor) + + val button = Toolbar.ActionButton( + drawableIcon, + context.getString(R.string.mozac_feature_customtabs_exit_button), + ) { + emitCloseFact() + session?.let { + useCases.remove(it.id) + } + closeListener.invoke() + } + toolbar.addNavigationAction(button) + } + + /** + * Display an action button from the custom tab config on the toolbar. + * When clicked, it activates the corresponding [PendingIntent]. + */ + @VisibleForTesting + internal fun addActionButton( + @ColorInt readableColor: Int, + buttonConfig: CustomTabActionButtonConfig?, + ) { + buttonConfig?.let { config -> + val icon = config.icon + val scaledIconSize = icon.resizeMaintainingAspectRatio(ACTION_BUTTON_MAX_DRAWABLE_DP_SIZE) + val drawableIcon = Bitmap.createScaledBitmap( + icon, + scaledIconSize.width.dpToPx(context.resources.displayMetrics), + scaledIconSize.height.dpToPx(context.resources.displayMetrics), + true, + ).toDrawable(context.resources) + + if (config.tint || forceActionButtonTinting) { + drawableIcon.setTint(readableColor) + } + + val button = Toolbar.ActionButton( + drawableIcon, + config.description, + ) { + emitActionButtonFact() + session?.let { + config.pendingIntent.sendWithUrl(context, it.content.url) + } + } + + toolbar.addBrowserAction(button) + } + } + + /** + * Display a share button as a button on the toolbar. + * When clicked, it activates [shareListener] and defaults to the [share] KTX helper. + */ + @VisibleForTesting + internal fun addShareButton(@ColorInt readableColor: Int) { + val drawableIcon = getDrawable(context, iconsR.drawable.mozac_ic_share_android_24)!! + drawableIcon.setTint(readableColor) + + val button = Toolbar.ActionButton( + drawableIcon, + context.getString(R.string.mozac_feature_customtabs_share_link), + ) { + val listener = shareListener ?: { + session?.let { + context.share(it.content.url) + } + } + emitActionButtonFact() + listener.invoke() + } + + toolbar.addBrowserAction(button) + } + + /** + * Build the menu items displayed when the 3-dot overflow menu is opened. + */ + @VisibleForTesting + internal fun addMenuItems( + menuItems: List<CustomTabMenuItem>, + index: Int, + ) { + menuItems.map { item -> + SimpleBrowserMenuItem(item.name) { + session?.let { + item.pendingIntent.sendWithUrl(context, it.content.url) + } + } + }.also { items -> + val combinedItems = menuBuilder?.let { builder -> + val newMenuItemList = mutableListOf<BrowserMenuItem>() + val insertIndex = index.coerceIn(0, builder.items.size) + + newMenuItemList.apply { + addAll(builder.items) + addAll(insertIndex, items) + } + } ?: items + + val combinedExtras = menuBuilder?.let { builder -> + builder.extras + Pair("customTab", true) + } + + toolbar.display.menuBuilder = BrowserMenuBuilder(combinedItems, combinedExtras.orEmpty()) + } + } + + /** + * When the back button is pressed if not initialized returns false, + * when initialized removes the current Custom Tabs session and returns true. + * Should be called when the back button is pressed. + */ + override fun onBackPressed(): Boolean { + return if (!initialized) { + false + } else { + if (sessionId != null && useCases.remove(sessionId)) { + closeListener.invoke() + true + } else { + false + } + } + } + + companion object { + private val ACTION_BUTTON_MAX_DRAWABLE_DP_SIZE = Size(48, 24) + } +} + +@VisibleForTesting +internal fun ColorSchemes.getConfiguredColorSchemeParams( + @NightMode nightMode: Int? = null, + isDarkMode: Boolean = false, +) = when { + noColorSchemeParamsSet() -> null + + defaultColorSchemeParamsOnly() -> defaultColorSchemeParams + + // Try to follow specified color scheme. + nightMode == MODE_NIGHT_FOLLOW_SYSTEM -> { + if (isDarkMode) { + darkColorSchemeParams?.withDefault(defaultColorSchemeParams) + ?: defaultColorSchemeParams + } else { + lightColorSchemeParams?.withDefault(defaultColorSchemeParams) + ?: defaultColorSchemeParams + } + } + + nightMode == MODE_NIGHT_NO -> lightColorSchemeParams?.withDefault( + defaultColorSchemeParams, + ) ?: defaultColorSchemeParams + + nightMode == MODE_NIGHT_YES -> darkColorSchemeParams?.withDefault( + defaultColorSchemeParams, + ) ?: defaultColorSchemeParams + + // No color scheme set, try to use default. + else -> defaultColorSchemeParams +} + +/** + * Try to convert the given [ColorScheme] to [NightMode]. + */ +@VisibleForTesting +@NightMode +internal fun Int.toNightMode() = when (this) { + COLOR_SCHEME_SYSTEM -> MODE_NIGHT_FOLLOW_SYSTEM + COLOR_SCHEME_LIGHT -> MODE_NIGHT_NO + COLOR_SCHEME_DARK -> MODE_NIGHT_YES + else -> null +} + +private fun ColorSchemes.noColorSchemeParamsSet() = + defaultColorSchemeParams == null && lightColorSchemeParams == null && darkColorSchemeParams == null + +private fun ColorSchemes.defaultColorSchemeParamsOnly() = + defaultColorSchemeParams != null && lightColorSchemeParams == null && darkColorSchemeParams == null + +private fun Context.isDarkMode() = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + +/** + * Try to create a [ColorSchemeParams] using the given [defaultColorSchemeParam] as a fallback if + * there are missing properties. + */ +@VisibleForTesting +internal fun ColorSchemeParams.withDefault(defaultColorSchemeParam: ColorSchemeParams?) = ColorSchemeParams( + toolbarColor = toolbarColor + ?: defaultColorSchemeParam?.toolbarColor, + secondaryToolbarColor = secondaryToolbarColor + ?: defaultColorSchemeParam?.secondaryToolbarColor, + navigationBarColor = navigationBarColor + ?: defaultColorSchemeParam?.navigationBarColor, + navigationBarDividerColor = navigationBarDividerColor + ?: defaultColorSchemeParam?.navigationBarDividerColor, +) diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserver.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserver.kt new file mode 100644 index 0000000000..446542030a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserver.kt @@ -0,0 +1,50 @@ +/* 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.feature.customtabs.feature + +import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.concept.toolbar.Toolbar + +/** + * Sets the title of the custom tab toolbar based on the session title and URL. + */ +class CustomTabSessionTitleObserver( + private val toolbar: Toolbar, +) { + private var url: String? = null + private var title: String? = null + private var showedTitle = false + + internal fun onTab(tab: CustomTabSessionState) { + if (tab.content.title != title) { + onTitleChanged(tab) + title = tab.content.title + } + + if (tab.content.url != url) { + onUrlChanged(tab) + url = tab.content.url + } + } + + private fun onUrlChanged(tab: CustomTabSessionState) { + // If we showed a title once in a custom tab then we are going to continue displaying + // a title (to avoid the layout bouncing around). However if no title is available then + // we just use the URL. + if (showedTitle && tab.content.title.isEmpty()) { + toolbar.title = tab.content.url + } + } + + private fun onTitleChanged(tab: CustomTabSessionState) { + if (tab.content.title.isNotEmpty()) { + toolbar.title = tab.content.title + showedTitle = true + } else if (showedTitle) { + // See comment in OnUrlChanged(). + toolbar.title = tab.content.url + } + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeature.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeature.kt new file mode 100644 index 0000000000..d92c26cda0 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeature.kt @@ -0,0 +1,67 @@ +/* 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.feature.customtabs.feature + +import android.content.pm.PackageManager +import android.net.Uri +import androidx.annotation.VisibleForTesting +import androidx.browser.customtabs.CustomTabsService.Relation +import androidx.browser.customtabs.CustomTabsSessionToken +import mozilla.components.feature.customtabs.store.CustomTabState +import mozilla.components.feature.customtabs.store.CustomTabsAction +import mozilla.components.feature.customtabs.store.OriginRelationPair +import mozilla.components.feature.customtabs.store.ValidateRelationshipAction +import mozilla.components.feature.customtabs.store.VerificationStatus.FAILURE +import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING +import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS +import mozilla.components.feature.customtabs.verify.OriginVerifier +import mozilla.components.service.digitalassetlinks.RelationChecker + +class OriginVerifierFeature( + private val packageManager: PackageManager, + private val relationChecker: RelationChecker, + private val dispatch: (CustomTabsAction) -> Unit, +) { + + private var cachedVerifier: Triple<String, Int, OriginVerifier>? = null + + suspend fun verify( + state: CustomTabState, + token: CustomTabsSessionToken, + @Relation relation: Int, + origin: Uri, + ): Boolean { + val packageName = state.creatorPackageName ?: return false + + val existingRelation = state.relationships[OriginRelationPair(origin, relation)] + return if (existingRelation == SUCCESS || existingRelation == FAILURE) { + // Return if relation is already success or failure + existingRelation == SUCCESS + } else { + val verifier = getVerifier(packageName, relation) + dispatch(ValidateRelationshipAction(token, relation, origin, PENDING)) + + val result = verifier.verifyOrigin(origin) + val status = if (result) SUCCESS else FAILURE + + dispatch(ValidateRelationshipAction(token, relation, origin, status)) + result + } + } + + @VisibleForTesting + internal fun getVerifier(packageName: String, @Relation relation: Int): OriginVerifier { + cachedVerifier?.let { + val (cachedPackage, cachedRelation, verifier) = it + if (cachedPackage == packageName && cachedRelation == relation) { + return verifier + } + } + + return OriginVerifier(packageName, relation, packageManager, relationChecker).also { + cachedVerifier = Triple(packageName, relation, it) + } + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidates.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidates.kt new file mode 100644 index 0000000000..07bf60b119 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidates.kt @@ -0,0 +1,32 @@ +/* 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.feature.customtabs.menu + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.concept.menu.candidate.TextMenuCandidate + +/** + * Build menu items displayed when the 3-dot overflow menu is opened. + * These items are provided by the app that creates the custom tab, + * and should be inserted alongside menu items created by the browser. + */ +fun CustomTabSessionState.createCustomTabMenuCandidates(context: Context) = + config.menuItems.map { item -> + TextMenuCandidate( + text = item.name, + ) { + item.pendingIntent.sendWithUrl(context, content.url) + } + } + +internal fun PendingIntent.sendWithUrl(context: Context, url: String) = send( + context, + 0, + Intent(null, url.toUri()), +) diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsAction.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsAction.kt new file mode 100644 index 0000000000..5c9e6941e6 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsAction.kt @@ -0,0 +1,40 @@ +/* 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.feature.customtabs.store + +import android.net.Uri +import androidx.browser.customtabs.CustomTabsService.Relation +import androidx.browser.customtabs.CustomTabsSessionToken +import mozilla.components.lib.state.Action + +sealed class CustomTabsAction : Action { + abstract val token: CustomTabsSessionToken +} + +/** + * Saves the package name corresponding to a custom tab token. + * + * @property token Token of the custom tab. + * @property packageName Package name of the app that created the custom tab. + */ +data class SaveCreatorPackageNameAction( + override val token: CustomTabsSessionToken, + val packageName: String, +) : CustomTabsAction() + +/** + * Marks the state of a custom tabs [Relation] verification. + * + * @property token Token of the custom tab to verify. + * @property relation Relationship type to verify. + * @property origin Origin to verify. + * @property status State of the verification process. + */ +data class ValidateRelationshipAction( + override val token: CustomTabsSessionToken, + @Relation val relation: Int, + val origin: Uri, + val status: VerificationStatus, +) : CustomTabsAction() diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceState.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceState.kt new file mode 100644 index 0000000000..e95b4262ce --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceState.kt @@ -0,0 +1,68 @@ +/* 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.feature.customtabs.store + +import android.net.Uri +import androidx.browser.customtabs.CustomTabsService +import androidx.browser.customtabs.CustomTabsSessionToken +import mozilla.components.lib.state.State + +/** + * Value type that represents the custom tabs state + * accessible from both the service and activity. + */ +data class CustomTabsServiceState( + val tabs: Map<CustomTabsSessionToken, CustomTabState> = emptyMap(), +) : State + +/** + * Value type that represents the state of a single custom tab + * accessible from both the service and activity. + * + * This data is meant to supplement [mozilla.components.browser.session.tab.CustomTabConfig], + * not replace it. It only contains data that the service also needs to work with. + * + * @property creatorPackageName Package name of the app that created the custom tab. + * @property relationships Map of origin and relationship type to current verification state. + */ +data class CustomTabState( + val creatorPackageName: String? = null, + val relationships: Map<OriginRelationPair, VerificationStatus> = emptyMap(), +) + +/** + * Pair of origin and relation type used as key in [CustomTabState.relationships]. + * + * @property origin URL that contains only the scheme, host, and port. + * https://html.spec.whatwg.org/multipage/origin.html#concept-origin + * @property relation Enum that indicates the relation type. + */ +data class OriginRelationPair( + val origin: Uri, + @CustomTabsService.Relation val relation: Int, +) + +/** + * Different states of Digital Asset Link verification. + */ +enum class VerificationStatus { + /** + * Indicates verification has started and hasn't returned yet. + * + * To avoid flashing the toolbar, we choose to hide it when a Digital Asset Link is being verified. + * We only show the toolbar when the verification fails, or an origin never requested to be verified. + */ + PENDING, + + /** + * Indicates that verification has completed and the link was verified. + */ + SUCCESS, + + /** + * Indicates that verification has completed and the link was invalid. + */ + FAILURE, +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducer.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducer.kt new file mode 100644 index 0000000000..13ee59c934 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducer.kt @@ -0,0 +1,28 @@ +/* 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.feature.customtabs.store + +internal object CustomTabsServiceStateReducer { + + fun reduce(state: CustomTabsServiceState, action: CustomTabsAction): CustomTabsServiceState { + val tabState = state.tabs.getOrElse(action.token) { CustomTabState() } + val newTabState = reduceTab(tabState, action) + return state.copy(tabs = state.tabs + Pair(action.token, newTabState)) + } + + private fun reduceTab(state: CustomTabState, action: CustomTabsAction): CustomTabState { + return when (action) { + is SaveCreatorPackageNameAction -> + state.copy(creatorPackageName = action.packageName) + is ValidateRelationshipAction -> + state.copy( + relationships = state.relationships + Pair( + OriginRelationPair(action.origin, action.relation), + action.status, + ), + ) + } + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStore.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStore.kt new file mode 100644 index 0000000000..e0b9ba5b86 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStore.kt @@ -0,0 +1,15 @@ +/* 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.feature.customtabs.store + +import mozilla.components.lib.state.Store + +class CustomTabsServiceStore( + initialState: CustomTabsServiceState = CustomTabsServiceState(), +) : Store<CustomTabsServiceState, CustomTabsAction>( + initialState, + CustomTabsServiceStateReducer::reduce, + threadNamePrefix = "CustomTabsService", +) diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/verify/OriginVerifier.kt b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/verify/OriginVerifier.kt new file mode 100644 index 0000000000..bcf4f34541 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/verify/OriginVerifier.kt @@ -0,0 +1,76 @@ +/* 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.feature.customtabs.verify + +import android.content.pm.PackageManager +import android.net.Uri +import androidx.annotation.VisibleForTesting +import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS +import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN +import androidx.browser.customtabs.CustomTabsService.Relation +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext +import mozilla.components.concept.fetch.Client +import mozilla.components.service.digitalassetlinks.AndroidAssetFinder +import mozilla.components.service.digitalassetlinks.AssetDescriptor +import mozilla.components.service.digitalassetlinks.Relation.HANDLE_ALL_URLS +import mozilla.components.service.digitalassetlinks.Relation.USE_AS_ORIGIN +import mozilla.components.service.digitalassetlinks.RelationChecker + +/** + * Used to verify postMessage origin for a designated package name. + * + * Uses Digital Asset Links to confirm that the given origin is associated with the package name. + * It caches any origin that has been verified during the current application + * lifecycle and reuses that without making any new network requests. + */ +class OriginVerifier( + private val packageName: String, + @Relation private val relation: Int, + packageManager: PackageManager, + private val relationChecker: RelationChecker, +) { + + @VisibleForTesting + internal val androidAsset by lazy { + AndroidAssetFinder().getAndroidAppAsset(packageName, packageManager).firstOrNull() + } + + /** + * Verify the claimed origin for the cached package name asynchronously. This will end up + * making a network request for non-cached origins with a HTTP [Client]. + * + * @param origin The postMessage origin the application is claiming to have. Can't be null. + */ + suspend fun verifyOrigin(origin: Uri) = withContext(IO) { verifyOriginInternal(origin) } + + @Suppress("ReturnCount") + private fun verifyOriginInternal(origin: Uri): Boolean { + val cachedOrigin = cachedOriginMap[packageName] + if (cachedOrigin == origin) return true + + if (origin.scheme != "https") return false + val relationship = when (relation) { + RELATION_USE_AS_ORIGIN -> USE_AS_ORIGIN + RELATION_HANDLE_ALL_URLS -> HANDLE_ALL_URLS + else -> return false + } + + val originVerified = relationChecker.checkRelationship( + source = AssetDescriptor.Web(site = origin.toString()), + target = androidAsset ?: return false, + relation = relationship, + ) + + if (originVerified && packageName !in cachedOriginMap) { + cachedOriginMap[packageName] = origin + } + return originVerified + } + + companion object { + private val cachedOriginMap = mutableMapOf<String, Uri>() + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-am/strings.xml new file mode 100644 index 0000000000..c7214823df --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-am/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">ወደ ቀዳሚው መተግበሪያ ተመለስ</string> + <string name="mozac_feature_customtabs_share_link">አገናኝ አጋራ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-an/strings.xml new file mode 100644 index 0000000000..e0e79c0ca1 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-an/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Tornar ta l’aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir lo vinclo</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..6815ac4db3 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ar/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">ارجع للتطبيق السابق</string> + <string name="mozac_feature_customtabs_share_link">شارِك الرابط</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ast/strings.xml new file mode 100644 index 0000000000..20a0e7e9fe --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ast/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Volver a l\'aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir l\'enllaz</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-az/strings.xml new file mode 100644 index 0000000000..b225d20e41 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-az/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Əvvəlki tətbiqə qayıt</string> + <string name="mozac_feature_customtabs_share_link">Keçidi paylaş</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-azb/strings.xml new file mode 100644 index 0000000000..deac72bfb9 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-azb/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">اؤنجهکی اَپَه دؤن</string> + <string name="mozac_feature_customtabs_share_link">باغلانتینی پایلاش</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ban/strings.xml new file mode 100644 index 0000000000..b990cefbab --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ban/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Uliang ka aplikasi sadurungnyane</string> + <string name="mozac_feature_customtabs_share_link">Ngbagiang tautan</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-be/strings.xml new file mode 100644 index 0000000000..fee7305095 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-be/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Вярнуцца ў папярэднюю праграму</string> + <string name="mozac_feature_customtabs_share_link">Падзяліцца спасылкай</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..967d209d75 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bg/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Връщане към предишното приложение</string> + <string name="mozac_feature_customtabs_share_link">Споделяне на препратка</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bn/strings.xml new file mode 100644 index 0000000000..4676782818 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bn/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">আগের অ্যাপে ফিরে যান</string> + <string name="mozac_feature_customtabs_share_link">লিংক শেয়ার করুন</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-br/strings.xml new file mode 100644 index 0000000000..7e77e03600 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-br/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Distreiñ dʼan arload kent</string> + <string name="mozac_feature_customtabs_share_link">Rannañ an ere</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bs/strings.xml new file mode 100644 index 0000000000..f9624f2eaa --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-bs/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Povratak na prethodnu aplikaciju</string> + <string name="mozac_feature_customtabs_share_link">Podijeli link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..e66e0ab37e --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ca/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Torna a l’aplicació anterior</string> + <string name="mozac_feature_customtabs_share_link">Comparteix l’enllaç</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cak/strings.xml new file mode 100644 index 0000000000..47a7f2d06b --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cak/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Titzolin pa ri jun chokoy</string> + <string name="mozac_feature_customtabs_share_link">Tikomonïx ri ximonel</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ceb/strings.xml new file mode 100644 index 0000000000..3a63483dab --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ceb/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Balik sa previous nga app</string> + <string name="mozac_feature_customtabs_share_link">i-Share ang link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ckb/strings.xml new file mode 100644 index 0000000000..78ded2fc7b --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ckb/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">بگەڕێوە بۆ بەرنامەی پێشوو</string> + <string name="mozac_feature_customtabs_share_link">بەستەر بڵاوبکەرەوە</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-co/strings.xml new file mode 100644 index 0000000000..912e8712fd --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-co/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Rivene à l’appiecazione precedente</string> + <string name="mozac_feature_customtabs_share_link">Sparte u liame</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..0a6027a0ec --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cs/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Návrat do předchozí aplikace</string> + <string name="mozac_feature_customtabs_share_link">Sdílet odkaz</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cy/strings.xml new file mode 100644 index 0000000000..04e4257505 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-cy/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Nôl i’r ap blaenorol</string> + <string name="mozac_feature_customtabs_share_link">Rhannu dolen</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000..52264f69a0 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-da/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Tilbage til forrige app</string> + <string name="mozac_feature_customtabs_share_link">Del link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..a061bc7ba2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-de/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Zurück zur vorherigen App</string> + <string name="mozac_feature_customtabs_share_link">Link teilen</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-dsb/strings.xml new file mode 100644 index 0000000000..b6c295e334 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-dsb/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Slědk k pjerwjejšnemu nałoženjeju</string> + <string name="mozac_feature_customtabs_share_link">Wótkaz źěliś</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000..4f7ef1cc74 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-el/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Επιστροφή στην προηγούμενη εφαρμογή</string> + <string name="mozac_feature_customtabs_share_link">Κοινή χρήση συνδέσμου</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rCA/strings.xml new file mode 100644 index 0000000000..7d001b2872 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rCA/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Return to previous app</string> + <string name="mozac_feature_customtabs_share_link">Share link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..7d001b2872 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Return to previous app</string> + <string name="mozac_feature_customtabs_share_link">Share link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000000..867203597e --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eo/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Reen al antaŭa programo</string> + <string name="mozac_feature_customtabs_share_link">Kundividi ligilon</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rAR/strings.xml new file mode 100644 index 0000000000..ec4b3f66e2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rAR/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir enlace</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rCL/strings.xml new file mode 100644 index 0000000000..8eda46d9e3 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rCL/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Regresar a la aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir enlace</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000000..ec4b3f66e2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir enlace</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rMX/strings.xml new file mode 100644 index 0000000000..ec4b3f66e2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es-rMX/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir enlace</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..ec4b3f66e2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-es/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Volver a la aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir enlace</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..4b62fbc1be --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-et/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Tagasi eelmise äpi juurde</string> + <string name="mozac_feature_customtabs_share_link">Jaga linki</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..0494ad16ad --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-eu/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Itzuli aurreko aplikaziora</string> + <string name="mozac_feature_customtabs_share_link">Partekatu lotura</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000..6a4589a70a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fa/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">بازگشت به برنامهٔ قبل</string> + <string name="mozac_feature_customtabs_share_link">اشتراکگذاری پیوند</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ff/strings.xml new file mode 100644 index 0000000000..17aaa5ca70 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ff/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Rutto e jaaɓnirgal ɓennungal</string> + <string name="mozac_feature_customtabs_share_link">Lollin jokkol</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..9eb780ce76 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fi/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Palaa edelliseen sovellukseen</string> + <string name="mozac_feature_customtabs_share_link">Jaa linkki</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..71e5ba49b7 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fr/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Revenir à l’application précédente</string> + <string name="mozac_feature_customtabs_share_link">Partager le lien</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fur/strings.xml new file mode 100644 index 0000000000..d0c99e5949 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fur/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Torne ae aplicazion precedente</string> + <string name="mozac_feature_customtabs_share_link">Condivît link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fy-rNL/strings.xml new file mode 100644 index 0000000000..a34d8bc45f --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-fy-rNL/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Tebek nei foarige app</string> + <string name="mozac_feature_customtabs_share_link">Keppeling diele</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ga-rIE/strings.xml new file mode 100644 index 0000000000..26d689932b --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ga-rIE/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Fill ar an aip roimhe seo</string> + <string name="mozac_feature_customtabs_share_link">Comhroinn an nasc</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gd/strings.xml new file mode 100644 index 0000000000..f194676ccb --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gd/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Till gun aplacaid roimhe</string> + <string name="mozac_feature_customtabs_share_link">Co-roinn an ceangal</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000000..f3044f9fb5 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gl/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Volver á aplicación anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartir ligazón</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gn/strings.xml new file mode 100644 index 0000000000..1e65bfac50 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gn/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Ejevyjey tembiporu’i mboyveguávape</string> + <string name="mozac_feature_customtabs_share_link">Emoherakuã juajuha</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gu-rIN/strings.xml new file mode 100644 index 0000000000..030d775962 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-gu-rIN/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">જુના એપમાં પાંછા જાઓ</string> + <string name="mozac_feature_customtabs_share_link">લિંક શેર કરો</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 0000000000..13a4031103 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">पिछले ऐप पर वापस जाएं</string> + <string name="mozac_feature_customtabs_share_link">लिंक साझा करें</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000000..b975cab3ac --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hr/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Vrati se na prethodnu aplikaciju</string> + <string name="mozac_feature_customtabs_share_link">Dijeli poveznicu</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hsb/strings.xml new file mode 100644 index 0000000000..f4d842a363 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hsb/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Wróćo k předchadnemu nałoženju</string> + <string name="mozac_feature_customtabs_share_link">Wotkaz dźělić</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..ffee0390d9 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hu/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Vissza az előző apphoz</string> + <string name="mozac_feature_customtabs_share_link">Hivatkozás megosztása</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000000..11e13e8549 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-hy-rAM/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Վերադառնալ նախորդ հավելվածին</string> + <string name="mozac_feature_customtabs_share_link">Տարածել հղումը</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ia/strings.xml new file mode 100644 index 0000000000..025c1f6fea --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ia/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Retornar al app previe</string> + <string name="mozac_feature_customtabs_share_link">Compartir le ligamine</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-in/strings.xml new file mode 100644 index 0000000000..ba8bbc8887 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-in/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Kembali ke aplikasi sebelumnya</string> + <string name="mozac_feature_customtabs_share_link">Bagikan tautan</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-is/strings.xml new file mode 100644 index 0000000000..8436de9ccc --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-is/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Farðu aftur í fyrra smáforrit</string> + <string name="mozac_feature_customtabs_share_link">Deila tengli</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..bbb8597fab --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-it/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Torna all’applicazione precedente</string> + <string name="mozac_feature_customtabs_share_link">Condividi link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-iw/strings.xml new file mode 100644 index 0000000000..bcb8ba7e7b --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-iw/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">חזרה ליישומון הקודם</string> + <string name="mozac_feature_customtabs_share_link">שיתוף קישור</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..c032d9fca5 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ja/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">前のアプリへ戻る</string> + <string name="mozac_feature_customtabs_share_link">リンクを共有</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ka/strings.xml new file mode 100644 index 0000000000..d1635e7825 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ka/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">წინა პროგრამაზე დაბრუნება</string> + <string name="mozac_feature_customtabs_share_link">ბმულის გაზიარება</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kaa/strings.xml new file mode 100644 index 0000000000..3282c0c294 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kaa/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Aldınǵı baǵdarlamaǵa qaytıw</string> + <string name="mozac_feature_customtabs_share_link">Siltemeni bólisiw</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kab/strings.xml new file mode 100644 index 0000000000..152805abb6 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kab/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Uɣal ar usnas izrin</string> + <string name="mozac_feature_customtabs_share_link">Bḍu aseɣwen</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kk/strings.xml new file mode 100644 index 0000000000..34c58c3238 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kk/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Алдыңғы қолданбаға оралу</string> + <string name="mozac_feature_customtabs_share_link">Сілтемемен бөлісу</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kmr/strings.xml new file mode 100644 index 0000000000..25aca1dea7 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kmr/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Vegere sepana berê</string> + <string name="mozac_feature_customtabs_share_link">Girêdanê parve bike</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kn/strings.xml new file mode 100644 index 0000000000..8a851a3309 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-kn/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">ಹಿಂದಿನ ಅನ್ವಯಕ್ಕೆ ಮರಳಿ</string> + <string name="mozac_feature_customtabs_share_link">ಕೊಂಡಿಯನ್ನು ಹಂಚಿಕೊಳ್ಳಿ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000000..b41a66886d --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ko/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">이전 앱으로 돌아가기</string> + <string name="mozac_feature_customtabs_share_link">링크 공유</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lij/strings.xml new file mode 100644 index 0000000000..dec5eaf455 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lij/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Vanni a l\'app de primma</string> + <string name="mozac_feature_customtabs_share_link">Condividdi link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..5de508f665 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lo/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">ກັບໄປຫາແອັບກ່ອນຫນ້ານີ້</string> + <string name="mozac_feature_customtabs_share_link">ແບ່ງປັນລີ້ງ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000..61de1b3ecf --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-lt/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Grįžti į ankstesnę programą</string> + <string name="mozac_feature_customtabs_share_link">Dalintis saitu</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ml/strings.xml new file mode 100644 index 0000000000..a1ee4e969f --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ml/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">മുമ്പത്തെ ആപ്പിലേക്ക് മടങ്ങുക</string> + <string name="mozac_feature_customtabs_share_link">കണ്ണി പങ്കിടുക</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-mr/strings.xml new file mode 100644 index 0000000000..4bacf2546d --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-mr/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">मागील अॅप वर परत या</string> + <string name="mozac_feature_customtabs_share_link">दुवा शेअर करा</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-my/strings.xml new file mode 100644 index 0000000000..1084e61f08 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-my/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">အရင်ကအက်ပ်သို့ သွားပါ</string> + <string name="mozac_feature_customtabs_share_link">လင့်ခ်ကို မျှဝေရန်</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 0000000000..d319f8b77a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Gå tilbake til forrige app</string> + <string name="mozac_feature_customtabs_share_link">Del lenke</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ne-rNP/strings.xml new file mode 100644 index 0000000000..9630785ed2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ne-rNP/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">पहिलाको एपमा फर्किनुहोस्</string> + <string name="mozac_feature_customtabs_share_link">लिङ्क सेयर गर्नुहोस्</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..6b0f757532 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nl/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Terug naar vorige app</string> + <string name="mozac_feature_customtabs_share_link">Koppeling delen</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nn-rNO/strings.xml new file mode 100644 index 0000000000..a815c4d553 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-nn-rNO/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Gå tilbake til førre app</string> + <string name="mozac_feature_customtabs_share_link">Del lenke</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-oc/strings.xml new file mode 100644 index 0000000000..01a0d4c645 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-oc/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Tornar a l’aplicacion precedenta</string> + <string name="mozac_feature_customtabs_share_link">Partejar lo ligam</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-or/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-or/strings.xml new file mode 100644 index 0000000000..1a4925a2c1 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-or/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_share_link">ଲିଙ୍କ ବିତରଣ କରନ୍ତୁ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rIN/strings.xml new file mode 100644 index 0000000000..9a2a887cab --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rIN/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">ਪਿਛਲੀ ਐਪ ‘ਤੇ ਜਾਓ</string> + <string name="mozac_feature_customtabs_share_link">ਲਿੰਕ ਸਾਂਝਾ ਕਰੋ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rPK/strings.xml new file mode 100644 index 0000000000..431f8fa03f --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pa-rPK/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">پچھلی ایپ نوں واپس جاؤ</string> + <string name="mozac_feature_customtabs_share_link">پتہ سانجھا کرو</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000..1cee15d909 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pl/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Wróć do poprzedniej aplikacji</string> + <string name="mozac_feature_customtabs_share_link">Udostępnij odnośnik</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..2838396787 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Retornar ao aplicativo anterior</string> + <string name="mozac_feature_customtabs_share_link">Compartilhar link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..94edd3d2a2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Voltar à aplicação anterior</string> + <string name="mozac_feature_customtabs_share_link">Partilhar ligação</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-rm/strings.xml new file mode 100644 index 0000000000..5c8624edf4 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-rm/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Turnar a la app precedenta</string> + <string name="mozac_feature_customtabs_share_link">Cundivider la colliaziun</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000000..f1bd593246 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ro/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Revenire la aplicația anterioară</string> + <string name="mozac_feature_customtabs_share_link">Partajează linkul</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..bfdac1f591 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ru/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Вернуться к предыдущему приложению</string> + <string name="mozac_feature_customtabs_share_link">Поделиться ссылкой</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sat/strings.xml new file mode 100644 index 0000000000..519b799596 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sat/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">ᱞᱟᱦᱟ ᱛᱮᱭᱟᱜ ᱮᱯ ᱨᱮ ᱨᱩᱣᱟᱹᱲᱚᱜ ᱢᱮ</string> + <string name="mozac_feature_customtabs_share_link">ᱞᱤᱝᱠ ᱦᱟᱹᱴᱤᱧ ᱢᱮ</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sc/strings.xml new file mode 100644 index 0000000000..33b8dad9e6 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sc/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Torra a s’aplicatzione pretzedente</string> + <string name="mozac_feature_customtabs_share_link">Cumpartzi su ligòngiu</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-si/strings.xml new file mode 100644 index 0000000000..f62e75a275 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-si/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">කලින් යෙදුමට ආපසු</string> + <string name="mozac_feature_customtabs_share_link">සබැඳිය බෙදාගන්න</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..db1a9c2fef --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sk/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Návrat do predchádzajúcej aplikácie</string> + <string name="mozac_feature_customtabs_share_link">Zdieľať odkaz</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-skr/strings.xml new file mode 100644 index 0000000000..1336257ab3 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-skr/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">پچھلی ایپ تے واپس ون٘ڄو</string> + <string name="mozac_feature_customtabs_share_link">لنک شیئر کرو</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000000..e71763b1ae --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sl/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Nazaj na prejšnjo aplikacijo</string> + <string name="mozac_feature_customtabs_share_link">Deli povezavo</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sq/strings.xml new file mode 100644 index 0000000000..f616f9fee4 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sq/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Kthehu te aplikacioni i mëparshëm</string> + <string name="mozac_feature_customtabs_share_link">Ndani lidhje me të tjerët</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000000..6fd3ffc4e4 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sr/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Врати се на претходну апликацију</string> + <string name="mozac_feature_customtabs_share_link">Подели везу</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-su/strings.xml new file mode 100644 index 0000000000..6baec852bf --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-su/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Balik deui ka aplikasi saméméhna</string> + <string name="mozac_feature_customtabs_share_link">Bagikeun tutumbu</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sv-rSE/strings.xml new file mode 100644 index 0000000000..ac83bc4426 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-sv-rSE/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Återgå till föregående app</string> + <string name="mozac_feature_customtabs_share_link">Dela länk</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000000..1935e8b52e --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ta/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">முந்தைய செயலிக்குத் திரும்பு</string> + <string name="mozac_feature_customtabs_share_link">தொடுப்பைப் பகிர்</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-te/strings.xml new file mode 100644 index 0000000000..cc2cfcd80b --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-te/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">మునుపటి అనువర్తనానికి తిరిగి వెళ్ళు</string> + <string name="mozac_feature_customtabs_share_link">లంకెను పంచుకోండి</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tg/strings.xml new file mode 100644 index 0000000000..bc9afc0c8e --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tg/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Бозгашт ба барномаи қаблӣ</string> + <string name="mozac_feature_customtabs_share_link">Мубодила кардани пайванд</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..177b490040 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-th/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">กลับไปที่แอปก่อนหน้า</string> + <string name="mozac_feature_customtabs_share_link">แบ่งปันลิงก์</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tl/strings.xml new file mode 100644 index 0000000000..3d37950f8c --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tl/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Bumalik sa nakaraang app</string> + <string name="mozac_feature_customtabs_share_link">Ibahagi ang link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tok/strings.xml new file mode 100644 index 0000000000..ec2c84f263 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tok/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">o tawa ilo pini</string> + <string name="mozac_feature_customtabs_share_link">o pana e nimi nasin</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000000..ec428ff885 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tr/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Önceki uygulamaya dön</string> + <string name="mozac_feature_customtabs_share_link">Bağlantıyı paylaş</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-trs/strings.xml new file mode 100644 index 0000000000..ffea9b71c2 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-trs/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Nānīkāj riña aplikasiûn garâjsunt akuan\'</string> + <string name="mozac_feature_customtabs_share_link">Dūyingô\' lînk</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tt/strings.xml new file mode 100644 index 0000000000..68c4066531 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tt/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Соңгы кушымтага кире кайту</string> + <string name="mozac_feature_customtabs_share_link">Сылтаманы уртаклашу</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tzm/strings.xml new file mode 100644 index 0000000000..cece299be4 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-tzm/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_share_link">Bḍu asɣen</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ug/strings.xml new file mode 100644 index 0000000000..6091e4f4e4 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ug/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">ئالدىنقى ئەپكە قايت</string> + <string name="mozac_feature_customtabs_share_link">ئۇلانمىنى ئورتاقلاش</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..1b927f06ce --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uk/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Повернутись до попередньої програми</string> + <string name="mozac_feature_customtabs_share_link">Поділитись посиланням</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ur/strings.xml new file mode 100644 index 0000000000..f7ffc60127 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-ur/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">پچھلی ایپلیکیشن میں واپس جائیں</string> + <string name="mozac_feature_customtabs_share_link">ربط شیئر کریں</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uz/strings.xml new file mode 100644 index 0000000000..76890b9883 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-uz/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Oldingi ilovaga qaytish</string> + <string name="mozac_feature_customtabs_share_link">Havolani ulashish</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vec/strings.xml new file mode 100644 index 0000000000..ee9fe88ef3 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vec/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Torna indrio a ƚ’aplicasione presedente</string> + <string name="mozac_feature_customtabs_share_link">Condividi link</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..91dabe207a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-vi/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Quay lại ứng dụng trước</string> + <string name="mozac_feature_customtabs_share_link">Chia sẻ liên kết</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-yo/strings.xml new file mode 100644 index 0000000000..793f44af04 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-yo/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">Padà sí áàpù ti tẹ́lẹ̀</string> + <string name="mozac_feature_customtabs_share_link">Pín líǹkì</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..9bc538909f --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">返回之前的应用</string> + <string name="mozac_feature_customtabs_share_link">分享链接</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..fd1477504f --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mozac_feature_customtabs_exit_button">回到先前的應用程式</string> + <string name="mozac_feature_customtabs_share_link">分享鏈結</string> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values/dimens.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..7ec404f03f --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/dimens.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> +<resources> + <dimen name="mozac_feature_customtabs_max_close_button_size">24dp</dimen> +</resources> diff --git a/mobile/android/android-components/components/feature/customtabs/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/strings.xml new file mode 100644 index 0000000000..f10d13b6ae --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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/. + --> + +<resources> + <string name="mozac_feature_customtabs_exit_button">Return to previous app</string> + <string name="mozac_feature_customtabs_share_link">Share link</string> +</resources>
\ No newline at end of file diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt new file mode 100644 index 0000000000..3b143f8721 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt @@ -0,0 +1,130 @@ +/* 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.feature.customtabs + +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.support.customtabs.ICustomTabsCallback +import android.support.customtabs.ICustomTabsService +import androidx.browser.customtabs.CustomTabsService +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.customtabs.store.CustomTabsServiceStore +import mozilla.components.service.digitalassetlinks.RelationChecker +import mozilla.components.support.test.mock +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class AbstractCustomTabsServiceTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + + @Test + fun customTabService() { + val customTabsService = object : MockCustomTabsService() { + override val customTabsServiceStore = CustomTabsServiceStore() + override fun getPackageManager(): PackageManager = mock() + } + + val customTabsServiceStub = customTabsService.onBind(mock()) + assertNotNull(customTabsServiceStub) + + val stub = customTabsServiceStub as ICustomTabsService.Stub + + val callback = mock<ICustomTabsCallback>() + doReturn(mock<IBinder>()).`when`(callback).asBinder() + + assertTrue(stub.warmup(123)) + assertTrue(stub.newSession(callback)) + assertNull(stub.extraCommand("", mock())) + assertFalse(stub.updateVisuals(mock(), mock())) + assertFalse(stub.requestPostMessageChannel(mock(), mock())) + assertEquals( + CustomTabsService.RESULT_FAILURE_DISALLOWED, + stub.postMessage(mock(), "", mock()), + ) + assertFalse( + stub.validateRelationship( + mock(), + 0, + mock(), + mock(), + ), + ) + assertTrue( + stub.mayLaunchUrl( + mock(), + mock(), + mock(), + emptyList<Bundle>(), + ), + ) + } + + @Test + fun `Warmup will access engine instance`() { + var engineAccessed = false + + val customTabsService = object : MockCustomTabsService() { + override val engine: Engine + get() { + engineAccessed = true + return mock() + } + } + + val stub = customTabsService.onBind(mock()) as ICustomTabsService.Stub + + assertTrue(stub.warmup(42)) + + assertTrue(engineAccessed) + } + + @Test + fun `mayLaunchUrl opens a speculative connection for most likely URL`() { + val engine: Engine = mock() + + val customTabsService = object : MockCustomTabsService() { + override val engine: Engine = engine + } + + val stub = customTabsService.onBind(mock()) as ICustomTabsService.Stub + + assertTrue(stub.mayLaunchUrl(mock(), Uri.parse("https://www.mozilla.org"), Bundle(), listOf())) + + verify(engine).speculativeConnect("https://www.mozilla.org") + } + + @Test + fun `verifier is only created when store and client are provided`() { + val basic = MockCustomTabsService() + assertNull(basic.verifier) + + val both = object : MockCustomTabsService() { + override val relationChecker: RelationChecker = mock() + + override fun getPackageManager(): PackageManager = mock() + } + assertNotNull(both.verifier) + } + + private open class MockCustomTabsService : AbstractCustomTabsService() { + override val engine: Engine = mock() + override val customTabsServiceStore: CustomTabsServiceStore = mock() + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt new file mode 100644 index 0000000000..097d95796a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt @@ -0,0 +1,416 @@ +/* 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.feature.customtabs + +import android.app.PendingIntent +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Color +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.util.SparseArray +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.browser.customtabs.TrustedWebUtils +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.ColorSchemeParams +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.utils.toSafeIntent +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class CustomTabConfigHelperTest { + + private lateinit var resources: Resources + + @Before + fun setup() { + resources = spy(testContext.resources) + doReturn(24f).`when`(resources).getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size) + } + + @Test + fun isCustomTabIntent() { + val customTabsIntent = CustomTabsIntent.Builder().build() + assertTrue(isCustomTabIntent(customTabsIntent.intent)) + assertFalse(isCustomTabIntent(mock<Intent>())) + } + + @Test + fun isTrustedWebActivityIntent() { + val customTabsIntent = CustomTabsIntent.Builder().build().intent + val trustedWebActivityIntent = Intent(customTabsIntent) + .putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true) + assertTrue(isTrustedWebActivityIntent(trustedWebActivityIntent)) + assertFalse(isTrustedWebActivityIntent(customTabsIntent)) + assertFalse(isTrustedWebActivityIntent(mock<Intent>())) + assertFalse( + isTrustedWebActivityIntent( + Intent().putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true), + ), + ) + } + + @Test + fun createFromIntentNoColorScheme() { + val customTabsIntent = CustomTabsIntent.Builder().build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(null, result.colorScheme) + } + + @Test + fun createFromIntentWithColorScheme() { + val colorScheme = CustomTabsIntent.COLOR_SCHEME_SYSTEM + val customTabsIntent = CustomTabsIntent.Builder().setColorScheme(colorScheme).build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(colorScheme, result.colorScheme) + } + + @Test + fun createFromIntentNoColorSchemeParams() { + val customTabsIntent = CustomTabsIntent.Builder().build() + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(null, result.colorSchemes) + } + + @Test + fun createFromIntentWithDefaultColorSchemeParams() { + val colorSchemeParams = createColorSchemeParams() + val customTabsIntent = CustomTabsIntent.Builder().setDefaultColorSchemeParams( + createCustomTabColorSchemeParamsFrom(colorSchemeParams), + ).build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(colorSchemeParams, result.colorSchemes!!.defaultColorSchemeParams) + } + + @Test + fun createFromIntentWithDefaultColorSchemeParamsWithNoProperties() { + val customTabsIntent = CustomTabsIntent.Builder().setDefaultColorSchemeParams( + CustomTabColorSchemeParams.Builder().build(), + ).build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(null, result.colorSchemes?.defaultColorSchemeParams) + } + + @Test + fun createFromIntentWithLightColorSchemeParams() { + val colorSchemeParams = createColorSchemeParams() + val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams( + CustomTabsIntent.COLOR_SCHEME_LIGHT, + createCustomTabColorSchemeParamsFrom(colorSchemeParams), + ).build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(colorSchemeParams, result.colorSchemes!!.lightColorSchemeParams) + } + + @Test + fun createFromIntentWithLightColorSchemeParamsWithNoProperties() { + val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams( + CustomTabsIntent.COLOR_SCHEME_LIGHT, + CustomTabColorSchemeParams.Builder().build(), + ).build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(null, result.colorSchemes?.lightColorSchemeParams) + } + + @Test + fun createFromIntentWithDarkColorSchemeParams() { + val colorSchemeParams = createColorSchemeParams() + val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams( + CustomTabsIntent.COLOR_SCHEME_DARK, + createCustomTabColorSchemeParamsFrom(colorSchemeParams), + ).build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(colorSchemeParams, result.colorSchemes!!.darkColorSchemeParams) + } + + @Test + fun createFromIntentWithDarkColorSchemeParamsWithNoProperties() { + val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams( + CustomTabsIntent.COLOR_SCHEME_DARK, + CustomTabColorSchemeParams.Builder().build(), + ).build() + + val result = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertEquals(null, result.colorSchemes?.lightColorSchemeParams) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) + fun getColorSchemeParamsBundleOnAndroidVersionTiramisu() { + val colorScheme = CustomTabsIntent.COLOR_SCHEME_DARK + val colorSchemeParams = createColorSchemeParams() + val customTabColorScheme = createCustomTabColorSchemeParamsFrom(colorSchemeParams) + val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams( + colorScheme, + customTabColorScheme, + ).build() + + val result = customTabsIntent.intent.toSafeIntent().getColorSchemeParamsBundle()!! + val expected = SparseArray<Bundle>() + expected.put(colorScheme, createBundleFrom(customTabColorScheme)) + + result[colorScheme].assertEquals(expected[colorScheme]) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.S_V2]) + fun getColorSchemeParamsBundlePreAndroidVersionTiramisu() { + val colorScheme = CustomTabsIntent.COLOR_SCHEME_DARK + val colorSchemeParams = createColorSchemeParams() + val customTabColorScheme = createCustomTabColorSchemeParamsFrom(colorSchemeParams) + val customTabsIntent = CustomTabsIntent.Builder().setColorSchemeParams( + colorScheme, + customTabColorScheme, + ).build() + + val result = customTabsIntent.intent.toSafeIntent().getColorSchemeParamsBundle()!! + val expected = SparseArray<Bundle>() + expected.put(colorScheme, createBundleFrom(customTabColorScheme)) + + result[colorScheme].assertEquals(expected[colorScheme]) + } + + @Test + fun createFromIntentWithCloseButton() { + val size = 24 + val builder = CustomTabsIntent.Builder() + val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888) + builder.setCloseButtonIcon(closeButtonIcon) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertEquals(closeButtonIcon, customTabConfig.closeButtonIcon) + assertEquals(size, customTabConfig.closeButtonIcon?.width) + assertEquals(size, customTabConfig.closeButtonIcon?.height) + + val customTabConfigNoResources = createCustomTabConfigFromIntent(builder.build().intent, null) + assertEquals(closeButtonIcon, customTabConfigNoResources.closeButtonIcon) + assertEquals(size, customTabConfigNoResources.closeButtonIcon?.width) + assertEquals(size, customTabConfigNoResources.closeButtonIcon?.height) + } + + @Test + fun createFromIntentWithMaxOversizedCloseButton() { + val size = 64 + val builder = CustomTabsIntent.Builder() + val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888) + builder.setCloseButtonIcon(closeButtonIcon) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertNull(customTabConfig.closeButtonIcon) + + val customTabConfigNoResources = createCustomTabConfigFromIntent(builder.build().intent, null) + assertEquals(closeButtonIcon, customTabConfigNoResources.closeButtonIcon) + } + + @Test + fun createFromIntentUsingDisplayMetricsForCloseButton() { + val size = 64 + val builder = CustomTabsIntent.Builder() + val resources: Resources = mock() + val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888) + builder.setCloseButtonIcon(closeButtonIcon) + + `when`(resources.getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size)).thenReturn(64f) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, resources) + assertEquals(closeButtonIcon, customTabConfig.closeButtonIcon) + } + + @Test + fun createFromIntentWithInvalidCloseButton() { + val customTabsIntent = CustomTabsIntent.Builder().build() + // Intent is a parcelable but not a Bitmap + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_CLOSE_BUTTON_ICON, Intent()) + + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertNull(customTabConfig.closeButtonIcon) + } + + @Test + fun createFromIntentWithUrlbarHiding() { + val builder = CustomTabsIntent.Builder() + builder.setUrlBarHidingEnabled(true) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertTrue(customTabConfig.enableUrlbarHiding) + } + + @Test + fun createFromIntentWithShareMenuItem() { + val builder = CustomTabsIntent.Builder() + builder.setShareState(CustomTabsIntent.SHARE_STATE_ON) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertTrue(customTabConfig.showShareMenuItem) + } + + @Test + fun createFromIntentWithShareState() { + val builder = CustomTabsIntent.Builder() + builder.setShareState(CustomTabsIntent.SHARE_STATE_ON) + + val extraShareState = builder.build().intent.getIntExtra(CustomTabsIntent.EXTRA_SHARE_STATE, 5) + assertEquals(CustomTabsIntent.SHARE_STATE_ON, extraShareState) + } + + @Test + fun createFromIntentWithCustomizedMenu() { + val builder = CustomTabsIntent.Builder() + val pendingIntent = PendingIntent.getActivity(null, 0, null, 0) + builder.addMenuItem("menuitem1", pendingIntent) + builder.addMenuItem("menuitem2", pendingIntent) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertEquals(2, customTabConfig.menuItems.size) + assertEquals("menuitem1", customTabConfig.menuItems[0].name) + assertSame(pendingIntent, customTabConfig.menuItems[0].pendingIntent) + assertEquals("menuitem2", customTabConfig.menuItems[1].name) + assertSame(pendingIntent, customTabConfig.menuItems[1].pendingIntent) + } + + @Test + fun createFromIntentWithActionButton() { + val builder = CustomTabsIntent.Builder() + + val bitmap = mock<Bitmap>() + val intent = PendingIntent.getActivity(testContext, 0, Intent("testAction"), 0) + builder.setActionButton(bitmap, "desc", intent) + + val customTabsIntent = builder.build() + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertNotNull(customTabConfig.actionButtonConfig) + assertEquals("desc", customTabConfig.actionButtonConfig?.description) + assertEquals(intent, customTabConfig.actionButtonConfig?.pendingIntent) + assertEquals(bitmap, customTabConfig.actionButtonConfig?.icon) + assertFalse(customTabConfig.actionButtonConfig!!.tint) + } + + @Test + fun createFromIntentWithInvalidActionButton() { + val customTabsIntent = CustomTabsIntent.Builder().build() + + val invalid = Bundle() + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, invalid) + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertNull(customTabConfig.actionButtonConfig) + } + + @Test + fun createFromIntentWithInvalidExtras() { + val customTabsIntent = CustomTabsIntent.Builder().build() + + val extrasField = Intent::class.java.getDeclaredField("mExtras") + extrasField.isAccessible = true + extrasField.set(customTabsIntent.intent, null) + extrasField.isAccessible = false + + assertFalse(isCustomTabIntent(customTabsIntent.intent)) + + // Make sure we're not failing + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertNotNull(customTabConfig) + assertNull(customTabConfig.actionButtonConfig) + } + + @Test + fun createFromIntentWithExitAnimationOption() { + val customTabsIntent = CustomTabsIntent.Builder().build() + val bundle = Bundle() + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE, bundle) + + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertEquals(bundle, customTabConfig.exitAnimations) + } + + @Test + fun createFromIntentWithPageTitleOption() { + val customTabsIntent = CustomTabsIntent.Builder().build() + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.SHOW_PAGE_TITLE) + + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertTrue(customTabConfig.titleVisible) + } + + @Test + fun createFromIntentWithSessionToken() { + val customTabsIntent: Intent = mock() + val bundle: Bundle = mock() + val binder: Binder = mock() + `when`(customTabsIntent.extras).thenReturn(bundle) + `when`(bundle.getBinder(CustomTabsIntent.EXTRA_SESSION)).thenReturn(binder) + + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent, testContext.resources) + assertNotNull(customTabConfig.sessionToken) + } + + private fun createColorSchemeParams() = ColorSchemeParams( + toolbarColor = Color.BLACK, + secondaryToolbarColor = Color.RED, + navigationBarColor = Color.BLUE, + navigationBarDividerColor = Color.YELLOW, + ) + + private fun createCustomTabColorSchemeParamsFrom(colorSchemeParams: ColorSchemeParams): CustomTabColorSchemeParams { + val customTabColorSchemeBuilder = CustomTabColorSchemeParams.Builder() + customTabColorSchemeBuilder.setToolbarColor(colorSchemeParams.toolbarColor!!) + customTabColorSchemeBuilder.setSecondaryToolbarColor(colorSchemeParams.secondaryToolbarColor!!) + customTabColorSchemeBuilder.setNavigationBarColor(colorSchemeParams.navigationBarColor!!) + customTabColorSchemeBuilder.setNavigationBarDividerColor(colorSchemeParams.navigationBarDividerColor!!) + return customTabColorSchemeBuilder.build() + } + + private fun createBundleFrom(customTabColorScheme: CustomTabColorSchemeParams): Bundle { + val expectedBundle = Bundle() + expectedBundle.putInt(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, customTabColorScheme.toolbarColor!!) + expectedBundle.putInt(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR, customTabColorScheme.secondaryToolbarColor!!) + expectedBundle.putInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR, customTabColorScheme.navigationBarColor!!) + expectedBundle.putInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR, customTabColorScheme.navigationBarDividerColor!!) + return expectedBundle + } + + /** + * As Bundle does not implement Equals, assert the values individually. + */ + private fun Bundle.assertEquals(bundle: Bundle) { + assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_TOOLBAR_COLOR), getInt(CustomTabsIntent.EXTRA_TOOLBAR_COLOR)) + assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR), getInt(CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_COLOR)) + assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR), getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_COLOR)) + assertEquals(bundle.getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR), getInt(CustomTabsIntent.EXTRA_NAVIGATION_BAR_DIVIDER_COLOR)) + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt new file mode 100644 index 0000000000..521726f404 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt @@ -0,0 +1,175 @@ +/* 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.feature.customtabs + +import android.content.Intent +import android.os.Bundle +import android.provider.Browser +import androidx.browser.customtabs.CustomTabsIntent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.CustomTabListAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState.Source +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.EngineSession.LoadUrlFlags +import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.tabs.CustomTabsUseCases +import mozilla.components.support.test.any +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.middleware.CaptureActionsMiddleware +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.whenever +import mozilla.components.support.utils.toSafeIntent +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +@ExperimentalCoroutinesApi +class CustomTabIntentProcessorTest { + @Test + fun processCustomTabIntentWithDefaultHandlers() { + val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() + val store = BrowserStore(middleware = listOf(middleware)) + val useCases = SessionUseCases(store) + val customTabsUseCases = CustomTabsUseCases(store, useCases.loadUrl) + + val handler = + CustomTabIntentProcessor(customTabsUseCases.add, testContext.resources) + + val intent = mock<Intent>() + whenever(intent.action).thenReturn(Intent.ACTION_VIEW) + whenever(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)).thenReturn(true) + whenever(intent.dataString).thenReturn("http://mozilla.org") + whenever(intent.putExtra(any<String>(), any<String>())).thenReturn(intent) + + handler.process(intent) + + store.waitUntilIdle() + + var customTabId: String? = null + + middleware.assertFirstAction(CustomTabListAction.AddCustomTabAction::class) { action -> + customTabId = action.tab.id + } + + middleware.assertFirstAction(EngineAction.LoadUrlAction::class) { action -> + assertEquals(customTabId, action.tabId) + assertEquals("http://mozilla.org", action.url) + assertEquals(LoadUrlFlags.external(), action.flags) + } + + verify(intent).putExtra(eq(EXTRA_SESSION_ID), any<String>()) + + val customTab = store.state.findCustomTab(customTabId!!) + assertNotNull(customTab!!) + assertEquals("http://mozilla.org", customTab.content.url) + assertTrue(customTab.source is Source.External.CustomTab) + assertNotNull(customTab.config) + assertFalse(customTab.content.private) + } + + @Test + fun processCustomTabIntentWithAdditionalHeaders() { + val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() + val store = BrowserStore(middleware = listOf(middleware)) + val useCases = SessionUseCases(store) + val customTabsUseCases = CustomTabsUseCases(store, useCases.loadUrl) + + val handler = + CustomTabIntentProcessor(customTabsUseCases.add, testContext.resources) + + val intent = mock<Intent>() + whenever(intent.action).thenReturn(Intent.ACTION_VIEW) + whenever(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)).thenReturn(true) + whenever(intent.dataString).thenReturn("http://mozilla.org") + whenever(intent.putExtra(any<String>(), any<String>())).thenReturn(intent) + + val headersBundle = Bundle().apply { + putString("X-Extra-Header", "true") + } + whenever(intent.getBundleExtra(Browser.EXTRA_HEADERS)).thenReturn(headersBundle) + val headers = handler.getAdditionalHeaders(intent.toSafeIntent()) + + handler.process(intent) + + store.waitUntilIdle() + + var customTabId: String? = null + + middleware.assertFirstAction(CustomTabListAction.AddCustomTabAction::class) { action -> + customTabId = action.tab.id + } + + middleware.assertFirstAction(EngineAction.LoadUrlAction::class) { action -> + assertEquals(customTabId, action.tabId) + assertEquals("http://mozilla.org", action.url) + assertEquals(LoadUrlFlags.external(), action.flags) + assertEquals(headers, action.additionalHeaders) + } + + verify(intent).putExtra(eq(EXTRA_SESSION_ID), any<String>()) + + val customTab = store.state.findCustomTab(customTabId!!) + assertNotNull(customTab!!) + assertEquals("http://mozilla.org", customTab.content.url) + assertTrue(customTab.source is Source.External.CustomTab) + assertNotNull(customTab.config) + assertFalse(customTab.content.private) + } + + @Test + fun processPrivateCustomTabIntentWithDefaultHandlers() { + val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() + val store = BrowserStore(middleware = listOf(middleware)) + val useCases = SessionUseCases(store) + val customTabsUseCases = CustomTabsUseCases(store, useCases.loadUrl) + + val handler = + CustomTabIntentProcessor(customTabsUseCases.add, testContext.resources, true) + + val intent = mock<Intent>() + whenever(intent.action).thenReturn(Intent.ACTION_VIEW) + whenever(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)).thenReturn(true) + whenever(intent.dataString).thenReturn("http://mozilla.org") + whenever(intent.putExtra(any<String>(), any<String>())).thenReturn(intent) + + handler.process(intent) + + store.waitUntilIdle() + + var customTabId: String? = null + + middleware.assertFirstAction(CustomTabListAction.AddCustomTabAction::class) { action -> + customTabId = action.tab.id + } + + middleware.assertFirstAction(EngineAction.LoadUrlAction::class) { action -> + assertEquals(customTabId, action.tabId) + assertEquals("http://mozilla.org", action.url) + assertEquals(LoadUrlFlags.external(), action.flags) + } + + verify(intent).putExtra(eq(EXTRA_SESSION_ID), any<String>()) + + val customTab = store.state.findCustomTab(customTabId!!) + assertNotNull(customTab!!) + assertEquals("http://mozilla.org", customTab.content.url) + assertTrue(customTab.source is Source.External.CustomTab) + assertNotNull(customTab.config) + assertTrue(customTab.content.private) + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabWindowFeatureTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabWindowFeatureTest.kt new file mode 100644 index 0000000000..8be4a6edab --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabWindowFeatureTest.kt @@ -0,0 +1,164 @@ +/* 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.feature.customtabs + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.graphics.Color +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.ColorSchemeParams +import mozilla.components.browser.state.state.ColorSchemes +import mozilla.components.browser.state.state.CustomTabActionButtonConfig +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.CustomTabMenuItem +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.window.WindowRequest +import mozilla.components.support.test.any +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import mozilla.components.support.test.rule.MainCoroutineRule +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class CustomTabWindowFeatureTest { + + @get:Rule + val coroutinesTestRule = MainCoroutineRule() + + private lateinit var store: BrowserStore + private val sessionId = "session-uuid" + private lateinit var activity: Activity + private lateinit var engineSession: EngineSession + + @Before + fun setup() { + activity = mock() + engineSession = mock() + + store = spy( + BrowserStore( + BrowserState( + customTabs = listOf( + createCustomTab( + id = sessionId, + url = "https://www.mozilla.org", + engineSession = engineSession, + ), + ), + ), + ), + ) + + whenever(activity.packageName).thenReturn("org.mozilla.firefox") + } + + @Test + fun `given a request to open window, when the url can be handled, then the activity should start`() { + val feature = spy(CustomTabWindowFeature(activity, store, sessionId)) + val windowRequest: WindowRequest = mock() + + feature.start() + whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN) + whenever(windowRequest.url).thenReturn("https://www.firefox.com") + store.dispatch(ContentAction.UpdateWindowRequestAction(sessionId, windowRequest)).joinBlocking() + + verify(activity).startActivity(any(), any()) + verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(sessionId)) + } + + @Test + fun `given a request to open window, when the url can't be handled, then handleError should be called`() { + val exception = ActivityNotFoundException() + val feature = spy(CustomTabWindowFeature(activity, store, sessionId)) + val windowRequest: WindowRequest = mock() + + feature.start() + whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN) + whenever(windowRequest.url).thenReturn("blob:https://www.firefox.com") + whenever(activity.startActivity(any(), any())).thenThrow(exception) + store.dispatch(ContentAction.UpdateWindowRequestAction(sessionId, windowRequest)).joinBlocking() + verify(engineSession).loadUrl("blob:https://www.firefox.com") + } + + @Test + fun `creates intent based on default custom tab config`() { + val feature = CustomTabWindowFeature(activity, store, sessionId) + val config = CustomTabConfig() + val intent = feature.configToIntent(config) + + val newConfig = createCustomTabConfigFromIntent(intent.intent, null) + assertEquals("org.mozilla.firefox", intent.intent.`package`) + assertEquals(config, newConfig) + } + + @Test + fun `creates intent based on custom tab config`() { + val feature = CustomTabWindowFeature(activity, store, sessionId) + val config = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = ColorSchemeParams( + toolbarColor = Color.RED, + navigationBarColor = Color.BLUE, + ), + ), + enableUrlbarHiding = true, + showShareMenuItem = true, + titleVisible = true, + ) + val intent = feature.configToIntent(config) + + val newConfig = createCustomTabConfigFromIntent(intent.intent, null) + assertEquals("org.mozilla.firefox", intent.intent.`package`) + assertEquals(config, newConfig) + } + + @Test + fun `creates intent with same menu items`() { + val feature = CustomTabWindowFeature(activity, store, sessionId) + val config = CustomTabConfig( + actionButtonConfig = CustomTabActionButtonConfig( + description = "button", + icon = mock(), + pendingIntent = mock(), + ), + menuItems = listOf( + CustomTabMenuItem("Item A", mock()), + CustomTabMenuItem("Item B", mock()), + CustomTabMenuItem("Item C", mock()), + ), + ) + val intent = feature.configToIntent(config) + + val newConfig = createCustomTabConfigFromIntent(intent.intent, null) + assertEquals("org.mozilla.firefox", intent.intent.`package`) + assertEquals(config, newConfig) + } + + @Test + fun `handles no requests when stopped`() { + val feature = CustomTabWindowFeature(activity, store, sessionId) + feature.start() + feature.stop() + + val windowRequest: WindowRequest = mock() + whenever(windowRequest.type).thenReturn(WindowRequest.Type.OPEN) + whenever(windowRequest.url).thenReturn("https://www.firefox.com") + store.dispatch(ContentAction.UpdateWindowRequestAction(sessionId, windowRequest)).joinBlocking() + verify(activity, never()).startActivity(any(), any()) + verify(store, never()).dispatch(ContentAction.ConsumeWindowRequestAction(sessionId)) + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt new file mode 100644 index 0000000000..4a97ba985a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt @@ -0,0 +1,1582 @@ +/* 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.feature.customtabs + +import android.app.PendingIntent +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.view.ViewGroup +import android.view.Window +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.TextView +import androidx.appcompat.app.AppCompatDelegate +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.content.ContextCompat.getColor +import androidx.core.view.forEach +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import mozilla.components.browser.menu.BrowserMenu +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.CustomTabListAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.ColorSchemeParams +import mozilla.components.browser.state.state.ColorSchemes +import mozilla.components.browser.state.state.CustomTabActionButtonConfig +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.CustomTabMenuItem +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.tabs.CustomTabsUseCases +import mozilla.components.support.ktx.android.content.res.resolveAttribute +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.middleware.CaptureActionsMiddleware +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyList +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` + +@RunWith(AndroidJUnit4::class) +class CustomTabsToolbarFeatureTest { + @Test + fun `start without sessionId invokes nothing`() { + val store = BrowserStore() + val toolbar: BrowserToolbar = mock() + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = null, useCases = useCases) {}) + + feature.start() + + verify(feature, never()).init(any()) + } + + @Test + fun `start calls initialize with the sessionId`() { + val tab = createCustomTab("https://www.mozilla.org", id = "mozilla") + + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = BrowserToolbar(testContext) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature).init(tab.config) + + // Calling start again should NOT call init again + + feature.start() + + verify(feature, times(1)).init(tab.config) + } + + @Test + fun `initialize updates toolbar`() { + val tab = createCustomTab("https://www.mozilla.org", id = "mozilla") + + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = BrowserToolbar(testContext) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {} + + feature.init(tab.config) + + assertFalse(toolbar.display.onUrlClicked.invoke()) + } + + @Test + fun `initialize updates toolbar, window and text color`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = ColorSchemeParams( + toolbarColor = Color.RED, + navigationBarColor = Color.BLUE, + ), + ), + ), + ) + + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val window: Window = mock() + `when`(window.decorView).thenReturn(mock()) + val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases, window = window) {} + + feature.init(tab.config) + + verify(toolbar).setBackgroundColor(Color.RED) + verify(window).statusBarColor = Color.RED + verify(window).navigationBarColor = Color.BLUE + + assertEquals(Color.WHITE, toolbar.display.colors.title) + assertEquals(Color.WHITE, toolbar.display.colors.text) + } + + @Test + fun `initialize does not update toolbar background if flag is set`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = ColorSchemeParams(toolbarColor = Color.RED), + ), + ), + ) + + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val window: Window = mock() + `when`(window.decorView).thenReturn(mock()) + + run { + val feature = CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + window = window, + updateTheme = false, + ) {} + + feature.init(tab.config) + + verify(toolbar, never()).setBackgroundColor(Color.RED) + } + + run { + val feature = CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + window = window, + updateTheme = true, + ) {} + + feature.init(tab.config) + + verify(toolbar).setBackgroundColor(Color.RED) + } + } + + @Test + fun `adds close button`() { + val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()) + + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {} + + feature.start() + + verify(toolbar).addNavigationAction(any()) + } + + @Test + fun `doesn't add close button if the button should be hidden`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + showCloseButton = false, + ), + ) + + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {} + + feature.start() + + verify(toolbar, never()).addNavigationAction(any()) + } + + @Test + fun `close button invokes callback and removes session`() { + val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() + + val store = BrowserStore( + middleware = listOf(middleware), + initialState = BrowserState( + customTabs = listOf( + createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()), + ), + ), + ) + + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + var closeClicked = false + val feature = CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) { + closeClicked = true + } + + feature.start() + + verify(toolbar).addNavigationAction(any()) + + val button = extractActionView(toolbar, testContext.getString(R.string.mozac_feature_customtabs_exit_button)) + + middleware.assertNotDispatched(CustomTabListAction.RemoveCustomTabAction::class) + + button?.performClick() + + assertTrue(closeClicked) + + middleware.assertLastAction(CustomTabListAction.RemoveCustomTabAction::class) { action -> + assertEquals("mozilla", action.tabId) + } + } + + @Test + fun `does not add share button by default`() { + val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature, never()).addShareButton(anyInt()) + verify(toolbar, never()).addBrowserAction(any()) + } + + @Test + fun `adds share button`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + showShareMenuItem = true, + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature).addShareButton(anyInt()) + verify(toolbar).addBrowserAction(any()) + } + + @Test + fun `share button uses custom share listener`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + showShareMenuItem = true, + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + var clicked = false + val feature = CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + shareListener = { clicked = true }, + ) {} + + feature.start() + + val captor = argumentCaptor<Toolbar.ActionButton>() + verify(toolbar).addBrowserAction(captor.capture()) + + val button = captor.value.createView(FrameLayout(testContext)) + button.performClick() + assertTrue(clicked) + } + + @Test + fun `initialize calls addActionButton`() { + val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature).addActionButton(anyInt(), any()) + } + + @Test + fun `GIVEN a square icon larger than the max drawable size WHEN adding action button to toolbar THEN the icon is scaled to fit`() { + val captor = argumentCaptor<Toolbar.ActionButton>() + val size = 48 + val pendingIntent: PendingIntent = mock() + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + actionButtonConfig = CustomTabActionButtonConfig( + description = "Button", + icon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888), + pendingIntent = pendingIntent, + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature).addActionButton(anyInt(), any()) + verify(toolbar).addBrowserAction(captor.capture()) + + val button = captor.value.createView(FrameLayout(testContext)) + assertEquals(24, (button as ImageButton).drawable.intrinsicHeight) + assertEquals(24, button.drawable.intrinsicWidth) + } + + @Test + fun `GIVEN a wide icon larger than the max drawable size WHEN adding action button to toolbar THEN the icon is scaled to fit`() { + val captor = argumentCaptor<Toolbar.ActionButton>() + val width = 96 + val height = 48 + val pendingIntent: PendingIntent = mock() + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + actionButtonConfig = CustomTabActionButtonConfig( + description = "Button", + icon = Bitmap.createBitmap(IntArray(width * height), width, height, Bitmap.Config.ARGB_8888), + pendingIntent = pendingIntent, + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature).addActionButton(anyInt(), any()) + verify(toolbar).addBrowserAction(captor.capture()) + + val button = captor.value.createView(FrameLayout(testContext)) + assertEquals(24, (button as ImageButton).drawable.intrinsicHeight) + assertEquals(48, button.drawable.intrinsicWidth) + } + + @Test + fun `GIVEN a tall icon larger than the max drawable size WHEN adding action button to toolbar THEN the icon is scaled to fit`() { + val captor = argumentCaptor<Toolbar.ActionButton>() + val width = 24 + val height = 48 + val pendingIntent: PendingIntent = mock() + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + actionButtonConfig = CustomTabActionButtonConfig( + description = "Button", + icon = Bitmap.createBitmap(IntArray(width * height), width, height, Bitmap.Config.ARGB_8888), + pendingIntent = pendingIntent, + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature).addActionButton(anyInt(), any()) + verify(toolbar).addBrowserAction(captor.capture()) + + val button = captor.value.createView(FrameLayout(testContext)) + assertEquals(24, (button as ImageButton).drawable.intrinsicHeight) + assertEquals(12, button.drawable.intrinsicWidth) + } + + @Test + fun `action button uses updated url`() { + val size = 48 + val pendingIntent: PendingIntent = mock() + val captor = argumentCaptor<Toolbar.ActionButton>() + val intentCaptor = argumentCaptor<Intent>() + + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + actionButtonConfig = CustomTabActionButtonConfig( + description = "Button", + icon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888), + pendingIntent = pendingIntent, + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + store.dispatch( + ContentAction.UpdateUrlAction( + "mozilla", + "https://github.com/mozilla-mobile/android-components", + ), + ).joinBlocking() + + verify(feature).addActionButton(anyInt(), any()) + verify(toolbar).addBrowserAction(captor.capture()) + + doNothing().`when`(pendingIntent).send(any(), anyInt(), any()) + + val button = captor.value.createView(FrameLayout(testContext)) + button.performClick() + + verify(pendingIntent).send(any(), anyInt(), intentCaptor.capture()) + assertEquals("https://github.com/mozilla-mobile/android-components", intentCaptor.value.dataString) + } + + @Test + fun `initialize calls addMenuItems when config has items`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", mock()), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + verify(feature).addMenuItems(anyList(), anyInt()) + } + + @Test + fun `initialize calls addMenuItems when menuBuilder has items`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", mock()), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + ) {}, + ) + + feature.start() + + verify(feature).addMenuItems(anyList(), anyInt()) + } + + @Test + fun `menu items added WITHOUT current items`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", mock()), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + ) {}, + ) + + feature.start() + + val menuBuilder = toolbar.display.menuBuilder + assertEquals(1, menuBuilder!!.items.size) + } + + @Test + fun `menu items added WITH current items`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", mock()), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + ) {}, + ) + + feature.start() + + val menuBuilder = toolbar.display.menuBuilder + assertEquals(3, menuBuilder!!.items.size) + } + + @Test + fun `menu item added at specified index`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", mock()), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 1, + ) {}, + ) + + feature.start() + + val menuBuilder = toolbar.display.menuBuilder!! + + assertEquals(3, menuBuilder.items.size) + assertTrue(menuBuilder.items[1] is SimpleBrowserMenuItem) + } + + @Test + fun `menu item added appended if index too large`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", mock()), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) {}, + ) + + feature.start() + + val menuBuilder = toolbar.display.menuBuilder!! + + assertEquals(3, menuBuilder.items.size) + assertTrue(menuBuilder.items[2] is SimpleBrowserMenuItem) + } + + @Test + fun `menu item added appended if index too small`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", mock()), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = -4, + ) {}, + ) + + feature.start() + + val menuBuilder = toolbar.display.menuBuilder!! + + assertEquals(3, menuBuilder.items.size) + assertTrue(menuBuilder.items[0] is SimpleBrowserMenuItem) + } + + @Test + fun `menu item uses updated url`() { + val pendingIntent: PendingIntent = mock() + val intentCaptor = argumentCaptor<Intent>() + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem("Share", pendingIntent), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy(CustomTabsToolbarFeature(store, toolbar, sessionId = "mozilla", useCases = useCases) {}) + + feature.start() + + store.dispatch( + ContentAction.UpdateUrlAction( + "mozilla", + "https://github.com/mozilla-mobile/android-components", + ), + ).joinBlocking() + + val menuBuilder = toolbar.display.menuBuilder!! + + val item = menuBuilder.items[0] + + val menu: BrowserMenu = mock() + val view = TextView(testContext) + + item.bind(menu, view) + + view.performClick() + + doNothing().`when`(pendingIntent).send(any(), anyInt(), any()) + + verify(pendingIntent).send(any(), anyInt(), intentCaptor.capture()) + assertEquals("https://github.com/mozilla-mobile/android-components", intentCaptor.value.dataString) + } + + @Test + fun `onBackPressed removes initialized session`() { + val store = BrowserStore( + initialState = BrowserState( + customTabs = listOf( + createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()), + ), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + var closeExecuted = false + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) { + closeExecuted = true + }, + ) + + feature.start() + + val result = feature.onBackPressed() + + assertTrue(result) + assertTrue(closeExecuted) + } + + @Test + fun `onBackPressed without a session does nothing`() { + val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + var closeExecuted = false + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = null, + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) { + closeExecuted = true + }, + ) + + feature.start() + + val result = feature.onBackPressed() + + assertFalse(result) + assertFalse(closeExecuted) + } + + @Test + fun `onBackPressed with uninitialized feature returns false`() { + val tab = createCustomTab("https://www.mozilla.org", id = "mozilla", config = CustomTabConfig()) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + var closeExecuted = false + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = null, + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) { + closeExecuted = true + }, + ) + + val result = feature.onBackPressed() + + assertFalse(result) + assertFalse(closeExecuted) + } + + @Test + fun `WHEN config toolbar color is dark THEN readableColor is white`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = ColorSchemeParams(toolbarColor = Color.BLACK), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) {}, + ) + + feature.start() + + verify(feature).updateTheme( + tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor, + tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor, + tab.config.colorSchemes!!.defaultColorSchemeParams!!.navigationBarDividerColor, + Color.WHITE, + ) + verify(feature).addCloseButton(Color.WHITE, tab.config.closeButtonIcon) + verify(feature).addActionButton(Color.WHITE, tab.config.actionButtonConfig) + assertEquals(Color.WHITE, toolbar.display.colors.text) + } + + @Test + fun `WHEN config toolbar color is not dark THEN readableColor is black`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = ColorSchemeParams(toolbarColor = Color.WHITE), + ), + ), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) {}, + ) + + feature.start() + + verify(feature).updateTheme( + tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor, + tab.config.colorSchemes!!.defaultColorSchemeParams!!.toolbarColor, + tab.config.colorSchemes!!.defaultColorSchemeParams!!.navigationBarDividerColor, + Color.BLACK, + ) + verify(feature).addCloseButton(Color.BLACK, tab.config.closeButtonIcon) + verify(feature).addActionButton(Color.BLACK, tab.config.actionButtonConfig) + } + + @Test + fun `WHEN config toolbar has no colour set THEN readableColor uses the toolbar display menu colour`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig(), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) {}, + ) + + feature.start() + + verify(feature).updateTheme( + tab.config.colorSchemes?.defaultColorSchemeParams?.toolbarColor, + tab.config.colorSchemes?.defaultColorSchemeParams?.toolbarColor, + tab.config.colorSchemes?.defaultColorSchemeParams?.navigationBarDividerColor, + toolbar.display.colors.menu, + ) + verify(feature).addCloseButton(toolbar.display.colors.menu, tab.config.closeButtonIcon) + verify(feature).addActionButton(toolbar.display.colors.menu, tab.config.actionButtonConfig) + assertEquals(Color.WHITE, toolbar.display.colors.menu) + } + + @Test + fun `WHEN tab is private THEN readableColor is the default private color`() { + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig(showShareMenuItem = true), + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store = store, + toolbar = toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + updateTheme = false, + ) {}, + ) + + feature.start() + + val colorResId = testContext.theme.resolveAttribute(android.R.attr.textColorPrimary) + val privateColor = getColor(testContext, colorResId) + verify(feature).addCloseButton(privateColor, tab.config.closeButtonIcon) + verify(feature).addActionButton(privateColor, tab.config.actionButtonConfig) + verify(feature).addShareButton(privateColor) + } + + @Test + fun `WHEN COLOR_SCHEME_SYSTEM THEN toNightMode returns MODE_NIGHT_FOLLOW_SYSTEM`() { + assertEquals(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, CustomTabsIntent.COLOR_SCHEME_SYSTEM.toNightMode()) + } + + @Test + fun `WHEN COLOR_SCHEME_LIGHT THEN toNightMode returns MODE_NIGHT_NO`() { + assertEquals(AppCompatDelegate.MODE_NIGHT_NO, CustomTabsIntent.COLOR_SCHEME_LIGHT.toNightMode()) + } + + @Test + fun `WHEN COLOR_SCHEME_DARK THEN toNightMode returns MODE_NIGHT_YES`() { + assertEquals(AppCompatDelegate.MODE_NIGHT_YES, CustomTabsIntent.COLOR_SCHEME_DARK.toNightMode()) + } + + @Test + fun `WHEN unknown color scheme THEN toNightMode returns null`() { + assertEquals(null, 100.toNightMode()) + } + + @Test + fun `WHEN no color scheme params set THEN getConfiguredColorSchemeParams returns null `() { + val customTabConfig = CustomTabConfig() + assertEquals(null, customTabConfig.colorSchemes?.getConfiguredColorSchemeParams()) + } + + @Test + fun `WHEN only default color scheme params set THEN getConfiguredColorSchemeParams returns default `() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + ), + ) + + assertEquals( + defaultColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(), + ) + } + + @Test + fun `WHEN night mode follow system and is light mode THEN getConfiguredColorSchemeParams returns light color scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ), + ) + + assertEquals( + lightColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + ), + ) + } + + @Test + fun `WHEN night mode follow system, is light mode no light color scheme THEN getConfiguredColorSchemeParams returns default scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ), + ) + + assertEquals( + defaultColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + ), + ) + } + + @Test + fun `WHEN night mode follow system and is dark mode THEN getConfiguredColorSchemeParams returns dark color scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ), + ) + + assertEquals( + darkColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + isDarkMode = true, + ), + ) + } + + @Test + fun `WHEN night mode follow system, is dark mode no dark color scheme THEN getConfiguredColorSchemeParams returns default scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + ), + ) + + assertEquals( + defaultColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + isDarkMode = true, + ), + ) + } + + @Test + fun `WHEN night mode no THEN getConfiguredColorSchemeParams returns light color scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ), + ) + + assertEquals( + lightColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_NO, + ), + ) + } + + @Test + fun `WHEN night mode no & no light color params THEN getConfiguredColorSchemeParams returns default color scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ), + ) + + assertEquals( + defaultColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_NO, + ), + ) + } + + @Test + fun `WHEN night mode yes THEN getConfiguredColorSchemeParams returns dark color scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ), + ) + + assertEquals( + darkColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_YES, + ), + ) + } + + @Test + fun `WHEN night mode yes & no dark color params THEN getConfiguredColorSchemeParams returns default color scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + ), + ) + + assertEquals( + defaultColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams( + nightMode = AppCompatDelegate.MODE_NIGHT_YES, + ), + ) + } + + @Test + fun `WHEN night mode not set THEN getConfiguredColorSchemeParams returns default color scheme`() { + val customTabConfig = CustomTabConfig( + colorSchemes = ColorSchemes( + defaultColorSchemeParams = defaultColorSchemeParams, + lightColorSchemeParams = lightColorSchemeParams, + darkColorSchemeParams = darkColorSchemeParams, + ), + ) + + assertEquals( + defaultColorSchemeParams, + customTabConfig.colorSchemes!!.getConfiguredColorSchemeParams(), + ) + } + + @Test + fun `WHEN ColorSchemeParams has all properties THEN withDefault returns the same ColorSchemeParams`() { + val result = lightColorSchemeParams.withDefault(defaultColorSchemeParams) + + assertEquals(lightColorSchemeParams, result) + } + + @Test + fun `WHEN ColorSchemeParams has some properties THEN withDefault uses default for the missing properties`() { + val colorSchemeParams = ColorSchemeParams( + toolbarColor = Color.BLACK, + navigationBarDividerColor = Color.YELLOW, + ) + + val expected = ColorSchemeParams( + toolbarColor = colorSchemeParams.toolbarColor, + secondaryToolbarColor = defaultColorSchemeParams.secondaryToolbarColor, + navigationBarColor = defaultColorSchemeParams.navigationBarColor, + navigationBarDividerColor = colorSchemeParams.navigationBarDividerColor, + ) + + val result = colorSchemeParams.withDefault(defaultColorSchemeParams) + + assertEquals(expected, result) + } + + @Test + fun `WHEN ColorSchemeParams has no properties THEN withDefault returns all default ColorSchemeParams`() { + val result = ColorSchemeParams().withDefault(defaultColorSchemeParams) + + assertEquals(defaultColorSchemeParams, result) + } + + @Test + fun `show title only if not empty`() { + val dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) + + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig(), + title = "", + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) {}, + ) + + feature.start() + + assertEquals("", toolbar.title) + + store.dispatch( + ContentAction.UpdateTitleAction( + "mozilla", + "Internet for people, not profit - Mozilla", + ), + ).joinBlocking() + + assertEquals("Internet for people, not profit - Mozilla", toolbar.title) + + Dispatchers.resetMain() + } + + @Test + fun `Will use URL as title if title was shown once and is now empty`() { + val dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) + + val tab = createCustomTab( + "https://www.mozilla.org", + id = "mozilla", + config = CustomTabConfig(), + title = "", + ) + val store = BrowserStore( + BrowserState( + customTabs = listOf(tab), + ), + ) + val toolbar = spy(BrowserToolbar(testContext)) + val useCases = CustomTabsUseCases( + store = store, + loadUrlUseCase = SessionUseCases(store).loadUrl, + ) + val feature = spy( + CustomTabsToolbarFeature( + store, + toolbar, + sessionId = "mozilla", + useCases = useCases, + menuBuilder = BrowserMenuBuilder(listOf(mock(), mock())), + menuItemIndex = 4, + ) {}, + ) + + feature.start() + + feature.start() + + assertEquals("", toolbar.title) + + store.dispatch( + ContentAction.UpdateUrlAction("mozilla", "https://www.mozilla.org/en-US/firefox/"), + ).joinBlocking() + + assertEquals("", toolbar.title) + + store.dispatch( + ContentAction.UpdateTitleAction( + "mozilla", + "Firefox - Protect your life online with privacy-first products", + ), + ).joinBlocking() + + assertEquals("Firefox - Protect your life online with privacy-first products", toolbar.title) + + store.dispatch( + ContentAction.UpdateUrlAction("mozilla", "https://github.com/mozilla-mobile/android-components"), + ).joinBlocking() + + assertEquals("https://github.com/mozilla-mobile/android-components", toolbar.title) + + store.dispatch( + ContentAction.UpdateTitleAction("mozilla", "Le GitHub"), + ).joinBlocking() + + assertEquals("Le GitHub", toolbar.title) + + store.dispatch( + ContentAction.UpdateUrlAction("mozilla", "https://github.com/mozilla-mobile/fenix"), + ).joinBlocking() + + assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title) + + store.dispatch( + ContentAction.UpdateTitleAction("mozilla", ""), + ).joinBlocking() + + assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title) + + store.dispatch( + ContentAction.UpdateTitleAction( + "mozilla", + "A collection of Android libraries to build browsers or browser-like applications.", + ), + ).joinBlocking() + + assertEquals("A collection of Android libraries to build browsers or browser-like applications.", toolbar.title) + + store.dispatch( + ContentAction.UpdateTitleAction("mozilla", ""), + ).joinBlocking() + + assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title) + } + + private fun extractActionView( + browserToolbar: BrowserToolbar, + contentDescription: String, + ): ImageButton? { + var actionView: ImageButton? = null + + browserToolbar.forEach { group -> + val viewGroup = group as ViewGroup + + viewGroup.forEach inner@{ subGroup -> + if (subGroup is ViewGroup) { + subGroup.forEach { + if (it is ImageButton && it.contentDescription == contentDescription) { + actionView = it + return@inner + } + } + } + } + } + + return actionView + } + + private val defaultColorSchemeParams = ColorSchemeParams( + toolbarColor = Color.CYAN, + secondaryToolbarColor = Color.GREEN, + navigationBarColor = Color.WHITE, + navigationBarDividerColor = Color.MAGENTA, + ) + + private val lightColorSchemeParams = ColorSchemeParams( + toolbarColor = Color.BLACK, + secondaryToolbarColor = Color.RED, + navigationBarColor = Color.BLUE, + navigationBarDividerColor = Color.YELLOW, + ) + + private val darkColorSchemeParams = ColorSchemeParams( + toolbarColor = Color.DKGRAY, + secondaryToolbarColor = Color.LTGRAY, + navigationBarColor = Color.GRAY, + navigationBarDividerColor = Color.WHITE, + ) +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt new file mode 100644 index 0000000000..c918442593 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt @@ -0,0 +1,113 @@ +/* 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.feature.customtabs.feature + +import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.concept.toolbar.AutocompleteDelegate +import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.support.test.ThrowProperty +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +class CustomTabSessionTitleObserverTest { + + @Test + fun `show title only if not empty`() { + val toolbar: Toolbar = mock() + val observer = CustomTabSessionTitleObserver(toolbar) + val url = "https://www.mozilla.org" + val title = "Internet for people, not profit - Mozilla" + + observer.onTab(createCustomTab(url, title = "")) + verify(toolbar, never()).title = "" + + observer.onTab(createCustomTab(url, title = title)) + verify(toolbar).title = title + } + + @Test + fun `Will use URL as title if title was shown once and is now empty`() { + val toolbar = MockToolbar() + var tab = createCustomTab("https://mozilla.org") + val observer = CustomTabSessionTitleObserver(toolbar) + + observer.onTab(tab) + assertEquals("", toolbar.title) + + tab = tab.withUrl("https://www.mozilla.org/en-US/firefox/") + observer.onTab(tab) + assertEquals("", toolbar.title) + + tab = tab.withTitle("Firefox - Protect your life online with privacy-first products") + observer.onTab(tab) + assertEquals("Firefox - Protect your life online with privacy-first products", toolbar.title) + + tab = tab.withUrl("https://github.com/mozilla-mobile/android-components") + observer.onTab(tab) + assertEquals("Firefox - Protect your life online with privacy-first products", toolbar.title) + + tab = tab.withTitle("") + observer.onTab(tab) + assertEquals("https://github.com/mozilla-mobile/android-components", toolbar.title) + + tab = tab.withTitle("A collection of Android libraries to build browsers or browser-like applications.") + observer.onTab(tab) + assertEquals("A collection of Android libraries to build browsers or browser-like applications.", toolbar.title) + + tab = tab.withTitle("") + observer.onTab(tab) + assertEquals("https://github.com/mozilla-mobile/android-components", toolbar.title) + } + + private class MockToolbar : Toolbar { + override var title: String = "" + override var highlight: Toolbar.Highlight = Toolbar.Highlight.NONE + override var url: CharSequence by ThrowProperty() + override var private: Boolean by ThrowProperty() + override var siteSecure: Toolbar.SiteSecurity by ThrowProperty() + override var siteTrackingProtection: Toolbar.SiteTrackingProtection by ThrowProperty() + override fun setSearchTerms(searchTerms: String) = Unit + override fun displayProgress(progress: Int) = Unit + override fun onBackPressed(): Boolean = false + override fun onStop() = Unit + override fun setOnUrlCommitListener(listener: (String) -> Boolean) = Unit + override fun setAutocompleteListener(filter: suspend (String, AutocompleteDelegate) -> Unit) = Unit + override fun addBrowserAction(action: Toolbar.Action) = Unit + override fun removeBrowserAction(action: Toolbar.Action) = Unit + override fun invalidateActions() = Unit + override fun addPageAction(action: Toolbar.Action) = Unit + override fun removePageAction(action: Toolbar.Action) = Unit + override fun addNavigationAction(action: Toolbar.Action) = Unit + override fun removeNavigationAction(action: Toolbar.Action) = Unit + override fun addEditActionStart(action: Toolbar.Action) = Unit + override fun addEditActionEnd(action: Toolbar.Action) = Unit + override fun removeEditActionEnd(action: Toolbar.Action) = Unit + override fun hideMenuButton() = Unit + override fun showMenuButton() = Unit + override fun setDisplayHorizontalPadding(horizontalPadding: Int) = Unit + override fun hidePageActionSeparator() = Unit + override fun showPageActionSeparator() = Unit + override fun setOnEditListener(listener: Toolbar.OnEditListener) = Unit + override fun displayMode() = Unit + override fun editMode(cursorPlacement: Toolbar.CursorPlacement) = Unit + override fun dismissMenu() = Unit + override fun enableScrolling() = Unit + override fun disableScrolling() = Unit + override fun collapse() = Unit + override fun expand() = Unit + } +} + +private fun CustomTabSessionState.withTitle(title: String) = copy( + content = content.copy(title = title), +) + +private fun CustomTabSessionState.withUrl(url: String) = copy( + content = content.copy(url = url), +) diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt new file mode 100644 index 0000000000..e035349879 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt @@ -0,0 +1,82 @@ +/* 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.feature.customtabs.feature + +import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS +import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN +import androidx.browser.customtabs.CustomTabsSessionToken +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import mozilla.components.feature.customtabs.store.CustomTabState +import mozilla.components.feature.customtabs.store.CustomTabsServiceStore +import mozilla.components.feature.customtabs.store.OriginRelationPair +import mozilla.components.feature.customtabs.store.ValidateRelationshipAction +import mozilla.components.feature.customtabs.store.VerificationStatus.FAILURE +import mozilla.components.feature.customtabs.store.VerificationStatus.PENDING +import mozilla.components.feature.customtabs.store.VerificationStatus.SUCCESS +import mozilla.components.feature.customtabs.verify.OriginVerifier +import mozilla.components.support.test.any +import mozilla.components.support.test.mock +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +@ExperimentalCoroutinesApi +class OriginVerifierFeatureTest { + + @Test + fun `verify fails if no creatorPackageName is saved`() = runTest { + val feature = OriginVerifierFeature(mock(), mock(), mock()) + + assertFalse(feature.verify(CustomTabState(), mock(), RELATION_HANDLE_ALL_URLS, mock())) + } + + @Test + fun `verify returns existing relationship`() = runTest { + val feature = OriginVerifierFeature(mock(), mock(), mock()) + val origin = "https://example.com".toUri() + val state = CustomTabState( + creatorPackageName = "com.example.twa", + relationships = mapOf( + OriginRelationPair(origin, RELATION_HANDLE_ALL_URLS) to SUCCESS, + OriginRelationPair(origin, RELATION_USE_AS_ORIGIN) to FAILURE, + OriginRelationPair("https://sample.com".toUri(), RELATION_HANDLE_ALL_URLS) to PENDING, + ), + ) + + assertTrue(feature.verify(state, mock(), RELATION_HANDLE_ALL_URLS, origin)) + assertFalse(feature.verify(state, mock(), RELATION_USE_AS_ORIGIN, origin)) + } + + @Test + fun `verify checks new relationships`() = runTest { + val store: CustomTabsServiceStore = mock() + val verifier: OriginVerifier = mock() + val feature = spy(OriginVerifierFeature(mock(), mock()) { store.dispatch(it) }) + doReturn(verifier).`when`(feature).getVerifier(anyString(), anyInt()) + doReturn(true).`when`(verifier).verifyOrigin(any()) + + val token: CustomTabsSessionToken = mock() + val origin = "https://sample.com".toUri() + val state = CustomTabState(creatorPackageName = "com.example.twa") + assertNotNull(state) + + assertTrue(feature.verify(state, token, RELATION_HANDLE_ALL_URLS, origin)) + + verify(verifier).verifyOrigin(origin) + verify(store).dispatch(ValidateRelationshipAction(token, RELATION_HANDLE_ALL_URLS, origin, PENDING)) + verify(store).dispatch(ValidateRelationshipAction(token, RELATION_HANDLE_ALL_URLS, origin, SUCCESS)) + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidatesTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidatesTest.kt new file mode 100644 index 0000000000..1f2e42ec1a --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/menu/CustomTabMenuCandidatesTest.kt @@ -0,0 +1,77 @@ +/* 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.feature.customtabs.menu + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.CustomTabMenuItem +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.concept.menu.candidate.MenuCandidate +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class CustomTabMenuCandidatesTest { + + @Test + fun `return an empty list if there are no menu items`() { + val customTabSessionState = createCustomTab( + url = "https://mozilla.org", + config = CustomTabConfig(menuItems = emptyList()), + ) + + assertEquals( + emptyList<MenuCandidate>(), + customTabSessionState.createCustomTabMenuCandidates(mock()), + ) + } + + @Test + fun `create a candidate for each menu item`() { + val pendingIntent1 = mock<PendingIntent>() + val pendingIntent2 = mock<PendingIntent>() + val customTabSessionState = createCustomTab( + url = "https://mozilla.org", + config = CustomTabConfig( + menuItems = listOf( + CustomTabMenuItem( + name = "item1", + pendingIntent = pendingIntent1, + ), + CustomTabMenuItem( + name = "item2", + pendingIntent = pendingIntent2, + ), + ), + ), + ) + + val context = mock<Context>() + val intent = argumentCaptor<Intent>() + val menuCandidates = customTabSessionState.createCustomTabMenuCandidates(context) + + assertEquals(2, menuCandidates.size) + assertEquals("item1", menuCandidates[0].text) + assertEquals("item2", menuCandidates[1].text) + + menuCandidates[0].onClick() + verify(pendingIntent1).send(eq(context), anyInt(), intent.capture()) + assertEquals("https://mozilla.org".toUri(), intent.value.data) + + menuCandidates[1].onClick() + verify(pendingIntent2).send(eq(context), anyInt(), intent.capture()) + assertEquals("https://mozilla.org".toUri(), intent.value.data) + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducerTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducerTest.kt new file mode 100644 index 0000000000..bfee3db851 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/store/CustomTabsServiceStateReducerTest.kt @@ -0,0 +1,174 @@ +/* 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.feature.customtabs.store + +import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS +import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN +import androidx.browser.customtabs.CustomTabsSessionToken +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CustomTabsServiceStateReducerTest { + + @Test + fun `reduce adds new tab to map`() { + val token: CustomTabsSessionToken = mock() + val initialState = CustomTabsServiceState() + val action = SaveCreatorPackageNameAction(token, "com.example.twa") + + assertEquals( + CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState(creatorPackageName = "com.example.twa"), + ), + ), + CustomTabsServiceStateReducer.reduce(initialState, action), + ) + } + + @Test + fun `reduce replaces existing tab in map`() { + val token: CustomTabsSessionToken = mock() + val initialState = CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState(creatorPackageName = "com.example.twa"), + ), + ) + val action = SaveCreatorPackageNameAction(token, "com.example.trusted.web.app") + + assertEquals( + CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState(creatorPackageName = "com.example.trusted.web.app"), + ), + ), + CustomTabsServiceStateReducer.reduce(initialState, action), + ) + } + + @Test + fun `reduce adds new relationship`() { + val token: CustomTabsSessionToken = mock() + val initialState = CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState(creatorPackageName = "com.example.twa"), + ), + ) + val action = ValidateRelationshipAction( + token, + RELATION_HANDLE_ALL_URLS, + "https://example.com".toUri(), + VerificationStatus.PENDING, + ) + + assertEquals( + CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState( + creatorPackageName = "com.example.twa", + relationships = mapOf( + Pair( + OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS), + VerificationStatus.PENDING, + ), + ), + ), + ), + ), + CustomTabsServiceStateReducer.reduce(initialState, action), + ) + } + + @Test + fun `reduce adds new relationship of different type`() { + val token: CustomTabsSessionToken = mock() + val initialState = CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState( + creatorPackageName = "com.example.twa", + relationships = mapOf( + Pair( + OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS), + VerificationStatus.FAILURE, + ), + ), + ), + ), + ) + val action = ValidateRelationshipAction( + token, + RELATION_USE_AS_ORIGIN, + "https://example.com".toUri(), + VerificationStatus.PENDING, + ) + + assertEquals( + CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState( + creatorPackageName = "com.example.twa", + relationships = mapOf( + Pair( + OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS), + VerificationStatus.FAILURE, + ), + Pair( + OriginRelationPair("https://example.com".toUri(), RELATION_USE_AS_ORIGIN), + VerificationStatus.PENDING, + ), + ), + ), + ), + ), + CustomTabsServiceStateReducer.reduce(initialState, action), + ) + } + + @Test + fun `reduce replaces existing relationship`() { + val token: CustomTabsSessionToken = mock() + val initialState = CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState( + creatorPackageName = "com.example.twa", + relationships = mapOf( + Pair( + OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS), + VerificationStatus.PENDING, + ), + ), + ), + ), + ) + val action = ValidateRelationshipAction( + token, + RELATION_HANDLE_ALL_URLS, + "https://example.com".toUri(), + VerificationStatus.SUCCESS, + ) + + assertEquals( + CustomTabsServiceState( + tabs = mapOf( + token to CustomTabState( + creatorPackageName = "com.example.twa", + relationships = mapOf( + Pair( + OriginRelationPair("https://example.com".toUri(), RELATION_HANDLE_ALL_URLS), + VerificationStatus.SUCCESS, + ), + ), + ), + ), + ), + CustomTabsServiceStateReducer.reduce(initialState, action), + ) + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt new file mode 100644 index 0000000000..5e6e384af4 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt @@ -0,0 +1,85 @@ +/* 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.feature.customtabs.verify + +import android.content.pm.PackageManager +import androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS +import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import mozilla.components.concept.fetch.Response +import mozilla.components.service.digitalassetlinks.AssetDescriptor +import mozilla.components.service.digitalassetlinks.Relation +import mozilla.components.service.digitalassetlinks.RelationChecker +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy +import org.mockito.MockitoAnnotations.openMocks + +@RunWith(AndroidJUnit4::class) +@ExperimentalCoroutinesApi +class OriginVerifierTest { + + private val androidAsset = AssetDescriptor.Android( + packageName = "com.app.name", + sha256CertFingerprint = "AA:BB:CC:10:20:30:01:02", + ) + + @Mock private lateinit var packageManager: PackageManager + + @Mock private lateinit var response: Response + + @Mock private lateinit var body: Response.Body + + @Mock private lateinit var checker: RelationChecker + + @Suppress("Deprecation") + @Before + fun setup() { + openMocks(this) + + doReturn(body).`when`(response).body + doReturn(200).`when`(response).status + doReturn("{\"linked\":true}").`when`(body).string() + } + + @Test + fun `only HTTPS allowed`() = runTest { + val verifier = buildVerifier(RELATION_HANDLE_ALL_URLS) + assertFalse(verifier.verifyOrigin("LOL".toUri())) + assertFalse(verifier.verifyOrigin("http://www.android.com".toUri())) + } + + @Test + fun verifyOrigin() = runTest { + val verifier = buildVerifier(RELATION_USE_AS_ORIGIN) + doReturn(true).`when`(checker).checkRelationship( + AssetDescriptor.Web("https://www.example.com"), + Relation.USE_AS_ORIGIN, + androidAsset, + ) + assertTrue(verifier.verifyOrigin("https://www.example.com".toUri())) + } + + private fun buildVerifier(relation: Int): OriginVerifier { + val verifier = spy( + OriginVerifier( + "com.app.name", + relation, + packageManager, + checker, + ), + ) + doReturn(androidAsset).`when`(verifier).androidAsset + return verifier + } +} diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/customtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..49324d83c5 --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,3 @@ +mock-maker-inline +// This allows mocking final classes (classes are final by default in Kotlin) + diff --git a/mobile/android/android-components/components/feature/customtabs/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/customtabs/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..932b01b9eb --- /dev/null +++ b/mobile/android/android-components/components/feature/customtabs/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 |