From d8bbc7858622b6d9c278469aab701ca0b609cddf Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:35:49 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../components/compose/cfr/README.md | 49 ++ .../components/compose/cfr/build.gradle | 63 +++ .../components/compose/cfr/proguard-rules.pro | 21 + .../compose/cfr/src/main/AndroidManifest.xml | 4 + .../mozilla/components/compose/cfr/CFRPopup.kt | 195 +++++++ .../components/compose/cfr/CFRPopupContent.kt | 186 +++++++ .../compose/cfr/CFRPopupFullscreenLayout.kt | 537 ++++++++++++++++++++ .../components/compose/cfr/CFRPopupShape.kt | 272 ++++++++++ .../cfr/helper/DisplayOrientationListener.kt | 65 +++ .../compose/cfr/helper/ViewDetachedListener.kt | 19 + .../compose/cfr/src/main/res/values-am/strings.xml | 5 + .../cfr/src/main/res/values-ast/strings.xml | 5 + .../cfr/src/main/res/values-azb/strings.xml | 5 + .../compose/cfr/src/main/res/values-be/strings.xml | 5 + .../compose/cfr/src/main/res/values-bg/strings.xml | 5 + .../compose/cfr/src/main/res/values-br/strings.xml | 5 + .../compose/cfr/src/main/res/values-bs/strings.xml | 5 + .../compose/cfr/src/main/res/values-ca/strings.xml | 5 + .../cfr/src/main/res/values-cak/strings.xml | 5 + .../cfr/src/main/res/values-ckb/strings.xml | 5 + .../compose/cfr/src/main/res/values-co/strings.xml | 5 + .../compose/cfr/src/main/res/values-cs/strings.xml | 5 + .../compose/cfr/src/main/res/values-cy/strings.xml | 5 + .../compose/cfr/src/main/res/values-da/strings.xml | 5 + .../compose/cfr/src/main/res/values-de/strings.xml | 5 + .../cfr/src/main/res/values-dsb/strings.xml | 5 + .../compose/cfr/src/main/res/values-el/strings.xml | 5 + .../cfr/src/main/res/values-en-rCA/strings.xml | 5 + .../cfr/src/main/res/values-en-rGB/strings.xml | 5 + .../compose/cfr/src/main/res/values-eo/strings.xml | 5 + .../cfr/src/main/res/values-es-rAR/strings.xml | 5 + .../cfr/src/main/res/values-es-rCL/strings.xml | 5 + .../cfr/src/main/res/values-es-rES/strings.xml | 5 + .../cfr/src/main/res/values-es-rMX/strings.xml | 5 + .../compose/cfr/src/main/res/values-es/strings.xml | 5 + .../compose/cfr/src/main/res/values-et/strings.xml | 5 + .../compose/cfr/src/main/res/values-eu/strings.xml | 5 + .../compose/cfr/src/main/res/values-fa/strings.xml | 5 + .../compose/cfr/src/main/res/values-fi/strings.xml | 5 + .../compose/cfr/src/main/res/values-fr/strings.xml | 5 + .../cfr/src/main/res/values-fur/strings.xml | 5 + .../cfr/src/main/res/values-fy-rNL/strings.xml | 5 + .../compose/cfr/src/main/res/values-gd/strings.xml | 5 + .../compose/cfr/src/main/res/values-gl/strings.xml | 5 + .../compose/cfr/src/main/res/values-gn/strings.xml | 5 + .../compose/cfr/src/main/res/values-hr/strings.xml | 5 + .../cfr/src/main/res/values-hsb/strings.xml | 5 + .../compose/cfr/src/main/res/values-hu/strings.xml | 5 + .../cfr/src/main/res/values-hy-rAM/strings.xml | 5 + .../compose/cfr/src/main/res/values-ia/strings.xml | 5 + .../compose/cfr/src/main/res/values-in/strings.xml | 5 + .../compose/cfr/src/main/res/values-is/strings.xml | 5 + .../compose/cfr/src/main/res/values-it/strings.xml | 5 + .../compose/cfr/src/main/res/values-iw/strings.xml | 5 + .../compose/cfr/src/main/res/values-ja/strings.xml | 5 + .../compose/cfr/src/main/res/values-ka/strings.xml | 5 + .../cfr/src/main/res/values-kaa/strings.xml | 5 + .../cfr/src/main/res/values-kab/strings.xml | 5 + .../compose/cfr/src/main/res/values-kk/strings.xml | 5 + .../cfr/src/main/res/values-kmr/strings.xml | 5 + .../compose/cfr/src/main/res/values-ko/strings.xml | 5 + .../compose/cfr/src/main/res/values-lo/strings.xml | 5 + .../compose/cfr/src/main/res/values-lt/strings.xml | 5 + .../cfr/src/main/res/values-nb-rNO/strings.xml | 5 + .../compose/cfr/src/main/res/values-nl/strings.xml | 5 + .../cfr/src/main/res/values-nn-rNO/strings.xml | 5 + .../compose/cfr/src/main/res/values-oc/strings.xml | 5 + .../cfr/src/main/res/values-pa-rIN/strings.xml | 5 + .../cfr/src/main/res/values-pa-rPK/strings.xml | 5 + .../compose/cfr/src/main/res/values-pl/strings.xml | 5 + .../cfr/src/main/res/values-pt-rBR/strings.xml | 5 + .../cfr/src/main/res/values-pt-rPT/strings.xml | 5 + .../compose/cfr/src/main/res/values-rm/strings.xml | 5 + .../compose/cfr/src/main/res/values-ru/strings.xml | 5 + .../cfr/src/main/res/values-sat/strings.xml | 5 + .../compose/cfr/src/main/res/values-sc/strings.xml | 5 + .../compose/cfr/src/main/res/values-si/strings.xml | 5 + .../compose/cfr/src/main/res/values-sk/strings.xml | 5 + .../cfr/src/main/res/values-skr/strings.xml | 5 + .../compose/cfr/src/main/res/values-sl/strings.xml | 5 + .../compose/cfr/src/main/res/values-sq/strings.xml | 5 + .../compose/cfr/src/main/res/values-sr/strings.xml | 5 + .../compose/cfr/src/main/res/values-su/strings.xml | 5 + .../cfr/src/main/res/values-sv-rSE/strings.xml | 5 + .../cfr/src/main/res/values-szl/strings.xml | 5 + .../compose/cfr/src/main/res/values-tg/strings.xml | 5 + .../compose/cfr/src/main/res/values-th/strings.xml | 5 + .../compose/cfr/src/main/res/values-tr/strings.xml | 5 + .../cfr/src/main/res/values-trs/strings.xml | 5 + .../compose/cfr/src/main/res/values-tt/strings.xml | 5 + .../compose/cfr/src/main/res/values-ug/strings.xml | 5 + .../compose/cfr/src/main/res/values-uk/strings.xml | 5 + .../compose/cfr/src/main/res/values-uz/strings.xml | 5 + .../cfr/src/main/res/values-vec/strings.xml | 5 + .../compose/cfr/src/main/res/values-vi/strings.xml | 5 + .../cfr/src/main/res/values-zh-rCN/strings.xml | 5 + .../cfr/src/main/res/values-zh-rTW/strings.xml | 5 + .../compose/cfr/src/main/res/values/strings.xml | 8 + .../compose/cfr/CFRPopupFullscreenLayoutTest.kt | 561 +++++++++++++++++++++ .../cfr/helper/DisplayOrientationListenerTest.kt | 136 +++++ .../compose/cfr/helper/ViewDetachedListenerTest.kt | 32 ++ .../org.mockito.plugins.MockMaker | 2 + 102 files changed, 2585 insertions(+) create mode 100644 mobile/android/android-components/components/compose/cfr/README.md create mode 100644 mobile/android/android-components/components/compose/cfr/build.gradle create mode 100644 mobile/android/android-components/components/compose/cfr/proguard-rules.pro create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopup.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupContent.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayout.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/CFRPopupShape.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/DisplayOrientationListener.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/java/mozilla/components/compose/cfr/helper/ViewDetachedListener.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-am/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-azb/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-be/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-br/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ckb/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-co/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-da/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-de/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-el/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-es/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-et/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-gd/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-gn/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-in/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-is/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-it/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-kmr/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-pa-rPK/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-pt-rPT/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-rm/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-sat/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-sc/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-si/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-su/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-szl/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-th/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-ug/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-uz/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-vec/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/CFRPopupFullscreenLayoutTest.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/DisplayOrientationListenerTest.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/test/java/mozilla/components/compose/cfr/helper/ViewDetachedListenerTest.kt create mode 100644 mobile/android/android-components/components/compose/cfr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker (limited to 'mobile/android/android-components/components/compose/cfr') 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 = , + 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 = { }, + text = { + Text( + text = stringResource(R.string.string1), + style = MaterialTheme.typography.body2, + ) + }, + action = { + Button(onClick = { }) { + 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 @@ + + 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 = 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? = 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, + 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 @@ + + + + አሰናብት + 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 @@ + + + + Escartar + 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 @@ + + + + باغلا + 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 @@ + + + + Адхіліць + 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 @@ + + + + Прекратяване + 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 @@ + + + + Argas + 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 @@ + + + + Odbaci + 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 @@ + + + + Descarta + 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 @@ + + + + Tichup ruwäch + 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 @@ + + + + پشتگوێخستن + 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 @@ + + + + Ricusà + 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 @@ + + + + Zavřít + 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 @@ + + + + Cau + 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 @@ + + + + Afvis + 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 @@ + + + + Schließen + 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 @@ + + + + Zachyśiś + 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 @@ + + + + Απόρριψη + 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 @@ + + + + Dismiss + 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 @@ + + + + Dismiss + 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 @@ + + + + Ignori + 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 @@ + + + + Descartar + 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 @@ + + + + Ocultar + 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 @@ + + + + Descartar + 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 @@ + + + + Descartar + 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 @@ + + + + Descartar + 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 @@ + + + + Peida + 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 @@ + + + + Baztertu + 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 @@ + + + + رد کردن + 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 @@ + + + + Hylkää + 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 @@ + + + + Fermer + 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 @@ + + + + Siere + 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 @@ + + + + Slute + 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 @@ + + + + Leig seachad + 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 @@ + + + + Rexeitar + 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 @@ + + + + Mboyke + 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 @@ + + + + Odbaci + 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 @@ + + + + Zaćisnyć + 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 @@ + + + + Eltüntetés + 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 @@ + + + + Բաց թողնել + 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 @@ + + + + Dimitter + 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 @@ + + + + Tutup + 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 @@ + + + + Hafna + 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 @@ + + + + Chiudi + 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 @@ + + + + סגירה + 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 @@ + + + + 閉じる + 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 @@ + + + + არიდება + 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 @@ + + + + Jabıw + 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 @@ + + + + Zgel + 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 @@ + + + + Тайдыру + 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 @@ + + + + Bigire + 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 @@ + + + + 닫기 + 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 @@ + + + + ປິດ + 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 @@ + + + + Paslėpti + 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 @@ + + + + Ignorer + 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 @@ + + + + Sluiten + 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 @@ + + + + Ignorer + 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 @@ + + + + Ignorar + 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 @@ + + + + ਖਾਰਜ ਕਰੋ + 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 @@ + + + + بند کرو + 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 @@ + + + + Zamknij + 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 @@ + + + + Descartar + 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 @@ + + + + Dispensar + 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 @@ + + + + Serrar + 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 @@ + + + + Скрыть + 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 @@ + + + + ᱵᱚᱱᱫ + 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 @@ + + + + Iscarta + 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 @@ + + + + ඉවතලන්න + 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 @@ + + + + Zavrieť + 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 @@ + + + + فارغ کرو + 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 @@ + + + + Zapri + 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 @@ + + + + Hidhe tej + 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 @@ + + + + Одбаци + 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 @@ + + + + Tutup + 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 @@ + + + + Ignorera + 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 @@ + + + + Ôdkoż + 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 @@ + + + + Нодида гузарондан + 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 @@ + + + + ปิด + 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 @@ + + + + Kapat + 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 @@ + + + + Si gi\'hiaj guendô\' + 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 @@ + + + + Яшерү + 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 @@ + + + + بولدىلا + 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 @@ + + + + Відхилити + 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 @@ + + + + Rad qilish + 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 @@ + + + + Sara + 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 @@ + + + + Bỏ qua + 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 @@ + + + + 知道了 + 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 @@ + + + + 知道了! + 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 @@ + + + + + Dismiss + 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() + + 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) -- cgit v1.2.3