summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/compose/cfr
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/compose/cfr')
-rw-r--r--mobile/android/android-components/components/compose/cfr/README.md49
-rw-r--r--mobile/android/android-components/components/compose/cfr/build.gradle63
-rw-r--r--mobile/android/android-components/components/compose/cfr/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt195
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt186
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt537
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt272
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt65
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt19
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml5
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml8
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt561
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt136
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt32
-rw-r--r--mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
102 files changed, 2585 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/compose/cfr/README.md b/mobile/android/android-components/components/compose/cfr/README.md
new file mode 100644
index 0000000000..843ac13f7f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/README.md
@@ -0,0 +1,49 @@
+# [Android Components](../../../README.md) > Compose > Tabs tray
+
+A standard Contextual Feature Recommendation popup using Jetpack Compose.
+
+## Usage
+
+```kotlin
+CFRPopup(
+ anchor = <View>,
+ properties = CFRPopupProperties(
+ popupWidth = 256.dp,
+ popupAlignment = INDICATOR_CENTERED_IN_ANCHOR,
+ popupBodyColors = listOf(
+ ContextCompat.getColor(context, R.color.color1),
+ ContextCompat.getColor(context, R.color.color2)
+ ),
+ dismissButtonColor = ContextCompat.getColor(context, R.color.color3),
+ ),
+ onDismiss = { <method call> },
+ text = {
+ Text(
+ text = stringResource(R.string.string1),
+ style = MaterialTheme.typography.body2,
+ )
+ },
+ action = {
+ Button(onClick = { <method call> }) {
+ Text(text = stringResource(R.string.string2))
+ }
+ },
+).apply {
+ show()
+}
+```
+
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
+
+```Groovy
+implementation "org.mozilla.components:compose-cfr:{latest-version}"
+```
+
+## License
+
+ 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/
diff --git a/mobile/android/android-components/components/compose/cfr/build.gradle b/mobile/android/android-components/components/compose/cfr/build.gradle
new file mode 100644
index 0000000000..84e722924e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/build.gradle
@@ -0,0 +1,63 @@
+/* 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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ kotlinOptions {
+ freeCompilerArgs += "-Xjvm-default=all"
+ }
+
+ namespace 'mozilla.components.compose.cfr'
+}
+
+dependencies {
+ implementation platform(ComponentsDependencies.androidx_compose_bom)
+ implementation project(':support-ktx')
+ implementation project(':ui-icons')
+
+ implementation ComponentsDependencies.androidx_compose_ui
+ implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
+ implementation ComponentsDependencies.androidx_compose_foundation
+ implementation ComponentsDependencies.androidx_compose_material
+ implementation ComponentsDependencies.androidx_core
+ implementation ComponentsDependencies.androidx_core_ktx
+ implementation ComponentsDependencies.androidx_lifecycle_runtime
+ implementation ComponentsDependencies.androidx_savedstate
+
+ debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.testing_coroutines
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+}
+
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/compose/cfr/proguard-rules.pro b/mobile/android/android-components/components/compose/cfr/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/AndroidManifest.xml b/mobile/android/android-components/components/compose/cfr/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..e16cda1d34
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/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/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt
new file mode 100644
index 0000000000..7d18e795b3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt
@@ -0,0 +1,195 @@
+/* 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.compose.cfr
+
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment
+import java.lang.ref.WeakReference
+
+/**
+ * Properties used to customize the behavior of a [CFRPopup].
+ *
+ * @property popupWidth Width of the popup. Defaults to [CFRPopup.DEFAULT_WIDTH]. To be used as maximum
+ * width when alignment is set to [PopupAlignment.BODY_CENTERED_IN_SCREEN].
+ * @property popupAlignment Where in relation to it's anchor should the popup be placed.
+ * @property popupBodyColors One or more colors serving as the popup background.
+ * If more colors are provided they will be used in a gradient.
+ * @property popupVerticalOffset Vertical distance between the indicator arrow and the anchor.
+ * This only applies if [overlapAnchor] is `false`.
+ * @property dismissButtonColor The tint color that should be applied to the dismiss button.
+ * @property dismissOnBackPress Whether the popup can be dismissed by pressing the back button.
+ * If true, pressing the back button will also call onDismiss().
+ * @property dismissOnClickOutside Whether the popup can be dismissed by clicking outside the
+ * popup's bounds. If true, clicking outside the popup will call onDismiss().
+ * @property overlapAnchor How the popup's indicator will be shown in relation to the anchor:
+ * - true - indicator will be shown exactly in the middle horizontally and vertically
+ * - false - indicator will be shown horizontally in the middle of the anchor but immediately below or above it
+ * @property indicatorDirection The direction the indicator arrow is pointing.
+ * @property indicatorArrowStartOffset Maximum distance between the popup start and the indicator arrow.
+ * If there isn't enough space this could automatically be overridden up to 0 such that
+ * the indicator arrow will be pointing to the middle of the anchor.
+ */
+data class CFRPopupProperties(
+ val popupWidth: Dp = CFRPopup.DEFAULT_WIDTH.dp,
+ val popupAlignment: PopupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ val popupBodyColors: List<Int> = listOf(Color.Blue.toArgb()),
+ val popupVerticalOffset: Dp = CFRPopup.DEFAULT_VERTICAL_OFFSET.dp,
+ val showDismissButton: Boolean = true,
+ val dismissButtonColor: Int = Color.Black.toArgb(),
+ val dismissOnBackPress: Boolean = false,
+ val dismissOnClickOutside: Boolean = false,
+ val overlapAnchor: Boolean = false,
+ val indicatorDirection: IndicatorDirection = IndicatorDirection.UP,
+ val indicatorArrowStartOffset: Dp = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
+)
+
+/**
+ * CFR - Contextual Feature Recommendation popup.
+ *
+ * @param anchor [View] that will serve as the anchor of the popup and serve as lifecycle owner
+ * for this popup also.
+ * @param properties [CFRPopupProperties] allowing to customize the popup appearance and behavior.
+ * @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
+ * was explicit - by tapping the "X" button or not.
+ * @param text [Text] already styled and ready to be shown in the popup.
+ * @param action Optional other composable to show just below the popup text.
+ */
+class CFRPopup(
+ @get:VisibleForTesting internal val anchor: View,
+ @get:VisibleForTesting internal val properties: CFRPopupProperties,
+ @get:VisibleForTesting internal val onDismiss: (Boolean) -> Unit = {},
+ @get:VisibleForTesting internal val text: @Composable (() -> Unit),
+ @get:VisibleForTesting internal val action: @Composable (() -> Unit) = {},
+) {
+ // This is just a facade for the CFRPopupFullScreenLayout composable offering a cleaner API.
+
+ @VisibleForTesting
+ internal var popup: WeakReference<CFRPopupFullscreenLayout>? = null
+
+ /**
+ * Construct and display a styled CFR popup shown at the coordinates of [anchor].
+ * This popup will be dismissed when the user clicks on the "x" button or based on other user actions
+ * with such behavior set in [CFRPopupProperties].
+ */
+ fun show() {
+ anchor.post {
+ // When we're in this Runnable, the 'show' method might have been called right before
+ // the activity is no longer attached to the WindowManager. When we get to calling
+ // the CFRPopupFullscreenLayout#show method below, we are now trying to attach the View
+ // with the WindowManager that has an unusable Activity.
+ //
+ // To protect against this, within this same Runnable, we check if the anchor view is
+ // safe to use before continuing.
+ //
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1799996
+ if (anchor.context == null || !anchor.isAttachedToWindow) {
+ return@post
+ }
+
+ CFRPopupFullscreenLayout(anchor, properties, onDismiss, text, action).apply {
+ this.show()
+ popup = WeakReference(this)
+ }
+ }
+ }
+
+ /**
+ * Immediately dismiss this CFR popup.
+ * The [onDismiss] callback won't be fired.
+ */
+ fun dismiss() {
+ popup?.get()?.dismiss()
+ }
+
+ /**
+ * Possible direction for the arrow indicator of a CFR popup.
+ * The direction is expressed in relation with the popup body containing the text.
+ */
+ enum class IndicatorDirection {
+ UP,
+ DOWN,
+ }
+
+ /**
+ * Possible alignments of the popup in relation to it's anchor.
+ */
+ enum class PopupAlignment {
+ /**
+ * The popup body will be centered in the space occupied by the anchor.
+ * Recommended to be used when the anchor is wider than the popup.
+ */
+ BODY_TO_ANCHOR_CENTER,
+
+ /**
+ * The popup body will be shown aligned to exactly the anchor start.
+ */
+ BODY_TO_ANCHOR_START,
+
+ /**
+ * The popup will be aligned such that the indicator arrow will point to exactly the middle of the anchor.
+ * Recommended to be used when there are multiple widgets displayed horizontally so that this will allow
+ * to indicate exactly which widget the popup refers to.
+ */
+ INDICATOR_CENTERED_IN_ANCHOR,
+
+ /**
+ * If the popup doesn't have enough space to expand to its full [CFRPopupProperties.popupWidth],
+ * it will be centred in the screen.
+ * If the popup does have enough space, it defaults to [INDICATOR_CENTERED_IN_ANCHOR].
+ * Recommended to be used when the popup text is very long.
+ */
+ BODY_CENTERED_IN_SCREEN,
+ }
+
+ companion object {
+ /**
+ * Default width for all CFRs.
+ */
+ internal const val DEFAULT_WIDTH = 335
+
+ /**
+ * Fixed horizontal padding.
+ * Allows the close button to extend with 10dp more to the end and intercept touches to
+ * a bit outside of the popup to ensure it respects a11y recommendations of 48dp size while
+ * also offer a bit more space to the text.
+ */
+ internal const val DEFAULT_EXTRA_HORIZONTAL_PADDING = 10
+
+ /**
+ * How tall the indicator arrow should be.
+ * This will also affect the width of the indicator's base which is double the height value.
+ */
+ internal const val DEFAULT_INDICATOR_HEIGHT = 7
+
+ /**
+ * Maximum distance between the popup start and the indicator.
+ */
+ internal const val DEFAULT_INDICATOR_START_OFFSET = 30
+
+ /**
+ * Corner radius for the popup body.
+ */
+ internal const val DEFAULT_CORNER_RADIUS = 12
+
+ /**
+ * Vertical distance between the indicator arrow and the anchor.
+ */
+ internal const val DEFAULT_VERTICAL_OFFSET = 9
+
+ /**
+ * Horizontal margin between the popup and viewport edges used to center the popup when alignment
+ * is set to [PopupAlignment.BODY_CENTERED_IN_SCREEN].
+ */
+ internal const val DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP = 16
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt
new file mode 100644
index 0000000000..318bf16829
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt
@@ -0,0 +1,186 @@
+/* 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.compose.cfr
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.DOWN
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.UP
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Complete content of the popup.
+ * [CFRPopupShape] with a gradient background containing [text] and a dismiss ("X") button.
+ *
+ * @param popupBodyColors One or more colors serving as the popup background.
+ * @param dismissButtonColor The tint color that should be applied to the dismiss button.
+ * @param indicatorDirection The direction the indicator arrow is pointing to.
+ * @param indicatorArrowStartOffset Maximum distance between the popup start and the indicator arrow.
+ * If there isn't enough space this could automatically be overridden up to 0.
+ * @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
+ * was explicit - by tapping the "X" button or not.
+ * @param text [Text] already styled and ready to be shown in the popup.
+ * @param action Optional other composable to show just below the popup text.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+@Suppress("LongMethod")
+fun CFRPopupContent(
+ popupBodyColors: List<Int>,
+ showDismissButton: Boolean,
+ dismissButtonColor: Int,
+ indicatorDirection: CFRPopup.IndicatorDirection,
+ indicatorArrowStartOffset: Dp,
+ onDismiss: (Boolean) -> Unit,
+ popupWidth: Dp = CFRPopup.DEFAULT_WIDTH.dp,
+ text: @Composable (() -> Unit),
+ action: @Composable (() -> Unit) = {},
+) {
+ val popupShape = CFRPopupShape(
+ indicatorDirection,
+ indicatorArrowStartOffset,
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp,
+ CFRPopup.DEFAULT_CORNER_RADIUS.dp,
+ )
+
+ Box(modifier = Modifier.width(popupWidth + CFRPopup.DEFAULT_EXTRA_HORIZONTAL_PADDING.dp)) {
+ Surface(
+ color = Color.Transparent,
+ // Need to override the default RectangleShape to avoid casting shadows for that shape.
+ shape = popupShape,
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ .background(
+ shape = popupShape,
+ brush = Brush.linearGradient(
+ colors = popupBodyColors.map { Color(it) },
+ end = Offset(0f, Float.POSITIVE_INFINITY),
+ start = Offset(Float.POSITIVE_INFINITY, 0f),
+ ),
+ )
+ .wrapContentHeight()
+ .width(popupWidth),
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(
+ start = 16.dp,
+ top = 16.dp + if (indicatorDirection == CFRPopup.IndicatorDirection.UP) {
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp
+ } else {
+ 0.dp
+ },
+ end = 16.dp,
+ bottom = 16.dp +
+ if (indicatorDirection == CFRPopup.IndicatorDirection.DOWN) {
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp
+ } else {
+ 0.dp
+ },
+ ),
+ ) {
+ Box(
+ modifier = Modifier.padding(
+ end = if (showDismissButton) 24.dp else 16.dp, // 8.dp extra padding to the "X" icon
+ ),
+ ) {
+ text()
+ }
+
+ action()
+ }
+ }
+
+ if (showDismissButton) {
+ IconButton(
+ onClick = { onDismiss(true) },
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(
+ end = 6.dp,
+ )
+ .size(48.dp)
+ .semantics {
+ testTagsAsResourceId = true
+ testTag = "cfr.dismiss"
+ },
+ ) {
+ Icon(
+ painter = painterResource(iconsR.drawable.mozac_ic_cross_20),
+ contentDescription = stringResource(R.string.mozac_cfr_dismiss_button_content_description),
+ modifier = Modifier
+ // Following alignment and padding are intended to visually align the middle
+ // of the "X" button with the top of the text.
+ .align(Alignment.Center)
+ .padding(
+ top = if (indicatorDirection == CFRPopup.IndicatorDirection.UP) 9.dp else 0.dp,
+ )
+ .size(24.dp),
+ tint = Color(dismissButtonColor),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupAbovePreview() {
+ CFRPopupContent(
+ popupBodyColors = listOf(Color.Cyan.toArgb(), Color.Blue.toArgb()),
+ showDismissButton = true,
+ dismissButtonColor = Color.Black.toArgb(),
+ indicatorDirection = DOWN,
+ indicatorArrowStartOffset = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
+ onDismiss = { },
+ text = { Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod") },
+ )
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupBelowPreview() {
+ CFRPopupContent(
+ popupBodyColors = listOf(Color.Cyan.toArgb(), Color.Blue.toArgb()),
+ showDismissButton = true,
+ dismissButtonColor = Color.Black.toArgb(),
+ indicatorDirection = UP,
+ indicatorArrowStartOffset = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
+ onDismiss = { },
+ text = { Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod") },
+ )
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt
new file mode 100644
index 0000000000..2ef47182ab
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt
@@ -0,0 +1,537 @@
+/* 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.compose.cfr
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.View
+import android.view.WindowManager
+import androidx.annotation.Px
+import androidx.annotation.VisibleForTesting
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.ViewRootForInspector
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.LayoutDirection.Ltr
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.DOWN
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.UP
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.BODY_CENTERED_IN_SCREEN
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.BODY_TO_ANCHOR_CENTER
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.BODY_TO_ANCHOR_START
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR
+import mozilla.components.compose.cfr.CFRPopupShape.Companion
+import mozilla.components.compose.cfr.helper.DisplayOrientationListener
+import mozilla.components.compose.cfr.helper.ViewDetachedListener
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.android.view.toScope
+import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
+
+@VisibleForTesting
+internal const val SHOW_AFTER_SCREEN_ORIENTATION_CHANGE_DELAY = 500L
+
+/**
+ * Value class allowing to easily reason about what an `Int` represents.
+ * This is compiled to the underlying `Int` type so incurs no performance penalty.
+ */
+@JvmInline
+@VisibleForTesting
+internal value class Pixels(val value: Int)
+
+/**
+ * Simple wrapper over the absolute x-coordinates of the popup. Includes any paddings.
+ */
+@VisibleForTesting
+internal data class PopupHorizontalBounds(
+ val startCoord: Pixels,
+ val endCoord: Pixels,
+)
+
+/**
+ * [AbstractComposeView] that can be added or removed dynamically in the current window to display
+ * a [Composable] based popup anywhere on the screen.
+ *
+ * @param anchor [View] that will serve as the anchor of the popup and serve as lifecycle owner
+ * for this popup also.
+ * @param properties [CFRPopupProperties] allowing to customize the popup behavior.
+ * @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
+ * was explicit - by tapping the "X" button or not.
+ * @param text [Text] already styled and ready to be shown in the popup.
+ * @param action Optional other composable to show just below the popup text.
+ */
+@SuppressLint("ViewConstructor") // Intended to be used only in code, don't need a View constructor
+internal class CFRPopupFullscreenLayout(
+ private val anchor: View,
+ private val properties: CFRPopupProperties,
+ private val onDismiss: (Boolean) -> Unit,
+ private val text: @Composable (() -> Unit),
+ private val action: @Composable (() -> Unit) = {},
+) : AbstractComposeView(anchor.context), ViewRootForInspector {
+ private val windowManager = anchor.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+ /**
+ * Listener for when the anchor is removed from the screen.
+ * Useful in the following situations:
+ * - lack of purpose - if there is no anchor the context/action to which this popup refers to disappeared
+ * - leak from WindowManager - if removing the app from task manager while the popup is shown.
+ *
+ * Will not inform client about this since the user did not expressly dismissed this popup.
+ */
+ private val anchorDetachedListener = ViewDetachedListener {
+ dismiss()
+ }
+
+ /**
+ * When the screen is rotated the popup may get improperly anchored
+ * because of the async nature of insets and screen rotation.
+ * To avoid any improper anchorage the popups are automatically dismissed.
+ *
+ * Will not inform client about this since the user did not expressly dismissed this popup.
+ *
+ * Since a UX decision has been made here:
+ * [link](https://github.com/mozilla-mobile/fenix/issues/27033#issuecomment-1302363014)
+ * to redisplay any **implicitly** dismissed CFRs, a short delay will be added,
+ * after which the CFR will be shown again.
+ *
+ */
+ @VisibleForTesting
+ internal lateinit var orientationChangeListener: DisplayOrientationListener
+
+ override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
+ private set
+
+ /**
+ * Add a new CFR popup to the current window overlaying everything already displayed.
+ * This popup will be dismissed when the user clicks on the "x" button or based on other user actions
+ * with such behavior set in [CFRPopupProperties].
+ */
+ fun show() {
+ setViewTreeLifecycleOwner(anchor.findViewTreeLifecycleOwner())
+ this.setViewTreeSavedStateRegistryOwner(anchor.findViewTreeSavedStateRegistryOwner())
+ anchor.addOnAttachStateChangeListener(anchorDetachedListener)
+ orientationChangeListener = getDisplayOrientationListener(anchor.context).also {
+ it.start()
+ }
+ windowManager.addView(this, createLayoutParams())
+ }
+
+ @Composable
+ override fun Content() {
+ val anchorLocation = IntArray(2).apply {
+ anchor.getLocationInWindow(this)
+ }
+
+ val anchorXCoordMiddle = Pixels(anchorLocation.first() + anchor.width / 2)
+ val indicatorArrowHeight = Pixels(
+ CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp.toPx(),
+ )
+
+ val popupBounds = computePopupHorizontalBounds(
+ anchorMiddleXCoord = anchorXCoordMiddle,
+ arrowIndicatorWidth = Pixels(Companion.getIndicatorBaseWidthForHeight(indicatorArrowHeight.value)),
+ screenWidth = Pixels(LocalConfiguration.current.screenWidthDp.dp.toPx()),
+ layoutDirection = LocalConfiguration.current.layoutDirection,
+ )
+
+ val indicatorOffset = computeIndicatorArrowStartCoord(
+ anchorMiddleXCoord = anchorXCoordMiddle,
+ popupStartCoord = popupBounds.startCoord,
+ arrowIndicatorWidth = Pixels(
+ Companion.getIndicatorBaseWidthForHeight(indicatorArrowHeight.value),
+ ),
+ )
+
+ Popup(
+ popupPositionProvider = getPopupPositionProvider(
+ anchorLocation = anchorLocation,
+ popupBounds = popupBounds,
+ ),
+ properties = PopupProperties(
+ focusable = true,
+ dismissOnBackPress = properties.dismissOnBackPress,
+ dismissOnClickOutside = properties.dismissOnClickOutside,
+ ),
+ onDismissRequest = {
+ // For when tapping outside the popup.
+ dismiss()
+ onDismiss(false)
+ },
+ ) {
+ CFRPopupContent(
+ popupBodyColors = properties.popupBodyColors,
+ showDismissButton = properties.showDismissButton,
+ dismissButtonColor = properties.dismissButtonColor,
+ indicatorDirection = properties.indicatorDirection,
+ indicatorArrowStartOffset = with(LocalDensity.current) {
+ indicatorOffset.value.toDp()
+ },
+ onDismiss = {
+ // For when tapping the "X" button.
+ dismiss()
+ onDismiss(true)
+ },
+ popupWidth = if (shouldCenterPopup(LocalConfiguration.current.screenWidthDp.dp.toPx())) {
+ (LocalConfiguration.current.screenWidthDp - 2 * CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP).dp
+ } else {
+ properties.popupWidth
+ },
+ text = text,
+ action = action,
+ )
+ }
+ }
+
+ @Composable
+ private fun getPopupPositionProvider(
+ anchorLocation: IntArray,
+ popupBounds: PopupHorizontalBounds,
+ ): PopupPositionProvider {
+ return object : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize,
+ ): IntOffset {
+ // Popup will be anchored such that the indicator arrow will point to the middle of the anchor View
+ // but the popup is allowed some space as start padding in which it can be displayed such that the
+ // indicator arrow is exactly at the top-start/bottom-start corner but slightly translated to end.
+ // Values are in pixels.
+ return IntOffset(
+ when (layoutDirection) {
+ Ltr -> popupBounds.startCoord.value
+ else -> popupBounds.endCoord.value
+ },
+ when (properties.indicatorDirection) {
+ UP -> {
+ when (properties.overlapAnchor) {
+ true -> anchorLocation.last() + anchor.height / 2
+ else -> anchorLocation.last() + anchor.height + properties.popupVerticalOffset.toPx()
+ }
+ }
+ DOWN -> {
+ when (properties.overlapAnchor) {
+ true -> anchorLocation.last() - popupContentSize.height + anchor.height / 2
+ else -> anchorLocation.last() - popupContentSize.height -
+ properties.popupVerticalOffset.toPx()
+ }
+ }
+ },
+ )
+ }
+ }
+ }
+
+ /**
+ * Whether or not the popup body should be centered in the screen, this is only true if the screen does not
+ * allow the popup to be centered at its maximum width.
+ */
+ private fun shouldCenterPopup(
+ screenWidth: Int,
+ ): Boolean {
+ val maximumPopupWidth = properties.popupWidth.toPx() +
+ 2 * CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP.dp.toPx()
+ return properties.popupAlignment == BODY_CENTERED_IN_SCREEN && maximumPopupWidth > screenWidth
+ }
+
+ /**
+ * Compute the x-coordinates for the absolute start and end position of the popup, including any padding.
+ * This assumes anchoring is indicated with an arrow to the horizontal middle of the anchor with the popup's
+ * body potentially extending to the `start` of the arrow indicator.
+ *
+ * @param anchorMiddleXCoord x-coordinate for the middle of the anchor.
+ * @param arrowIndicatorWidth x-distance the arrow indicator occupies.
+ * @param screenWidth available width in which the popup will be shown.
+ * @param layoutDirection the layout direction of the anchor layout.
+ */
+ @VisibleForTesting
+ @Suppress("LongMethod")
+ internal fun computePopupHorizontalBounds(
+ anchorMiddleXCoord: Pixels,
+ arrowIndicatorWidth: Pixels,
+ screenWidth: Pixels,
+ layoutDirection: Int,
+ ): PopupHorizontalBounds {
+ val arrowIndicatorHalfWidth = arrowIndicatorWidth.value / 2
+
+ return if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ computeHorizontalBoundsLTR(
+ anchorStart = Pixels(anchorMiddleXCoord.value - arrowIndicatorHalfWidth),
+ screenWidth = screenWidth,
+ )
+ } else {
+ computeHorizontalBoundsRTL(
+ anchorStart = Pixels(anchorMiddleXCoord.value + arrowIndicatorHalfWidth),
+ screenWidth = screenWidth,
+ )
+ }
+ }
+
+ private fun computeHorizontalBoundsLTR(
+ anchorStart: Pixels,
+ screenWidth: Pixels,
+ ): PopupHorizontalBounds {
+ val popupPadding = Pixels(CFRPopup.DEFAULT_EXTRA_HORIZONTAL_PADDING.dp.toPx())
+ val leftInsets = Pixels(getLeftInsets())
+ val popupWidth = Pixels(properties.popupWidth.toPx())
+ val viewportMargin =
+ CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP.dpToPx(anchor.resources.displayMetrics)
+ var startCoord = when (properties.popupAlignment) {
+ BODY_TO_ANCHOR_START -> {
+ Pixels(anchor.x.roundToInt() + leftInsets.value)
+ }
+
+ BODY_TO_ANCHOR_CENTER -> {
+ Pixels(
+ anchor.x.roundToInt()
+ .plus((anchor.width - popupWidth.value) / 2)
+ .plus(leftInsets.value),
+ )
+ }
+
+ INDICATOR_CENTERED_IN_ANCHOR,
+ BODY_CENTERED_IN_SCREEN,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(viewportMargin + leftInsets.value)
+ } else {
+ Pixels(
+ (anchorStart.value)
+ .minus(properties.indicatorArrowStartOffset.toPx())
+ .coerceAtLeast(leftInsets.value),
+ )
+ }
+ }
+ }
+
+ val endCoord = when (properties.popupAlignment) {
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(screenWidth.value - viewportMargin)
+ } else {
+ var maybeEndCoord = Pixels(
+ startCoord.value
+ .plus(popupWidth.value)
+ .plus(popupPadding.value),
+ )
+ // Handle the scenario in which the popup would get pass the end of the screen.
+ // Allow it to only be shown between [0, screenWidth] and if these bounds are surpassed
+ // translate it horizontally to the start to show as much of it as possible.
+ val endCoordOverflow = maybeEndCoord.value - screenWidth.value
+ if (endCoordOverflow > 0) {
+ startCoord = Pixels(
+ startCoord.value
+ .minus(endCoordOverflow)
+ .coerceAtLeast(leftInsets.value),
+ )
+ maybeEndCoord =
+ Pixels(maybeEndCoord.value.coerceAtMost(screenWidth.value + leftInsets.value))
+ }
+ maybeEndCoord
+ }
+ }
+
+ else -> {
+ Pixels(
+ startCoord.value
+ .plus(popupWidth.value)
+ .plus(popupPadding.value)
+ .coerceAtMost(screenWidth.value + leftInsets.value),
+ )
+ }
+ }
+
+ return PopupHorizontalBounds(
+ startCoord = startCoord,
+ endCoord = endCoord,
+ )
+ }
+
+ private fun computeHorizontalBoundsRTL(
+ anchorStart: Pixels,
+ screenWidth: Pixels,
+ ): PopupHorizontalBounds {
+ val popupPadding = Pixels(CFRPopup.DEFAULT_EXTRA_HORIZONTAL_PADDING.dp.toPx())
+ val leftInsets = Pixels(getLeftInsets())
+ val popupWidth = Pixels(properties.popupWidth.toPx())
+ val viewportMargin =
+ CFRPopup.DEFAULT_HORIZONTAL_VIEWPORT_MARGIN_DP.dpToPx(anchor.resources.displayMetrics)
+ var startCoord = when (properties.popupAlignment) {
+ BODY_TO_ANCHOR_START -> {
+ Pixels(anchor.x.roundToInt() + anchor.width + leftInsets.value)
+ }
+ BODY_TO_ANCHOR_CENTER -> {
+ val anchorEndCoord = anchor.x.roundToInt() + anchor.width
+ Pixels(
+ anchorEndCoord
+ .minus((anchor.width - popupWidth.value) / 2)
+ .plus(leftInsets.value),
+ )
+ }
+
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(screenWidth.value - viewportMargin)
+ } else {
+ Pixels(
+ // Push the popup as far to the start (in RTL) as possible.
+ anchorStart.value
+ .plus(properties.indicatorArrowStartOffset.toPx())
+ .coerceAtMost(screenWidth.value + leftInsets.value),
+ )
+ }
+ }
+ }
+
+ val endCoord = when (properties.popupAlignment) {
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ if (shouldCenterPopup(screenWidth.value)) {
+ Pixels(viewportMargin - leftInsets.value)
+ } else {
+ var maybeEndCoord = Pixels(
+ startCoord.value
+ .minus(popupWidth.value)
+ .minus(popupPadding.value),
+ )
+ val endCoordOverflow = leftInsets.value - maybeEndCoord.value
+ // Handle the scenario in which the popup would get pass the end of the screen (in RTL)
+ // Allow it to only be shown between [0, screenWidth] and if these bounds are surpassed
+ // translate it horizontally to the start to show as much of it as possible.
+ if (endCoordOverflow > 0) {
+ startCoord = Pixels(
+ startCoord.value
+ .plus(endCoordOverflow.absoluteValue)
+ .coerceAtMost(screenWidth.value + leftInsets.value),
+ )
+ maybeEndCoord =
+ Pixels(maybeEndCoord.value.coerceAtLeast(leftInsets.value))
+ }
+ maybeEndCoord
+ }
+ }
+
+ else -> {
+ Pixels(
+ startCoord.value
+ .minus(popupWidth.value)
+ .minus(popupPadding.value)
+ .coerceAtLeast(leftInsets.value),
+ )
+ }
+ }
+
+ return PopupHorizontalBounds(startCoord, endCoord)
+ }
+
+ /**
+ * Compute the x-coordinate for where the popup's indicator arrow should start
+ * relative to the available distance between it and the popup's starting x-coordinate.
+ *
+ * @param anchorMiddleXCoord x-coordinate for the middle of the anchor.
+ * @param popupStartCoord x-coordinate for the popup start
+ * @param arrowIndicatorWidth Width of the arrow indicator.
+ */
+ @Composable
+ private fun computeIndicatorArrowStartCoord(
+ anchorMiddleXCoord: Pixels,
+ popupStartCoord: Pixels,
+ arrowIndicatorWidth: Pixels,
+ ): Pixels {
+ return when (properties.popupAlignment) {
+ BODY_TO_ANCHOR_START,
+ BODY_TO_ANCHOR_CENTER,
+ -> Pixels(properties.indicatorArrowStartOffset.toPx())
+ BODY_CENTERED_IN_SCREEN,
+ INDICATOR_CENTERED_IN_ANCHOR,
+ -> {
+ val arrowIndicatorHalfWidth = arrowIndicatorWidth.value / 2
+ if (LocalConfiguration.current.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
+ Pixels(anchorMiddleXCoord.value - arrowIndicatorHalfWidth - popupStartCoord.value)
+ } else {
+ val visiblePopupEndCoord = popupStartCoord.value
+ Pixels(visiblePopupEndCoord - anchorMiddleXCoord.value - arrowIndicatorHalfWidth)
+ }
+ }
+ }
+ }
+
+ /**
+ * Cleanup and remove the current popup from the screen.
+ * Clients are not automatically informed about this. Use a separate call to [onDismiss] if needed.
+ */
+ internal fun dismiss() {
+ anchor.removeOnAttachStateChangeListener(anchorDetachedListener)
+ orientationChangeListener.stop()
+ disposeComposition()
+ setViewTreeLifecycleOwner(null)
+ this.setViewTreeSavedStateRegistryOwner(null)
+ windowManager.removeViewImmediate(this)
+ }
+
+ /**
+ * Create fullscreen translucent layout params.
+ * This will allow placing the visible popup anywhere on the screen.
+ */
+ @VisibleForTesting
+ internal fun createLayoutParams(): WindowManager.LayoutParams =
+ WindowManager.LayoutParams().apply {
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+ token = anchor.applicationWindowToken
+ width = WindowManager.LayoutParams.MATCH_PARENT
+ height = WindowManager.LayoutParams.MATCH_PARENT
+ format = PixelFormat.TRANSLUCENT
+ flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ }
+
+ private fun getDisplayOrientationListener(context: Context) = DisplayOrientationListener(context) {
+ dismiss()
+ anchor.toScope().launch {
+ delay(SHOW_AFTER_SCREEN_ORIENTATION_CHANGE_DELAY)
+ show()
+ }
+ }
+
+ /**
+ * Intended to allow querying the insets of the navigation bar.
+ * Value will be `0` except for when the screen is rotated by 90 degrees.
+ */
+ private fun getLeftInsets() = ViewCompat.getRootWindowInsets(anchor)
+ ?.getInsets(WindowInsetsCompat.Type.systemBars())?.left
+ ?: 0.coerceAtLeast(0)
+
+ @Px
+ internal fun Dp.toPx(): Int {
+ return this.value
+ .dpToPx(anchor.resources.displayMetrics)
+ .roundToInt()
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt
new file mode 100644
index 0000000000..8606bdcebc
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt
@@ -0,0 +1,272 @@
+/* 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.compose.cfr
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.DOWN
+import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection.UP
+import kotlin.math.roundToInt
+
+/**
+ * How wide the base of the indicator should be in relation with the indicator's height.
+ */
+private const val INDICATOR_BASE_TO_HEIGHT_RATIO = 2f
+
+/**
+ * A [Shape] describing a popup with an indicator triangle shown above or below the popup.
+ *
+ * @param indicatorDirection The direction the indicator arrow is pointing to.
+ * @param indicatorArrowStartOffset Distance between the popup start and the indicator arrow start
+ * @param indicatorArrowHeight Height of the indicator triangle. This influences the base length.
+ * @param cornerRadius The radius of the popup's corners.
+ * If [indicatorArrowStartOffset] is `0` then the top-start corner will not be rounded.
+ */
+class CFRPopupShape(
+ private val indicatorDirection: CFRPopup.IndicatorDirection,
+ private val indicatorArrowStartOffset: Dp,
+ private val indicatorArrowHeight: Dp,
+ private val cornerRadius: Dp,
+) : Shape {
+ @Suppress("LongMethod")
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density,
+ ): Outline {
+ val indicatorArrowStartOffsetPx = indicatorArrowStartOffset.value * density.density
+ val indicatorArrowHeightPx = indicatorArrowHeight.value * density.density
+ val indicatorArrowBasePx =
+ getIndicatorBaseWidthForHeight((indicatorArrowHeight.value * density.density).roundToInt())
+ val cornerRadiusPx = cornerRadius.value * density.density
+ val indicatorCornerRadiusPx = cornerRadiusPx.coerceAtMost(indicatorArrowStartOffsetPx)
+
+ // All outlines are drawn in a LTR space but with accounting for the LTR direction.
+ return when (indicatorDirection) {
+ CFRPopup.IndicatorDirection.UP -> {
+ Outline.Generic(
+ path = Path().apply {
+ reset()
+
+ lineTo(0f, size.height - cornerRadiusPx)
+ quadraticBezierTo(
+ 0f,
+ size.height,
+ cornerRadiusPx,
+ size.height,
+ )
+
+ lineTo(size.width - cornerRadiusPx, size.height)
+ quadraticBezierTo(
+ size.width,
+ size.height,
+ size.width,
+ size.height - cornerRadiusPx,
+ )
+
+ if (layoutDirection == LayoutDirection.Ltr) {
+ lineTo(size.width, cornerRadiusPx + indicatorArrowHeightPx)
+ quadraticBezierTo(
+ size.width,
+ indicatorArrowHeightPx,
+ size.width - cornerRadiusPx,
+ indicatorArrowHeightPx,
+ )
+
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx, indicatorArrowHeightPx)
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx / 2, 0f)
+ lineTo(indicatorArrowStartOffsetPx, indicatorArrowHeightPx)
+
+ lineTo(indicatorCornerRadiusPx, indicatorArrowHeightPx)
+ quadraticBezierTo(
+ 0f,
+ indicatorArrowHeightPx,
+ 0f,
+ indicatorArrowHeightPx + indicatorCornerRadiusPx,
+ )
+ } else {
+ lineTo(size.width, indicatorCornerRadiusPx + indicatorArrowHeightPx)
+ quadraticBezierTo(
+ size.width,
+ indicatorArrowHeightPx,
+ size.width - indicatorCornerRadiusPx,
+ indicatorArrowHeightPx,
+ )
+
+ val indicatorEnd = size.width - indicatorArrowStartOffsetPx
+ lineTo(indicatorEnd, indicatorArrowHeightPx)
+ lineTo(indicatorEnd - indicatorArrowBasePx / 2, 0f)
+ lineTo(indicatorEnd - indicatorArrowBasePx, indicatorArrowHeightPx)
+
+ lineTo(cornerRadiusPx, indicatorArrowHeightPx)
+ quadraticBezierTo(
+ 0f,
+ indicatorArrowHeightPx,
+ 0f,
+ indicatorArrowHeightPx + cornerRadiusPx,
+ )
+ }
+
+ close()
+ },
+ )
+ }
+ CFRPopup.IndicatorDirection.DOWN -> {
+ val messageBodyHeightPx = size.height - indicatorArrowHeightPx
+
+ Outline.Generic(
+ path = Path().apply {
+ reset()
+
+ if (layoutDirection == LayoutDirection.Ltr) {
+ lineTo(0f, messageBodyHeightPx - indicatorCornerRadiusPx)
+ quadraticBezierTo(
+ 0f,
+ size.height - indicatorArrowHeightPx,
+ indicatorCornerRadiusPx,
+ size.height - indicatorArrowHeightPx,
+ )
+
+ lineTo(indicatorArrowStartOffsetPx, messageBodyHeightPx)
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx / 2, size.height)
+ lineTo(indicatorArrowStartOffsetPx + indicatorArrowBasePx, messageBodyHeightPx)
+
+ lineTo(size.width - cornerRadiusPx, messageBodyHeightPx)
+ quadraticBezierTo(
+ size.width,
+ messageBodyHeightPx,
+ size.width,
+ messageBodyHeightPx - cornerRadiusPx,
+ )
+ } else {
+ lineTo(0f, messageBodyHeightPx - cornerRadiusPx)
+ quadraticBezierTo(
+ 0f,
+ messageBodyHeightPx,
+ cornerRadiusPx,
+ messageBodyHeightPx,
+ )
+
+ val indicatorStartPx = size.width - indicatorArrowStartOffsetPx - indicatorArrowBasePx
+ lineTo(indicatorStartPx, messageBodyHeightPx)
+ lineTo(indicatorStartPx + indicatorArrowBasePx / 2, size.height)
+ lineTo(indicatorStartPx + indicatorArrowBasePx, messageBodyHeightPx)
+
+ lineTo(size.width - indicatorCornerRadiusPx, messageBodyHeightPx)
+ quadraticBezierTo(
+ size.width,
+ messageBodyHeightPx,
+ size.width,
+ messageBodyHeightPx - indicatorCornerRadiusPx,
+ )
+ }
+
+ lineTo(size.width, cornerRadiusPx)
+ quadraticBezierTo(
+ size.width,
+ 0f,
+ size.width - cornerRadiusPx,
+ 0f,
+ )
+
+ lineTo(cornerRadiusPx, 0f)
+ quadraticBezierTo(
+ 0f,
+ 0f,
+ 0f,
+ cornerRadiusPx,
+ )
+
+ close()
+ },
+ )
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * This [Shape]'s arrow indicator will have an automatic width depending on the set height.
+ * This method allows knowing what the base width will be before instantiating the class.
+ */
+ fun getIndicatorBaseWidthForHeight(height: Int): Int {
+ return (height * INDICATOR_BASE_TO_HEIGHT_RATIO).roundToInt()
+ }
+ }
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupBelowShapePreview() {
+ Box(
+ modifier = Modifier
+ .height(100.dp)
+ .width(200.dp)
+ .background(
+ shape = CFRPopupShape(UP, 10.dp, 10.dp, 10.dp),
+ brush = Brush.linearGradient(
+ colors = listOf(Color.Cyan, Color.Blue),
+ end = Offset(0f, Float.POSITIVE_INFINITY),
+ start = Offset(Float.POSITIVE_INFINITY, 0f),
+ ),
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = "This is just a test",
+ color = MaterialTheme.colors.onPrimary,
+ )
+ }
+}
+
+@Composable
+@Preview(locale = "en", name = "LTR")
+@Preview(locale = "ar", name = "RTL")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
+private fun CFRPopupAboveShapePreview() {
+ Box(
+ modifier = Modifier
+ .height(100.dp)
+ .width(200.dp)
+ .background(
+ shape = CFRPopupShape(DOWN, 10.dp, 10.dp, 10.dp),
+ brush = Brush.linearGradient(
+ colors = listOf(Color.Cyan, Color.Blue),
+ end = Offset(0f, Float.POSITIVE_INFINITY),
+ start = Offset(Float.POSITIVE_INFINITY, 0f),
+ ),
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = "This is just a test",
+ color = MaterialTheme.colors.onPrimary,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt
new file mode 100644
index 0000000000..4a44ac6127
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt
@@ -0,0 +1,65 @@
+/* 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.compose.cfr.helper
+
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.DisplayListener
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Inform when the rotation of the screen changes.
+ * Since this is using a [DisplayManager] listener it's important to call [start] and [stop]
+ * at the appropriate moments to register and unregister said listener.
+ *
+ * @param context Android context needed to interact with the [DisplayManager]
+ * @param onDisplayRotationChanged Listener for when the display rotation changes.
+ * This will be called when the display changes to any of the four main orientations:
+ * [PORTRAIT, LANDSCAPE, REVERSE_PORTRAIT, REVERSE_LANDSCAPE].
+ * No updates will be triggered if the "Auto-rotate" functionality is disabled for the device.
+ */
+internal class DisplayOrientationListener(
+ private val context: Context,
+ val onDisplayRotationChanged: () -> Unit,
+) : DisplayListener {
+ private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+
+ @VisibleForTesting
+ internal var currentOrientation = getCurrentOrientation()
+
+ /**
+ * Start listening for display orientation changes.
+ * It's important to also call [stop] when done listening to prevent leaking the listener.
+ */
+ fun start() {
+ displayManager.registerDisplayListener(this, null)
+ }
+
+ /**
+ * Stop listening for display orientation changes and cleanup the current [DisplayManager] listener.
+ */
+ fun stop() {
+ displayManager.unregisterDisplayListener(this)
+ }
+
+ override fun onDisplayAdded(displayId: Int) = Unit
+
+ override fun onDisplayRemoved(displayId: Int) = Unit
+
+ override fun onDisplayChanged(displayId: Int) {
+ val newOrientation = getCurrentOrientation(displayId)
+
+ if (newOrientation != this.currentOrientation) {
+ this.currentOrientation = newOrientation
+ onDisplayRotationChanged()
+ }
+ }
+
+ private fun getCurrentOrientation(displayId: Int = 0): Int = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ true -> context.resources.configuration.orientation
+ false -> displayManager.getDisplay(displayId)?.rotation ?: currentOrientation
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt
new file mode 100644
index 0000000000..da6782604a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.compose.cfr.helper
+
+import android.view.View
+
+/**
+ * Simpler [View.OnAttachStateChangeListener] only informing about
+ * [View.OnAttachStateChangeListener.onViewDetachedFromWindow].
+ */
+internal class ViewDetachedListener(val onDismiss: () -> Unit) : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) = Unit
+
+ override fun onViewDetachedFromWindow(v: View) {
+ onDismiss()
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..c97e99ec94
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">አሰናብት</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..0676ead800
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Escartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..77a2c88eb2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">باغلا</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..19d6f18471
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Адхіліць</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..12463e5dda
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Прекратяване</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..6b257c25c7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Argas</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..d8b54cb34b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Odbaci</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..c5e24af615
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descarta</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..3152b578ba
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Tichup ruwäch</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..437dd527f5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">پشتگوێخستن</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..6ac10d7f41
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ricusà</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..7931a04581
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zavřít</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..cc82da5e42
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Cau</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..d03d570df3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Afvis</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..fc26b597f7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Schließen</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..2c84c274e2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zachyśiś</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..5a3bba4948
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Απόρριψη</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..d3515657b4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..d3515657b4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..af51005a75
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignori</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..e4a97a05b3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ocultar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..e6ef08c0b2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Peida</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..6002549de1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Baztertu</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..7d50c5d847
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">رد کردن</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..dae33bc1fd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Hylkää</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..68a4823ca3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Fermer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..4a414e4c5b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Siere</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..105ba034c4
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Slute</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..13c3244a40
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Leig seachad</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..fdc1c7e79f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Rexeitar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..9615457637
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Mboyke</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..d8b54cb34b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Odbaci</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..d4b638b5be
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zaćisnyć</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..00bc0d7781
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Eltüntetés</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..6a33629dd2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Բաց թողնել</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..716e14b9a7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dimitter</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..a1d014b221
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Tutup</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..6ef45a90ca
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Hafna</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..07debd3b5c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Chiudi</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..d7c5324959
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">סגירה</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..a1efc5c03d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">閉じる</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..49bea70e48
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">არიდება</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..927fa4b2d3
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Jabıw</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..05b7c36c7a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zgel</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..1e137a1fb1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Тайдыру</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..0a59a2e76f
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Bigire</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..a8f2e61c9b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">닫기</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..1200deb331
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ປິດ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..d2d923b1b9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Paslėpti</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..a403088daf
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..db4b674192
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Sluiten</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..a403088daf
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorer</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..7726a050b9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..495a1e811e
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ਖਾਰਜ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..27ee637bfd
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">بند کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..6fb5777d89
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zamknij</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..f14c6e05a9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Descartar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..da76c02be9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dispensar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..12c3220442
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Serrar</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..c9748091ec
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Скрыть</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..e432bced8a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ᱵᱚᱱᱫ</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..2cf10b5097
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Iscarta</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..692fe43d68
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ඉවතලන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..335a45ecc7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zavrieť</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..ed279b9081
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">فارغ کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..b476df56f0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Zapri</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..23c71495b1
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Hidhe tej</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..f535bf88e7
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Одбаци</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..a1d014b221
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Tutup</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..87506c9962
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ignorera</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml
new file mode 100644
index 0000000000..ca2214b892
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Ôdkoż</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..e77dfe593c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Нодида гузарондан</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..4ab6fc629d
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">ปิด</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..76f55fe0e8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Kapat</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..b5bdf60de2
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Si gi\'hiaj guendô\'</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..acdeb1ed5b
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Яшерү</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..1eaac24e2a
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">بولدىلا</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..1d1f11dbbb
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Відхилити</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..716312dfd9
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Rad qilish</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..8c4ff51166
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Sara</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..3d9e560fee
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Bỏ qua</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..1c5ff52f06
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">知道了</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..9414216213
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">知道了!</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml b/mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..86c00ee3c0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<!-- 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>
+ <!-- Content description (not visible, for screen readers etc.): Description for the close button of a Contextual Feature Recommendation Popup. -->
+ <string name="mozac_cfr_dismiss_button_content_description">Dismiss</string>
+</resources>
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt
new file mode 100644
index 0000000000..a0259e0bc5
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt
@@ -0,0 +1,561 @@
+/* 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.compose.cfr
+
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.graphics.PixelFormat
+import android.view.View
+import android.view.ViewManager
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
+import mozilla.components.compose.cfr.CFRPopup.PopupAlignment
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@RunWith(AndroidJUnit4::class)
+class CFRPopupFullscreenLayoutTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `WHEN the popup is shown THEN setup lifecycle owners`() {
+ val anchor = View(testContext).apply {
+ setViewTreeLifecycleOwner(mock())
+ this.setViewTreeSavedStateRegistryOwner(mock())
+ }
+
+ val popupView = spy(
+ CFRPopupFullscreenLayout(
+ anchor = anchor,
+ properties = mock(),
+ onDismiss = mock(),
+ text = { },
+ action = { },
+ ),
+ )
+ popupView.show()
+
+ assertNotNull(popupView.findViewTreeLifecycleOwner())
+ assertEquals(
+ anchor.findViewTreeLifecycleOwner(),
+ popupView.findViewTreeLifecycleOwner(),
+ )
+ assertNotNull(popupView.findViewTreeSavedStateRegistryOwner())
+ assertEquals(
+ assertNotNull(anchor.findViewTreeSavedStateRegistryOwner()),
+ assertNotNull(popupView.findViewTreeSavedStateRegistryOwner()),
+ )
+ }
+
+ @Test
+ fun `WHEN the popup is dismissed THEN cleanup lifecycle owners and detach from window`() {
+ val context = spy(testContext)
+ val anchor = View(context).apply {
+ setViewTreeLifecycleOwner(mock())
+ this.setViewTreeSavedStateRegistryOwner(mock())
+ }
+ val windowManager = spy(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
+ doReturn(windowManager).`when`(context).getSystemService(Context.WINDOW_SERVICE)
+ val popupView = CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { })
+ popupView.show()
+ assertNotNull(popupView.findViewTreeLifecycleOwner())
+ assertNotNull(popupView.findViewTreeSavedStateRegistryOwner())
+
+ popupView.dismiss()
+
+ assertNull(popupView.findViewTreeLifecycleOwner())
+ assertNull(popupView.findViewTreeSavedStateRegistryOwner())
+ verify(windowManager).removeViewImmediate(popupView)
+ }
+
+ @Test
+ fun `GIVEN a popup WHEN adding it to window THEN use translucent layout params`() {
+ val context = spy(testContext)
+ val anchor = View(context)
+ val windowManager = spy(context.getSystemService(Context.WINDOW_SERVICE))
+ doReturn(windowManager).`when`(context).getSystemService(Context.WINDOW_SERVICE)
+ val popupView = CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { })
+ val layoutParamsCaptor = argumentCaptor<LayoutParams>()
+
+ popupView.show()
+
+ verify(windowManager as ViewManager).addView(eq(popupView), layoutParamsCaptor.capture())
+ assertEquals(LayoutParams.TYPE_APPLICATION_PANEL, layoutParamsCaptor.value.type)
+ assertEquals(anchor.applicationWindowToken, layoutParamsCaptor.value.token)
+ assertEquals(LayoutParams.MATCH_PARENT, layoutParamsCaptor.value.width)
+ assertEquals(LayoutParams.MATCH_PARENT, layoutParamsCaptor.value.height)
+ assertEquals(PixelFormat.TRANSLUCENT, layoutParamsCaptor.value.format)
+ assertEquals(
+ LayoutParams.FLAG_LAYOUT_IN_SCREEN or LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ layoutParamsCaptor.value.flags,
+ )
+ }
+
+ @Test
+ fun `WHEN creating layout params THEN get fullscreen translucent layout params`() {
+ val anchor = View(testContext)
+ val popupView = CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { })
+
+ val result = popupView.createLayoutParams()
+
+ assertEquals(LayoutParams.TYPE_APPLICATION_PANEL, result.type)
+ assertEquals(anchor.applicationWindowToken, result.token)
+ assertEquals(LayoutParams.MATCH_PARENT, result.width)
+ assertEquals(LayoutParams.MATCH_PARENT, result.height)
+ assertEquals(PixelFormat.TRANSLUCENT, result.format)
+ assertEquals(
+ LayoutParams.FLAG_LAYOUT_IN_SCREEN or LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ result.flags,
+ )
+ }
+
+ @Test
+ fun `GIVEN LTR and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(190, result.startCoord.value)
+ assertEquals(400, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN account for the provided indicator offset`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ // The popup should be translated to the start to ensure the offset to the indicator is respected.
+ assertEquals(140, result.startCoord.value)
+ assertEquals(350, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds and the popup doesn't fit THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 900.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ // The popup should be translated to the start to ensure it fits the screen.
+ assertEquals(90, result.startCoord.value)
+ assertEquals(1000, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(800),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(810, result.startCoord.value)
+ assertEquals(600, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds THEN account for the provided indicator offset`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(800),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ // The popup should be translated to the start to ensure the offset to the indicator is respected.
+ assertEquals(860, result.startCoord.value)
+ assertEquals(650, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and INDICATOR_CENTERED_IN_ANCHOR WHEN computing popup bounds and the popup doesn't fit THEN return the right X coordinates`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 900.dp,
+ popupAlignment = PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(800),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ // The popup should be translated to the start to ensure it fits the screen.
+ assertEquals(910, result.startCoord.value)
+ assertEquals(0, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 300.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(200, result.startCoord.value)
+ assertEquals(510, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 300.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(200, result.startCoord.value)
+ assertEquals(510, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(600, result.startCoord.value)
+ assertEquals(390, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_START WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 200.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_START,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(600, result.startCoord.value)
+ assertEquals(390, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(300, result.startCoord.value)
+ assertEquals(710, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(300, result.startCoord.value)
+ assertEquals(710, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(700, result.startCoord.value)
+ assertEquals(290, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL and BODY_TO_ANCHOR_CENTER WHEN computing popup bounds THEN return the right X coordinates and don't account for the provided indicator offset`() {
+ val anchor = spy(View(testContext))
+ doReturn(600).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
+ indicatorArrowStartOffset = 50.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(300),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(700, result.startCoord.value)
+ assertEquals(290, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR direction and popup is larger than viewport width WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN return the correct horizontal bounds`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 500.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(500),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(16, result.startCoord.value)
+ // The screen width minus the viewport margin
+ assertEquals(484, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN LTR direction and popup fits inside the viewport WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN the horizontal bounds are calculated for BODY_TO_ANCHOR_CENTER alignment`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(200),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_LTR,
+ )
+
+ assertEquals(190, result.startCoord.value)
+ assertEquals(600, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL direction and popup is larger than viewport width WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN return the correct horizontal bounds`() {
+ val anchor = spy(View(testContext))
+ doReturn(400).`when`(anchor).width
+ doReturn(200f).`when`(anchor).x
+ val properties = CFRPopupProperties(
+ popupWidth = 500.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(400),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(500),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ // The screen width minus the viewport margin
+ assertEquals(484, result.startCoord.value)
+ assertEquals(16, result.endCoord.value)
+ }
+
+ @Test
+ fun `GIVEN RTL direction and popup fits inside the viewport WHEN computing popup bounds for CENTERED_IN_SCREEN alignment THEN the horizontal bounds are calculated for BODY_TO_ANCHOR_CENTER alignment`() {
+ val anchor = View(testContext)
+ val properties = CFRPopupProperties(
+ popupWidth = 400.dp,
+ popupAlignment = PopupAlignment.BODY_CENTERED_IN_SCREEN,
+ indicatorArrowStartOffset = 0.dp,
+ )
+ val popupView = CFRPopupFullscreenLayout(anchor, properties, mock(), { }, { })
+
+ val result = popupView.computePopupHorizontalBounds(
+ anchorMiddleXCoord = Pixels(700),
+ arrowIndicatorWidth = Pixels(20),
+ screenWidth = Pixels(1000),
+ layoutDirection = View.LAYOUT_DIRECTION_RTL,
+ )
+
+ assertEquals(710, result.startCoord.value)
+ assertEquals(300, result.endCoord.value)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun `GIVEN there is a CFR Popup showing WHEN the orientation of the device changes THEN the CFR will be dismissed and shown again after a delay`() = runTestOnMain {
+ val context = spy(testContext)
+ val anchor = View(context).apply {
+ setViewTreeLifecycleOwner(mock())
+ this.setViewTreeSavedStateRegistryOwner(mock())
+ }
+ val popupView = spy(CFRPopupFullscreenLayout(anchor, mock(), mock(), { }, { }))
+ popupView.show()
+
+ testContext.resources.configuration.orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ popupView.orientationChangeListener.onDisplayChanged(1)
+
+ advanceTimeBy(SHOW_AFTER_SCREEN_ORIENTATION_CHANGE_DELAY)
+ verify(popupView, times(1)).dismiss()
+ verify(popupView, times(1)).show()
+ // Test that show() is called the second time after exactly the expected delay.
+ advanceTimeBy(1)
+ verify(popupView, times(2)).show()
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt
new file mode 100644
index 0000000000..5f8a0874e0
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt
@@ -0,0 +1,136 @@
+/* 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.compose.cfr.helper
+
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.hardware.display.DisplayManager
+import android.os.Build
+import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+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.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(sdk = [Build.VERSION_CODES.N])
+class DisplayOrientationListenerTest {
+ private val context: Context = mock()
+ private val displayManager: DisplayManager = mock()
+
+ @Before
+ fun setup() {
+ doReturn(displayManager).`when`(context).getSystemService(Context.DISPLAY_SERVICE)
+
+ val display: Display = mock()
+ doReturn(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT).`when`(display).rotation
+ doReturn(display).`when`(displayManager).getDisplay(0)
+ }
+
+ @Test
+ fun `WHEN started THEN register it as a display listener`() {
+ val listener = DisplayOrientationListener(context) { }
+
+ listener.start()
+
+ verify(displayManager).registerDisplayListener(listener, null)
+ }
+
+ @Test
+ fun `WHEN stopped THEN unregister from being a display listener`() {
+ val listener = DisplayOrientationListener(context) { }
+
+ listener.stop()
+
+ verify(displayManager).unregisterDisplayListener(listener)
+ }
+
+ @Test
+ fun `WHEN a display is added THEN don't inform the client`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+
+ listener.onDisplayAdded(1)
+
+ assertFalse(hasRotationChanged)
+ }
+
+ @Test
+ fun `WHEN a display is removed THEN don't inform the client`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+
+ listener.onDisplayRemoved(1)
+
+ assertFalse(hasRotationChanged)
+ }
+
+ @Test
+ fun `GIVEN display is null WHEN a display is changed THEN don't inform the client`() {
+ val onDisplayRotationChanged = mock<() -> Unit>()
+ val listener = DisplayOrientationListener(context, onDisplayRotationChanged)
+ doReturn(null).`when`(displayManager).getDisplay(1)
+
+ listener.onDisplayChanged(1)
+
+ verifyNoInteractions(onDisplayRotationChanged)
+ }
+
+ @Test
+ fun `WHEN a display is changed but doesn't have a new rotation THEN don't inform the client`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+ val display: Display = mock()
+ doReturn(listener.currentOrientation).`when`(display).rotation
+ doReturn(display).`when`(displayManager).getDisplay(1)
+
+ listener.onDisplayChanged(1)
+
+ assertFalse(hasRotationChanged)
+ }
+
+ @Test
+ fun `GIVEN an old Android version WHEN a display is changed and has a new rotation THEN inform the client and remember the new rotation`() {
+ var hasRotationChanged = false
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+ val display: Display = mock()
+ doReturn(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE).`when`(display).rotation
+ doReturn(display).`when`(displayManager).getDisplay(1)
+
+ listener.onDisplayChanged(1)
+
+ assertTrue(hasRotationChanged)
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, listener.currentOrientation)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.S])
+ fun `GIVEN a new Android version WHEN a display is changed and has a new rotation THEN inform the client and remember the new rotation`() {
+ var hasRotationChanged = false
+ val config = Configuration().apply {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ }
+ val resources: Resources = mock()
+ doReturn(config).`when`(resources).configuration
+ doReturn(resources).`when`(context).resources
+ val listener = DisplayOrientationListener(context) { hasRotationChanged = true }
+
+ config.orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
+ listener.onDisplayChanged(1)
+
+ assertTrue(hasRotationChanged)
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE, listener.currentOrientation)
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt
new file mode 100644
index 0000000000..bb6467f44c
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.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.compose.cfr.helper
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ViewDetachedListenerTest {
+ @Test
+ fun `WHEN the View is attached THEN don't inform the client`() {
+ var wasCallbackCalled = false
+ val listener = ViewDetachedListener { wasCallbackCalled = true }
+
+ listener.onViewAttachedToWindow(mock())
+
+ assertFalse(wasCallbackCalled)
+ }
+
+ @Test
+ fun `WHEN the View is detached THEN don't inform the client`() {
+ var wasCallbackCalled = false
+ val listener = ViewDetachedListener { wasCallbackCalled = true }
+
+ listener.onViewDetachedFromWindow(mock())
+
+ assertTrue(wasCallbackCalled)
+ }
+}
diff --git a/mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,2 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)