summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/support/ktx/src
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/android-components/components/support/ktx/src')
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml11
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt12
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt31
-rw-r--r--mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt57
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml11
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt13
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt344
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt75
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt193
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt23
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt20
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt102
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt23
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt80
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt194
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt49
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt57
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt98
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt38
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt20
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt28
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt12
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt36
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt47
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt95
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt19
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt41
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt187
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt46
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt33
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt26
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt107
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt14
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt26
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt442
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt40
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt98
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt104
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml7
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml5
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml9
-rw-r--r--mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml15
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt26
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt55
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt304
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt115
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt229
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt49
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt78
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt74
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt78
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt243
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt137
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt131
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt66
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt64
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt45
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt21
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt27
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt72
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt143
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt57
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt98
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt221
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt135
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt142
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt40
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt29
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt70
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt595
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt38
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt90
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt72
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt94
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt51
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties1
183 files changed, 7518 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml b/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..d0418bdaa1
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <application>
+ <activity
+ android:name="mozilla.components.support.ktx.TestActivity"
+ android:exported="false" />
+ </application>
+
+</manifest>
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt
new file mode 100644
index 0000000000..ad3d123c10
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/TestActivity.kt
@@ -0,0 +1,12 @@
+/* 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.support.ktx
+
+import android.app.Activity
+
+/**
+ * Empty activity only to be used in UI tests.
+ */
+internal class TestActivity : Activity()
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt
new file mode 100644
index 0000000000..f0ec9bf2d3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/net/OnDeviceUriKtTest.kt
@@ -0,0 +1,31 @@
+/* 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.support.ktx.android.net
+
+import android.content.Context
+import androidx.core.net.toUri
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class OnDeviceUriKtTest {
+ private val context: Context
+ get() = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun isUnderPrivateAppDirectory() {
+ var uri = "file:///data/user/0/${context.packageName}/any_directory/file.text".toUri()
+
+ assertTrue(uri.isUnderPrivateAppDirectory(context))
+
+ uri = "file:///data/data/${context.packageName}/any_directory/file.text".toUri()
+
+ assertTrue(uri.isUnderPrivateAppDirectory(context))
+
+ uri = "file:///data/directory/${context.packageName}/any_directory/file.text".toUri()
+
+ assertFalse(uri.isUnderPrivateAppDirectory(context))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt
new file mode 100644
index 0000000000..93b12d237e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/androidTest/java/mozilla/components/support/ktx/android/view/WindowKtTest.kt
@@ -0,0 +1,57 @@
+/* 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.support.ktx.android.view
+
+import android.graphics.Color
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SdkSuppress
+import mozilla.components.support.ktx.TestActivity
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+
+class WindowKtTest {
+ @get:Rule
+ internal val activityRule: ActivityScenarioRule<TestActivity> = ActivityScenarioRule(TestActivity::class.java)
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ fun whenALightColorIsAppliedToStatusBarThenSetIsAppearanceLightStatusBarsToTrue() {
+ activityRule.scenario.onActivity {
+ it.window.setStatusBarTheme(Color.WHITE)
+
+ assertTrue(it.window.createWindowInsetsController().isAppearanceLightStatusBars)
+ }
+ }
+
+ @Test
+ fun whenADarkColorIsAppliedToStatusBarThenSetIsAppearanceLightStatusBarsToFalse() {
+ activityRule.scenario.onActivity {
+ it.window.setStatusBarTheme(Color.BLACK)
+
+ assertFalse(it.window.createWindowInsetsController().isAppearanceLightStatusBars)
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 23)
+ @Test
+ fun whenALightColorIsAppliedToNavigationBarThemeThenSetIsAppearanceLightNavigationBarsToTrue() {
+ activityRule.scenario.onActivity {
+ it.window.setNavigationBarTheme(Color.WHITE)
+
+ assertTrue(it.window.createWindowInsetsController().isAppearanceLightNavigationBars)
+ }
+ }
+
+ @Test
+ fun whenADarkColorIsAppliedToNavigationBarThemeThenSetIsAppearanceLightNavigationBarsToFalse() {
+ activityRule.scenario.onActivity {
+ it.window.setNavigationBarTheme(Color.BLACK)
+
+ assertFalse(it.window.createWindowInsetsController().isAppearanceLightNavigationBars)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..c93a4ab93a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.SEND" />
+ <data android:mimeType="text/plain" />
+ </intent>
+ </queries>
+</manifest>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt
new file mode 100644
index 0000000000..5123683117
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/arch/lifecycle/Lifecycle.kt
@@ -0,0 +1,13 @@
+/* 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.support.ktx.android.arch.lifecycle
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+
+/**
+ * Calls [Lifecycle.addObserver] for a variable list of [LifecycleObserver]s.
+ */
+fun Lifecycle.addObservers(vararg observers: LifecycleObserver) = observers.forEach { addObserver(it) }
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt
new file mode 100644
index 0000000000..e1e4dd7299
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Context.kt
@@ -0,0 +1,344 @@
+/* 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.support.ktx.android.content
+
+import android.app.ActivityManager
+import android.content.ActivityNotFoundException
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_DIAL
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SENDTO
+import android.content.Intent.EXTRA_EMAIL
+import android.content.Intent.EXTRA_STREAM
+import android.content.Intent.EXTRA_SUBJECT
+import android.content.Intent.EXTRA_TEXT
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.hardware.camera2.CameraManager
+import android.net.Uri
+import android.os.Build
+import android.os.Process
+import android.provider.ContactsContract
+import android.view.accessibility.AccessibilityManager
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.checkSelfPermission
+import androidx.core.content.FileProvider
+import androidx.core.content.getSystemService
+import mozilla.components.support.base.log.Log
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.R
+import mozilla.components.support.ktx.android.content.res.resolveAttribute
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+import java.io.File
+
+/**
+ * The (visible) version name of the application, as specified by the <manifest> tag's versionName
+ * attribute. E.g. "2.0".
+ */
+val Context.appVersionName: String
+ get() = packageManager.getPackageInfoCompat(packageName, 0).versionName
+
+/**
+ * Returns the name (label) of the application or the package name as a fallback.
+ */
+val Context.appName: String
+ get() = packageManager.getApplicationLabel(applicationInfo).toString()
+
+/**
+ * Returns whether or not the operating system is under low memory conditions.
+ */
+fun Context.isOSOnLowMemory(): Boolean {
+ val activityManager: ActivityManager = getSystemService()!!
+ return ActivityManager.MemoryInfo().also { memoryInfo ->
+ activityManager.getMemoryInfo(memoryInfo)
+ }.lowMemory
+}
+
+/**
+ * Returns if a list of permission have been granted, if all the permission have been granted
+ * returns true otherwise false.
+ */
+fun Context.isPermissionGranted(permission: Iterable<String>): Boolean {
+ return permission.all { checkSelfPermission(this, it) == PERMISSION_GRANTED }
+}
+
+fun Context.isPermissionGranted(vararg permission: String): Boolean {
+ return isPermissionGranted(permission.asIterable())
+}
+
+/**
+ * Checks whether or not the device has a camera.
+ *
+ * @return true if a camera was found, otherwise false.
+ */
+@Suppress("TooGenericExceptionCaught")
+fun Context.hasCamera(): Boolean {
+ return try {
+ val cameraManager: CameraManager? = getSystemService()
+ cameraManager?.cameraIdList?.isNotEmpty() ?: false
+ } catch (_: Throwable) {
+ false
+ }
+}
+
+/**
+ * Shares content via [ACTION_SEND] intent.
+ *
+ * @param text the data to be shared [EXTRA_TEXT]
+ * @param subject of the intent [EXTRA_TEXT]
+ * @return true it is able to share false otherwise.
+ */
+fun Context.share(text: String, subject: String = getString(R.string.mozac_support_ktx_share_dialog_title)): Boolean {
+ return try {
+ val intent = Intent(ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(EXTRA_SUBJECT, subject)
+ putExtra(EXTRA_TEXT, text)
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+
+ startActivity(
+ intent.createChooserExcludingCurrentApp(
+ this,
+ getString(R.string.mozac_support_ktx_menu_share_with),
+ ),
+ )
+ true
+ } catch (e: ActivityNotFoundException) {
+ Log.log(Log.Priority.WARN, message = "No activity to share to found", throwable = e, tag = "Reference-Browser")
+ false
+ }
+}
+
+/**
+ * Shares content via [ACTION_SEND] intent.
+ *
+ * @param filePath Path of the copied file.
+ * @param contentType Content type (MIME type) to indicate the media type of the resource.
+ * @param subject of the intent [EXTRA_SUBJECT]
+ * @param message of the intent [EXTRA_TEXT]
+ *
+ * @return true it is able to share false otherwise.
+ */
+fun Context.shareMedia(
+ filePath: String,
+ contentType: String?,
+ subject: String? = null,
+ message: String? = null,
+): Boolean {
+ val contentUri = getContentUriForFile(filePath)
+
+ val intent = Intent().apply {
+ action = ACTION_SEND
+ type = contentType ?: contentResolver.getType(contentUri)
+ flags = FLAG_ACTIVITY_NEW_DOCUMENT or FLAG_GRANT_READ_URI_PERMISSION
+ putExtra(EXTRA_STREAM, contentUri)
+ if (subject != null) {
+ putExtra(EXTRA_SUBJECT, subject)
+ }
+ if (message != null) {
+ putExtra(EXTRA_TEXT, message)
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android Q allows us to show a thumbnail preview of the file to be shared.
+ clipData = ClipData.newRawUri(contentUri.toString(), contentUri)
+ }
+ }
+
+ val shareIntent = Intent.createChooser(intent, getString(R.string.mozac_support_ktx_menu_share_with)).apply {
+ flags = FLAG_ACTIVITY_NEW_TASK or FLAG_GRANT_READ_URI_PERMISSION
+ }
+
+ return try {
+ startActivity(shareIntent)
+ true
+ } catch (error: ActivityNotFoundException) {
+ Log.log(Log.Priority.WARN, message = "No activity to share to found", throwable = error, tag = "shareMedia")
+
+ false
+ }
+}
+
+/**
+ * Creates a content URI for the given [filePath] to add to the device clipboard and maybe displays
+ * confirmation feedback.
+ *
+ * @param filePath Path of the copied file.
+ * @param onCopyConfirmation The confirmation action of copying an image.
+ */
+fun Context.copyImage(
+ filePath: String,
+ onCopyConfirmation: () -> Unit,
+) {
+ val contentUri = getContentUriForFile(filePath)
+
+ val clipData = ClipData.newUri(contentResolver, "Copied media URI", contentUri)
+ getClipboardManager().setPrimaryClip(clipData)
+
+ onCopyConfirmation.invoke()
+}
+
+private fun Context.getContentUriForFile(filePath: String) = FileProvider.getUriForFile(
+ this,
+ "${applicationContext.packageName}.feature.downloads.fileprovider", // (packageName + FILE_PROVIDER_EXTENSION)
+ File(filePath),
+)
+
+private fun Context.getClipboardManager() =
+ getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+/**
+ * Emails content via [ACTION_SENDTO] intent.
+ *
+ * @param address the email address to send to [EXTRA_EMAIL]
+ * @param subject of the intent [EXTRA_TEXT]
+ * @return true it is able to share email false otherwise.
+ */
+fun Context.email(
+ address: String,
+ subject: String = getString(R.string.mozac_support_ktx_share_dialog_title),
+): Boolean {
+ return try {
+ val intent = Intent(ACTION_SENDTO, Uri.parse("mailto:$address"))
+ intent.putExtra(EXTRA_SUBJECT, subject)
+
+ val emailIntent = Intent.createChooser(
+ intent,
+ getString(R.string.mozac_support_ktx_menu_email_with),
+ ).apply {
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+
+ startActivity(emailIntent)
+ true
+ } catch (e: ActivityNotFoundException) {
+ Logger.warn("No activity found to handle email intent", throwable = e)
+ false
+ }
+}
+
+/**
+ * Calls phone number via [ACTION_DIAL] intent.
+ *
+ * Note: we purposely use ACTION_DIAL rather than ACTION_CALL as the latter requires user permission
+ * @param phoneNumber the phone number to send to [ACTION_DIAL]
+ * @param subject of the intent [EXTRA_TEXT]
+ * @return true it is able to share phone call false otherwise.
+ */
+fun Context.call(
+ phoneNumber: String,
+ subject: String = getString(R.string.mozac_support_ktx_share_dialog_title),
+): Boolean {
+ return try {
+ val intent = Intent(ACTION_DIAL, Uri.parse("tel:$phoneNumber"))
+ intent.putExtra(EXTRA_SUBJECT, subject)
+
+ val callIntent = Intent.createChooser(
+ intent,
+ getString(R.string.mozac_support_ktx_menu_call_with),
+ ).apply {
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+
+ startActivity(callIntent)
+ true
+ } catch (e: ActivityNotFoundException) {
+ Logger.warn("No activity found to handle dial intent", throwable = e)
+ false
+ }
+}
+
+/**
+ * Add to contact via [ContactsContract.Intents.Insert.ACTION]
+ *
+ * @param address the email address to add to [ContactsContract.Intents.Insert.EMAIL]
+ * @return true it is able to share email false otherwise.
+ */
+fun Context.addContact(
+ address: String,
+): Boolean {
+ return try {
+ val intent = Intent(ContactsContract.Intents.Insert.ACTION).apply {
+ type = ContactsContract.RawContacts.CONTENT_TYPE
+ putExtra(ContactsContract.Intents.Insert.EMAIL, address)
+ putExtra(
+ ContactsContract.Intents.Insert.EMAIL_TYPE,
+ ContactsContract.CommonDataKinds.Email.TYPE_WORK,
+ )
+ addFlags(FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ startActivity(intent)
+ true
+ } catch (e: ActivityNotFoundException) {
+ Logger.warn("No activity found to handle dial intent", throwable = e)
+ false
+ }
+}
+
+/**
+ * Check if TalkBack service is enabled.
+ *
+ * (via https://stackoverflow.com/a/12362545/512580)
+ */
+inline val Context.isScreenReaderEnabled: Boolean
+ get() = getSystemService<AccessibilityManager>()?.isTouchExplorationEnabled ?: false
+
+@VisibleForTesting
+internal var isMainProcess: Boolean? = null
+
+/**
+ * Returns true if we are running in the main process false otherwise.
+ */
+fun Context.isMainProcess(): Boolean {
+ if (isMainProcess != null) return isMainProcess as Boolean
+
+ val pid = Process.myPid()
+ val activityManager: ActivityManager? = getSystemService()
+
+ isMainProcess = activityManager?.runningAppProcesses.orEmpty().any { processInfo ->
+ processInfo.pid == pid && processInfo.processName == packageName
+ }
+
+ return isMainProcess as Boolean
+}
+
+/**
+ * Takes a function runs it only it if we are running in the main process, otherwise the function will not be executed.
+ * @param [block] function to be executed in the main process.
+ */
+inline fun Context.runOnlyInMainProcess(block: () -> Unit) {
+ if (isMainProcess()) {
+ block()
+ }
+}
+
+/**
+ * Returns the color int corresponding to the attribute.
+ */
+@ColorInt
+fun Context.getColorFromAttr(@AttrRes attr: Int) =
+ ContextCompat.getColor(this, theme.resolveAttribute(attr))
+
+/**
+ * Returns a tinted drawable for the given resource ID.
+ * @param resId ID of the drawable to load.
+ * @param tint Tint color int to apply to the drawable.
+ */
+fun Context.getDrawableWithTint(@DrawableRes resId: Int, @ColorInt tint: Int) =
+ AppCompatResources.getDrawable(this, resId)?.apply {
+ mutate()
+ setTint(tint)
+ }
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt
new file mode 100644
index 0000000000..041e105c33
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/Intent.kt
@@ -0,0 +1,75 @@
+/* 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.support.ktx.android.content
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.LabeledIntent
+import android.os.Build
+import android.os.Parcelable
+import mozilla.components.support.utils.ext.queryIntentActivitiesCompat
+
+/**
+ * Modify the current intent to be used in an intent chooser excluding the current app.
+ *
+ * @param context Android context used for various system interactions.
+ * @param title Title that will be displayed in the chooser.
+ *
+ * @return a new Intent object that you can hand to Context.startActivity() and related methods.
+ */
+fun Intent.createChooserExcludingCurrentApp(
+ context: Context,
+ title: CharSequence,
+): Intent {
+ val chooserIntent: Intent
+ val resolveInfos = context.packageManager.queryIntentActivitiesCompat(this, 0).toHashSet()
+
+ val excludedComponentNames = resolveInfos
+ .map { it.activityInfo }
+ .filter { it.packageName == context.packageName }
+ .map { ComponentName(it.packageName, it.name) }
+
+ // Starting with Android N we can use Intent.EXTRA_EXCLUDE_COMPONENTS to exclude components
+ // other way we are constrained to use Intent.EXTRA_INITIAL_INTENTS.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ chooserIntent = Intent.createChooser(this, title)
+ .putExtra(
+ Intent.EXTRA_EXCLUDE_COMPONENTS,
+ excludedComponentNames.toTypedArray(),
+ )
+ } else {
+ var targetIntents = resolveInfos
+ .filterNot { it.activityInfo.packageName == context.packageName }
+ .map { resolveInfo ->
+ val activityInfo = resolveInfo.activityInfo
+ val targetIntent = Intent(this).apply {
+ component = ComponentName(activityInfo.packageName, activityInfo.name)
+ }
+ LabeledIntent(
+ targetIntent,
+ activityInfo.packageName,
+ resolveInfo.labelRes,
+ resolveInfo.icon,
+ )
+ }
+
+ // Sometimes on Android M and below an empty chooser is displayed, problem reported also here
+ // https://issuetracker.google.com/issues/37085761
+ // To fix that we are creating a chooser with an empty intent
+ chooserIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ Intent.createChooser(Intent(), title)
+ } else {
+ targetIntents = targetIntents.toMutableList()
+ Intent.createChooser(targetIntents.removeAt(0), title)
+ }
+ chooserIntent.putExtra(
+ Intent.EXTRA_INITIAL_INTENTS,
+ targetIntents.toTypedArray<Parcelable>(),
+ )
+ }
+ chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ return chooserIntent
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt
new file mode 100644
index 0000000000..1661c188dd
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/SharedPreferences.kt
@@ -0,0 +1,193 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.support.ktx.android.content
+
+import android.content.SharedPreferences
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/**
+ * Represents a class that holds a reference to [SharedPreferences].
+ */
+interface PreferencesHolder {
+ val preferences: SharedPreferences
+}
+
+private class BooleanPreference(
+ private val key: String,
+ private val default: Boolean,
+) : ReadWriteProperty<PreferencesHolder, Boolean> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Boolean =
+ thisRef.preferences.getBoolean(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Boolean) =
+ thisRef.preferences.edit().putBoolean(key, value).apply()
+}
+
+private class FloatPreference(
+ private val key: String,
+ private val default: Float,
+) : ReadWriteProperty<PreferencesHolder, Float> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Float =
+ thisRef.preferences.getFloat(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Float) =
+ thisRef.preferences.edit().putFloat(key, value).apply()
+}
+
+private class IntPreference(
+ private val key: String,
+ private val default: Int,
+) : ReadWriteProperty<PreferencesHolder, Int> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Int =
+ thisRef.preferences.getInt(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Int) =
+ thisRef.preferences.edit().putInt(key, value).apply()
+}
+
+private class LongPreference(
+ private val key: String,
+ private val default: Long,
+) : ReadWriteProperty<PreferencesHolder, Long> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Long =
+ thisRef.preferences.getLong(key, default)
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Long) =
+ thisRef.preferences.edit().putLong(key, value).apply()
+}
+
+private class StringPreference(
+ private val key: String,
+ private val default: String,
+ private val persistDefaultIfNotExists: Boolean = false,
+) : ReadWriteProperty<PreferencesHolder, String> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): String {
+ return thisRef.preferences.getString(key, null) ?: run {
+ when (persistDefaultIfNotExists) {
+ true -> {
+ thisRef.preferences.edit().putString(key, default).apply()
+ thisRef.preferences.getString(key, null)!!
+ }
+ false -> default
+ }
+ }
+ }
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: String) =
+ thisRef.preferences.edit().putString(key, value).apply()
+}
+
+private class StringSetPreference(
+ private val key: String,
+ private val default: Set<String>,
+) : ReadWriteProperty<PreferencesHolder, Set<String>> {
+
+ override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Set<String> =
+ thisRef.preferences.getStringSet(key, default) ?: default
+
+ override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Set<String>) =
+ thisRef.preferences.edit().putStringSet(key, value).apply()
+}
+
+/**
+ * Property delegate for getting and setting a boolean shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * val isTelemetryOn by booleanPreference("telemetry", default = false)
+ * }
+ * ```
+ */
+fun booleanPreference(key: String, default: Boolean): ReadWriteProperty<PreferencesHolder, Boolean> =
+ BooleanPreference(key, default)
+
+/**
+ * Property delegate for getting and setting a float number shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var percentage by floatPreference("percentage", default = 0f)
+ * }
+ * ```
+ */
+fun floatPreference(key: String, default: Float): ReadWriteProperty<PreferencesHolder, Float> =
+ FloatPreference(key, default)
+
+/**
+ * Property delegate for getting and setting an int number shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var widgetNumInvocations by intPreference("widget_number_of_invocations", default = 0)
+ * }
+ * ```
+ */
+fun intPreference(key: String, default: Int): ReadWriteProperty<PreferencesHolder, Int> =
+ IntPreference(key, default)
+
+/**
+ * Property delegate for getting and setting a long number shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * val appInstanceId by longPreference("app_instance_id", default = 123456789L)
+ * }
+ * ```
+ */
+fun longPreference(key: String, default: Long): ReadWriteProperty<PreferencesHolder, Long> =
+ LongPreference(key, default)
+
+/**
+ * Property delegate for getting and setting a string shared preference.
+ * Optionally this will persist the default value if one is not already persisted.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var permissionsEnabledEnum by stringPreference(
+ * "permissions_enabled",
+ * default = "blocked",
+ * persistDefaultIfNotExists = true,
+ * )
+ * }
+ * ```
+ */
+fun stringPreference(
+ key: String,
+ default: String,
+ persistDefaultIfNotExists: Boolean = false,
+): ReadWriteProperty<PreferencesHolder, String> =
+ StringPreference(key, default, persistDefaultIfNotExists)
+
+/**
+ * Property delegate for getting and setting a string set shared preference.
+ *
+ * Example usage:
+ * ```
+ * class Settings : PreferenceHolder {
+ * ...
+ * var connectedDevices by stringSetPreference("connected_devices", default = emptySet())
+ * }
+ * ```
+ */
+fun stringSetPreference(key: String, default: Set<String>): ReadWriteProperty<PreferencesHolder, Set<String>> =
+ StringSetPreference(key, default)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt
new file mode 100644
index 0000000000..e9a8a9e140
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/pm/PackageManager.kt
@@ -0,0 +1,23 @@
+/* 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.support.ktx.android.content.pm
+
+import android.content.pm.PackageManager
+import mozilla.components.support.utils.ext.getPackageInfoCompat
+
+/**
+ * Check if a package is installed
+ *
+ * @param packageName The name of the package to check for.
+ */
+fun PackageManager.isPackageInstalled(packageName: String): Boolean {
+ return try {
+ // Turn off all the flags since we don't need the return value
+ getPackageInfoCompat(packageName, 0)
+ true
+ } catch (e: PackageManager.NameNotFoundException) {
+ false
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt
new file mode 100644
index 0000000000..72fa88eba9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/AssetManager.kt
@@ -0,0 +1,20 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.res.AssetManager
+import org.json.JSONObject
+
+/**
+ * Read a file from the "assets" and create a a JSONObject from its content.
+ *
+ * @param fileName The name of the asset to open. This name can be
+ * hierarchical.
+ */
+fun AssetManager.readJSONObject(fileName: String) = JSONObject(
+ open(fileName).bufferedReader().use {
+ it.readText()
+ },
+)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt
new file mode 100644
index 0000000000..3253c722be
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Resources.kt
@@ -0,0 +1,102 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.res.Resources
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.text.SpannableStringBuilder
+import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+import android.text.SpannedString
+import androidx.annotation.StringRes
+import java.util.Formatter
+import java.util.Locale
+
+/**
+ * Returns the primary locale according to the user's preference.
+ */
+val Resources.locale: Locale
+ get() = if (SDK_INT >= Build.VERSION_CODES.N) {
+ configuration.locales[0]
+ } else {
+ @Suppress("Deprecation")
+ configuration.locale
+ }
+
+/**
+ * Returns the character sequence associated with a given resource [id],
+ * substituting format arguments with additional styling spans.
+ *
+ * Credit to Michael Spitsin https://medium.com/@programmerr47/working-with-spans-in-android-ca4ab1327bc4
+ *
+ * @param id The desired resource identifier, corresponding to a string resource.
+ * @param spanParts The format arguments that will be used for substitution.
+ * The first element of each pair is the text to insert, similar to [String.format].
+ * The second element of each pair is a span that will be used to style the inserted string.
+ */
+@Suppress("SpreadOperator")
+fun Resources.getSpanned(
+ @StringRes id: Int,
+ vararg spanParts: Pair<Any, Any>,
+): SpannedString {
+ val builder = SpannableStringBuilder()
+ val formatArgs = spanParts.map { (text) -> text }.toTypedArray()
+ val formatter = Formatter(SpannableAppendable(builder, spanParts), locale)
+ formatter.format(getString(id), *formatArgs)
+ return SpannedString(builder)
+}
+
+/**
+ * [Appendable] implementation that wraps [SpannableStringBuilder]
+ * and inserts spans from the span parts array.
+ */
+private class SpannableAppendable(
+ private val builder: SpannableStringBuilder,
+ spanParts: Array<out Pair<Any, Any>>,
+) : Appendable {
+
+ /**
+ * Map of values from span parts, with keys converted to char sequences.
+ */
+ private val spansMap = spanParts
+ .toMap()
+ .mapKeys { (key) -> key.let { it as? CharSequence ?: it.toString() } }
+
+ override fun append(csq: CharSequence?) = apply { appendSmart(csq) }
+
+ override fun append(csq: CharSequence?, start: Int, end: Int) = apply {
+ if (csq != null) {
+ if (start in 0 until end && end <= csq.length) {
+ append(csq.subSequence(start, end))
+ } else {
+ throw IndexOutOfBoundsException("start " + start + ", end " + end + ", s.length() " + csq.length)
+ }
+ }
+ }
+
+ override fun append(c: Char) = apply { builder.append(c.toString()) }
+
+ /**
+ * Tries to find [csq] in the [spansMap] and use the corresponding span value.
+ * If [csq] is not found, the map is searched manually by converting values to strings.
+ * If no match is found afterwards, [csq] is appended with no corresponding span.
+ */
+ private fun appendSmart(csq: CharSequence?) {
+ if (csq != null) {
+ if (csq in spansMap) {
+ val span = spansMap.getValue(csq)
+ builder.append(csq, span, SPAN_EXCLUSIVE_EXCLUSIVE)
+ } else {
+ val possibleMatchDict = spansMap.filter { (text) -> text.toString() == csq }
+ if (possibleMatchDict.isNotEmpty()) {
+ val spanDictEntry = possibleMatchDict.entries.first()
+ builder.append(spanDictEntry.key, spanDictEntry.value, SPAN_EXCLUSIVE_EXCLUSIVE)
+ } else {
+ builder.append(csq)
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt
new file mode 100644
index 0000000000..4ad046a05d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/content/res/Theme.kt
@@ -0,0 +1,23 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.res.Resources
+import android.util.TypedValue
+import androidx.annotation.AnyRes
+import androidx.annotation.AttrRes
+
+/**
+ * Resolves the resource ID corresponding to the given attribute.
+ *
+ * @sample
+ * context.theme.resolveAttribute(R.attr.textColor) == R.color.light_text_color
+ */
+@AnyRes
+fun Resources.Theme.resolveAttribute(@AttrRes attribute: Int): Int {
+ val outValue = TypedValue()
+ resolveAttribute(attribute, outValue, true)
+ return outValue.resourceId
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt
new file mode 100644
index 0000000000..bac132e64e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/graphics/Bitmap.kt
@@ -0,0 +1,80 @@
+/* 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.support.ktx.android.graphics
+
+import android.graphics.Bitmap
+import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Shader.TileMode
+import android.util.Base64
+import androidx.annotation.CheckResult
+import java.io.ByteArrayOutputStream
+
+/**
+ * Transform bitmap into base64 encoded data uri (PNG).
+ */
+fun Bitmap.toDataUri(): String {
+ val stream = ByteArrayOutputStream()
+ compress(Bitmap.CompressFormat.PNG, BITMAP_COMPRESSION_QUALITY, stream)
+ val encodedImage = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT)
+ return "data:image/png;base64,$encodedImage"
+}
+
+private const val BITMAP_COMPRESSION_QUALITY = 100
+
+/**
+ * Returns a new bitmap that is the receiver Bitmap with four rounded corners;
+ * the receiver is unmodified.
+ *
+ * This operation is expensive: it requires allocating an identical Bitmap and copying
+ * all of the Bitmap's pixels. Consider these theoretically cheaper alternatives:
+ * - android:background= a drawable with rounded corners
+ * - Wrap your bitmap's ImageView with a layout that masks your view with rounded corners (e.g. CardView)
+ */
+@CheckResult
+fun Bitmap.withRoundedCorners(cornerRadiusPx: Float): Bitmap {
+ val roundedBitmap = Bitmap.createBitmap(width, height, config)
+ val canvas = Canvas(roundedBitmap)
+ val paint = Paint().apply {
+ isAntiAlias = true
+ shader = BitmapShader(this@withRoundedCorners, TileMode.CLAMP, TileMode.CLAMP)
+ }
+
+ canvas.drawRoundRect(
+ 0.0f,
+ 0.0f,
+ width.toFloat(),
+ height.toFloat(),
+ cornerRadiusPx,
+ cornerRadiusPx,
+ paint,
+ )
+ return roundedBitmap
+}
+
+/**
+ * Returns true if all pixels have the same value, false otherwise.
+ */
+fun Bitmap.arePixelsAllTheSame(): Boolean {
+ val testPixel = getPixel(0, 0)
+
+ // For perf, I expect iteration order is important. Under the hood, the pixels are represented
+ // by a single array: if you iterate along the buffer, you can take advantage of cache hits
+ // (since several words in memory are imported each time memory is accessed).
+ //
+ // We choose this iteration order (width first) because getPixels writes into a single array
+ // with index 1 being the same value as getPixel(1, 0) (i.e. it writes width first).
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val color = getPixel(x, y)
+ if (color != testPixel) {
+ return false
+ }
+ }
+ }
+
+ return true
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt
new file mode 100644
index 0000000000..e5d1df1175
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/net/Uri.kt
@@ -0,0 +1,194 @@
+/* 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.support.ktx.android.net
+
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.provider.OpenableColumns
+import android.webkit.MimeTypeMap
+import androidx.annotation.VisibleForTesting
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.kotlin.sanitizeFileName
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.util.UUID
+
+internal val commonPrefixes = listOf("www.", "mobile.", "m.")
+internal val mobileSubdomains = listOf("mobile", "m")
+
+/**
+ * Returns the host without common prefixes like "www" or "m".
+ */
+val Uri.hostWithoutCommonPrefixes: String?
+ get() {
+ val host = host ?: return null
+ for (prefix in commonPrefixes) {
+ if (host.startsWith(prefix)) return host.substring(prefix.length)
+ }
+ return host
+ }
+
+/**
+ * Checks that the Uri has the same host as [other], with mobile subdomains removed.
+ * @param other The Uri to be compared.
+ */
+fun Uri.sameHostWithoutMobileSubdomainAs(other: Uri): Boolean {
+ val thisHost = hostWithoutCommonPrefixes?.let {
+ it.split(".")
+ .filter { subdomain -> mobileSubdomains.none { mobileSubdomain -> mobileSubdomain == subdomain } }
+ } ?: return false
+ val otherHost = other.hostWithoutCommonPrefixes?.let {
+ it.split(".")
+ .filter { subdomain -> mobileSubdomains.none { mobileSubdomain -> mobileSubdomain == subdomain } }
+ } ?: return false
+ return thisHost == otherHost
+}
+
+/**
+ * Returns true if the [Uri] uses the "http" or "https" protocol scheme.
+ */
+val Uri.isHttpOrHttps: Boolean
+ get() = scheme == "http" || scheme == "https"
+
+/**
+ * Checks that the given URL is in one of the given URL [scopes].
+ *
+ * https://www.w3.org/TR/appmanifest/#dfn-within-scope
+ *
+ * @param scopes Uris that each represent a scope.
+ * A Uri is within the scope if the origin matches and it starts with the scope's path.
+ * @return True if this Uri is within any of the given scopes.
+ */
+fun Uri.isInScope(scopes: Iterable<Uri>): Boolean {
+ val path = path.orEmpty()
+ return scopes.any { scope ->
+ sameOriginAs(scope) && path.startsWith(scope.path.orEmpty())
+ }
+}
+
+/**
+ * Checks that Uri has the same scheme and host as [other].
+ */
+fun Uri.sameSchemeAndHostAs(other: Uri) = scheme == other.scheme && host == other.host
+
+/**
+ * Checks that Uri has the same origin as [other].
+ *
+ * https://html.spec.whatwg.org/multipage/origin.html#same-origin
+ */
+fun Uri.sameOriginAs(other: Uri) = sameSchemeAndHostAs(other) && port == other.port
+
+/**
+ * Indicate if the [this] uri is under the application private directory.
+ */
+fun Uri.isUnderPrivateAppDirectory(context: Context): Boolean {
+ return when (this.scheme) {
+ ContentResolver.SCHEME_FILE -> {
+ try {
+ val uriPath = path ?: return true
+ val uriCanonicalPath = File(uriPath).canonicalPath
+ val dataDirCanonicalPath = File(context.applicationInfo.dataDir).canonicalPath
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+ uriCanonicalPath.startsWith(dataDirCanonicalPath)
+ } else {
+ // We have to do this manual check on early builds of Android 11
+ // as symlink didn't resolve from /data/user/ to data/data
+ // we have to revise this again once Android 11 is out
+ // https://github.com/mozilla-mobile/android-components/issues/7750
+ uriCanonicalPath.startsWith("/data/data") || uriCanonicalPath.startsWith("/data/user")
+ }
+ } catch (e: IOException) {
+ true
+ }
+ }
+ else -> false
+ }
+}
+
+/**
+ * Return a file name for [this] give Uri.
+ * @return A file name for the content, or generated file name if the URL is invalid or the type is unknown
+ */
+fun Uri.getFileName(contentResolver: ContentResolver): String {
+ return when (this.scheme) {
+ ContentResolver.SCHEME_FILE -> File(path ?: "").name.sanitizeFileName()
+ ContentResolver.SCHEME_CONTENT -> getFileNameForContentUris(contentResolver)
+ else -> {
+ generateFileName(getFileExtension(contentResolver))
+ }
+ }
+}
+
+/**
+ * Return a file extension for [this] give Uri (only supports content:// schemes).
+ * @return A file extension for the content, or empty string if the URL is invalid or the type is unknown
+ */
+fun Uri.getFileExtension(contentResolver: ContentResolver): String {
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(contentResolver.getType(this)) ?: ""
+}
+
+/**
+ * Copy the content of [this] [Uri] into a temporary file in the given [dirToCopy]
+ * @return A "file://" [Uri] which contains the content of [this] [Uri].
+ */
+fun Uri.toFileUri(context: Context, dirToCopy: String = "/temps"): Uri {
+ val contentResolver = context.contentResolver
+ val cacheUploadDirectory = File(context.cacheDir, dirToCopy)
+
+ if (!cacheUploadDirectory.exists()) {
+ cacheUploadDirectory.mkdir()
+ }
+
+ val temporalFile = File(cacheUploadDirectory, getFileName(contentResolver))
+ try {
+ contentResolver.openInputStream(this)!!.use { inStream ->
+ copyFile(temporalFile, inStream)
+ }
+ } catch (e: IOException) {
+ Logger("Uri.kt").warn("Could not convert uri to file uri", e)
+ }
+ return Uri.parse("file:///${Uri.encode(temporalFile.absolutePath)}")
+}
+
+@VisibleForTesting
+internal fun copyFile(temporalFile: File, inStream: InputStream): Long {
+ return FileOutputStream(temporalFile).use { outStream ->
+ inStream.copyTo(outStream)
+ }
+}
+
+@VisibleForTesting
+internal fun Uri.getFileNameForContentUris(contentResolver: ContentResolver): String {
+ var fileName = ""
+ contentResolver.query(this, null, null, null, null)?.use { cursor ->
+ val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ val fileExtension = getFileExtension(contentResolver)
+ fileName = if (nameIndex == -1) {
+ generateFileName(fileExtension)
+ } else {
+ cursor.moveToFirst()
+ cursor.getString(nameIndex) ?: generateFileName(fileExtension)
+ }
+ }
+ return fileName.sanitizeFileName()
+}
+
+/**
+ * Generate a file name using a randomUUID + the current timestamp.
+ */
+@VisibleForTesting
+internal fun generateFileName(fileExtension: String = ""): String {
+ val randomId = UUID.randomUUID().toString().removePrefix("-").trim()
+ val timeStamp = System.currentTimeMillis()
+ return if (fileExtension.isNotEmpty()) {
+ "$randomId$timeStamp.$fileExtension"
+ } else {
+ "$randomId$timeStamp"
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt
new file mode 100644
index 0000000000..d4d7063fc0
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/notification/Notification.kt
@@ -0,0 +1,49 @@
+/* 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/. */
+
+@file:Suppress("MatchingDeclarationName")
+
+package mozilla.components.support.ktx.android.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.annotation.StringRes
+import androidx.core.content.getSystemService
+
+/**
+ * Make sure a notification channel exists.
+ * @param context A [Context], used for creating the notification channel.
+ * @param onSetupChannel A lambda in the context of the NotificationChannel that gives you the
+ * opportunity to apply any setup on the channel before gets created.
+ * @param onCreateChannel A lambda in the context of the NotificationManager that gives you the
+ * opportunity to perform any operation on the [NotificationManager].
+ * @return Returns the channel id to be used.
+ */
+fun ensureNotificationChannelExists(
+ context: Context,
+ channelDate: ChannelData,
+ onSetupChannel: NotificationChannel.() -> Unit = {},
+ onCreateChannel: NotificationManager.() -> Unit = {},
+): String {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationManager = requireNotNull(context.getSystemService<NotificationManager>())
+ val channel = NotificationChannel(
+ channelDate.id,
+ context.getString(channelDate.name),
+ channelDate.importance,
+ )
+ onSetupChannel(channel)
+ notificationManager.createNotificationChannel(channel)
+ onCreateChannel(notificationManager)
+ }
+
+ return channelDate.id
+}
+
+/**
+ * Wraps the data of a NotificationChannel as this class is available after API 26.
+ */
+class ChannelData(val id: String, @StringRes val name: Int, val importance: Int)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt
new file mode 100644
index 0000000000..de9608508e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONArray.kt
@@ -0,0 +1,57 @@
+/* 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.support.ktx.android.org.json
+
+import org.json.JSONArray
+import org.json.JSONException
+
+/**
+ * Convenience method to convert a JSONArray into a sequence.
+ *
+ * @param getter callback to get the value for an index in the array.
+ */
+inline fun <V> JSONArray.asSequence(crossinline getter: JSONArray.(Int) -> V): Sequence<V> {
+ val indexRange = 0 until length()
+ return indexRange.asSequence().map { i -> getter(i) }
+}
+
+fun JSONArray.asSequence(): Sequence<Any> = asSequence { i -> get(i) }
+
+/**
+ * Convenience method to convert a JSONArray into a List
+ *
+ * @return list with the JSONArray values, or an empty list if the JSONArray was null
+ */
+@Suppress("UNCHECKED_CAST")
+fun <T> JSONArray?.toList(): List<T> {
+ val array = this ?: return emptyList()
+ return array.asSequence().map { it as T }.toList()
+}
+
+/**
+ * Returns a list containing only the non-null results of applying the given [transform] function
+ * to each element in the original collection as returned by [getFromArray]. If [getFromArray]
+ * or [transform] throws a [JSONException], these elements will also be omitted.
+ *
+ * Here's an example call:
+ * ```kotlin
+ * jsonArray.mapNotNull(JSONArray::getJSONObject) { jsonObj -> jsonObj.getString("author") }
+ * ```
+ */
+inline fun <T, R : Any> JSONArray.mapNotNull(getFromArray: JSONArray.(index: Int) -> T, transform: (T) -> R?): List<R> {
+ val transformedResults = mutableListOf<R>()
+ for (i in 0 until this.length()) {
+ try {
+ val transformed = transform(getFromArray(i))
+ if (transformed != null) { transformedResults.add(transformed) }
+ } catch (e: JSONException) { /* Do nothing: we skip bad data. */ }
+ }
+
+ return transformedResults
+}
+
+fun Iterable<Any>.toJSONArray() = JSONArray().also { array ->
+ forEach { array.put(it) }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt
new file mode 100644
index 0000000000..a939afd2e6
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/org/json/JSONObject.kt
@@ -0,0 +1,98 @@
+/* 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.support.ktx.android.org.json
+
+import org.json.JSONObject
+import java.util.TreeMap
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGet(key: String): Any? = if (isNull(key)) null else get(key)
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGetString(key: String): String? = if (isNull(key)) null else getString(key)
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGetInt(key: String): Int? = if (isNull(key)) null else getInt(key)
+
+/**
+ * Returns the value mapped by {@code key} if it exists, and
+ * if the value returned is not null. If it's null, it returns null
+ */
+fun JSONObject.tryGetLong(key: String): Long? = if (isNull(key)) null else getLong(key)
+
+/**
+ * Puts the specified value under the key if it's not null
+ */
+fun JSONObject.putIfNotNull(key: String, value: Any?) {
+ if (value != null) {
+ put(key, value)
+ }
+}
+
+/**
+ * Sorts the keys of a JSONObject (and all of its child JSONObjects) alphabetically
+ */
+fun JSONObject.sortKeys(): JSONObject {
+ val map = TreeMap<String, Any>()
+ for (key in this.keys()) {
+ map[key] = this[key]
+ }
+ val jsonObject = JSONObject()
+ for (key in map.keys) {
+ if (map[key] is JSONObject) {
+ map[key] = (map[key] as JSONObject).sortKeys()
+ }
+ jsonObject.put(key, map[key])
+ }
+ return jsonObject
+}
+
+/**
+ * Convert a Map<String, String> to a JSONObject
+ */
+fun Map<String, String>.toJSON() = JSONObject().apply {
+ forEach { (key, value) -> put(key, value) }
+}
+
+/**
+ * Merge the contents of another [JSONObject] with this object,
+ * overwriting the colliding keys.
+ *
+ * @param other the [JSONObject] providing the data to be
+ * merged with this one.
+ */
+fun JSONObject.mergeWith(other: JSONObject) {
+ for (key in other.keys()) {
+ put(key, other[key])
+ }
+}
+
+/**
+ * Gets the [JSONObject] value with the given key if it exists.
+ * Otherwise calls the defaultValue function, adds its
+ * result to the object, and returns that.
+ *
+ * @param key the key to get or create.
+ * @param defaultValue a function returning a new default value
+ * @return the existing or new value
+ */
+fun JSONObject.getOrPutJSONObject(key: String, defaultValue: () -> JSONObject): JSONObject {
+ optJSONObject(key)?.let {
+ return it
+ } ?: run {
+ val value = defaultValue()
+ put(key, value)
+ return value
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt
new file mode 100644
index 0000000000..2bd0be117b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Bundle.kt
@@ -0,0 +1,38 @@
+/* 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.support.ktx.android.os
+
+import android.os.Bundle
+
+/**
+ * Returns `true` if the two specified bundles are *structurally* equal to one another,
+ * i.e. contain the same number of the same elements in the same order.
+ */
+@Suppress("ComplexMethod")
+infix fun Bundle.contentEquals(other: Bundle): Boolean {
+ if (size() != other.size()) return false
+
+ @Suppress("DEPRECATION") // we still need to use get(String) in order to compare any Objects.
+ return keySet().all { key ->
+ val valueTwo = other.get(key)
+ when (val valueOne = get(key)) {
+ // Compare bundles deeply
+ is Bundle -> valueTwo is Bundle && valueOne contentEquals valueTwo
+
+ // Compare arrays using contentEquals
+ is BooleanArray -> valueTwo is BooleanArray && valueOne contentEquals valueTwo
+ is ByteArray -> valueTwo is ByteArray && valueOne contentEquals valueTwo
+ is CharArray -> valueTwo is CharArray && valueOne contentEquals valueTwo
+ is DoubleArray -> valueTwo is DoubleArray && valueOne contentEquals valueTwo
+ is FloatArray -> valueTwo is FloatArray && valueOne contentEquals valueTwo
+ is IntArray -> valueTwo is IntArray && valueOne contentEquals valueTwo
+ is LongArray -> valueTwo is LongArray && valueOne contentEquals valueTwo
+ is ShortArray -> valueTwo is ShortArray && valueOne contentEquals valueTwo
+ is Array<*> -> valueTwo is Array<*> && valueOne contentEquals valueTwo
+
+ else -> valueOne == valueTwo
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt
new file mode 100644
index 0000000000..ffa8f6e3b5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/StrictMode.kt
@@ -0,0 +1,20 @@
+/* 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.support.ktx.android.os
+
+import android.os.StrictMode
+
+/**
+ * Runs the given [functionBlock] and sets the ThreadPolicy after its completion.
+ *
+ * This function is written in the style of [AutoCloseable.use].
+ *
+ * @return the value returned by [functionBlock].
+ */
+inline fun <R> StrictMode.ThreadPolicy.resetAfter(functionBlock: () -> R): R = try {
+ functionBlock()
+} finally {
+ StrictMode.setThreadPolicy(this)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt
new file mode 100644
index 0000000000..8c0cb9a271
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/os/Vibrator.kt
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.ktx.android.os
+
+import android.Manifest.permission.VIBRATE
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.VibrationEffect
+import android.os.VibrationEffect.DEFAULT_AMPLITUDE
+import android.os.Vibrator
+import androidx.annotation.RequiresPermission
+
+/**
+ * Vibrate constantly for the specified period of time.
+ *
+ * @param milliseconds The number of milliseconds to vibrate.
+ */
+@RequiresPermission(VIBRATE)
+fun Vibrator.vibrateOneShot(milliseconds: Long) {
+ if (SDK_INT >= Build.VERSION_CODES.O) {
+ vibrate(VibrationEffect.createOneShot(milliseconds, DEFAULT_AMPLITUDE))
+ } else {
+ @Suppress("Deprecation")
+ vibrate(milliseconds)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt
new file mode 100644
index 0000000000..ffea3eaf64
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/Base64.kt
@@ -0,0 +1,12 @@
+/* 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.support.ktx.android.util
+
+import android.util.Base64
+
+object Base64 {
+ fun encodeToUriString(data: String) =
+ "data:text/html;base64," + Base64.encodeToString(data.toByteArray(), Base64.DEFAULT)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt
new file mode 100644
index 0000000000..ecb54c7ea3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/DisplayMetrics.kt
@@ -0,0 +1,36 @@
+/* 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.support.ktx.android.util
+
+import android.util.DisplayMetrics
+import android.util.TypedValue
+
+/**
+ * Converts a value in density independent pixels (dp) to a float value.
+ */
+fun Int.dpToFloat(displayMetrics: DisplayMetrics) = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ this.toFloat(),
+ displayMetrics,
+)
+
+/**
+ * Converts a value in density independent pixels (dp) to the actual pixel values for the display.
+ */
+fun Int.dpToPx(displayMetrics: DisplayMetrics) = dpToFloat(displayMetrics).toInt()
+
+/** Converts a value in density independent pixels (dp) to a px value. */
+fun Float.dpToPx(displayMetrics: DisplayMetrics) = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ this,
+ displayMetrics,
+)
+
+/** Converts a value in scale independent pixels (sp) to a px value. */
+fun Float.spToPx(displayMetrics: DisplayMetrics) = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ this,
+ displayMetrics,
+)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt
new file mode 100644
index 0000000000..b1d29d000d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/util/JsonReader.kt
@@ -0,0 +1,47 @@
+/* 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.support.ktx.android.util
+
+import android.util.JsonReader
+import android.util.JsonToken
+
+/**
+ * Returns the [JsonToken.STRING] value of the next token or `null` if the next token
+ * is [JsonToken.NULL].
+ */
+fun JsonReader.nextStringOrNull(): String? {
+ return if (peek() == JsonToken.NULL) {
+ nextNull()
+ null
+ } else {
+ nextString()
+ }
+}
+
+/**
+ * Returns the [JsonToken.BOOLEAN] value of the next token or `null` if the next token
+ * is [JsonToken.NULL].
+ */
+fun JsonReader.nextBooleanOrNull(): Boolean? {
+ return if (peek() == JsonToken.NULL) {
+ nextNull()
+ null
+ } else {
+ nextBoolean()
+ }
+}
+
+/**
+ * Returns the [JsonToken.NUMBER] value of the next token or `null` if the next token
+ * is [JsonToken.NULL].
+ */
+fun JsonReader.nextIntOrNull(): Int? {
+ return if (peek() == JsonToken.NULL) {
+ nextNull()
+ null
+ } else {
+ nextInt()
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt
new file mode 100644
index 0000000000..283bd30720
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Activity.kt
@@ -0,0 +1,95 @@
+/* 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.support.ktx.android.view
+
+import android.app.Activity
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES
+import android.view.View
+import android.view.WindowManager
+import androidx.core.view.ViewCompat
+import androidx.core.view.ViewCompat.onApplyWindowInsets
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Attempts to enter immersive mode - fullscreen with the status bar and navigation buttons hidden,
+ * expanding itself into the notch area for devices running API 28+.
+ *
+ * This will automatically register and use an inset listener: [View.OnApplyWindowInsetsListener]
+ * to restore immersive mode if interactions with various other widgets like the keyboard or dialogs
+ * got the activity out of immersive mode without [exitImmersiveMode] being called.
+ */
+fun Activity.enterImmersiveMode(
+ insetsController: WindowInsetsControllerCompat = window.createWindowInsetsController(),
+) {
+ insetsController.hideInsets()
+
+ ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, insetsCompat ->
+ if (insetsCompat.isVisible(WindowInsetsCompat.Type.statusBars())) {
+ insetsController.hideInsets()
+ }
+ // Allow the decor view to have a chance to process the incoming WindowInsets.
+ onApplyWindowInsets(view, insetsCompat)
+ }
+
+ if (SDK_INT >= VERSION_CODES.P) {
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+ WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
+ )
+ window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ }
+}
+
+private fun WindowInsetsControllerCompat.hideInsets() {
+ apply {
+ hide(WindowInsetsCompat.Type.systemBars())
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+}
+
+/**
+ * Shows the system UI windows that were hidden, thereby exiting the immersive experience.
+ * For devices running API 28+, this function also restores the application's use
+ * of the notch area of the phone to the default behavior.
+ *
+ * @param insetsController is an optional [WindowInsetsControllerCompat] object for controlling the
+ * window insets.
+ */
+fun Activity.exitImmersiveMode(
+ insetsController: WindowInsetsControllerCompat = window.createWindowInsetsController(),
+) {
+ insetsController.show(WindowInsetsCompat.Type.systemBars())
+
+ ViewCompat.setOnApplyWindowInsetsListener(window.decorView, null)
+
+ if (SDK_INT >= VERSION_CODES.P) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ window.attributes.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ }
+}
+
+/**
+ * Calls [Activity.reportFullyDrawn] while also preventing crashes under some circumstances.
+ *
+ * @param errorLogger the logger to be used if errors are logged.
+ */
+fun Activity.reportFullyDrawnSafe(errorLogger: Logger) {
+ try {
+ reportFullyDrawn()
+ } catch (e: SecurityException) {
+ // This exception is throw on some Samsung devices. We were unable to identify the root
+ // cause but suspect it's related to Samsung security features. See
+ // https://github.com/mozilla-mobile/fenix/issues/12345#issuecomment-655058864 for details.
+ //
+ // We include "Fully drawn" in the log statement so that this error appears when grepping
+ // for fully drawn time.
+ errorLogger.error("Fully drawn - unable to call reportFullyDrawn", e)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.kt
new file mode 100644
index 0000000000..7861fc7e64
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/MotionEvent.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.support.ktx.android.view
+
+import android.view.MotionEvent
+
+/**
+ * Executes the given [functionBlock] function on this resource and then closes it down correctly whether
+ * an exception is thrown or not. This is inspired by [java.lang.AutoCloseable.use].
+ */
+inline fun <R> MotionEvent.use(functionBlock: (MotionEvent) -> R): R {
+ try {
+ return functionBlock(this)
+ } finally {
+ recycle()
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt
new file mode 100644
index 0000000000..0fec74272e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/TextView.kt
@@ -0,0 +1,41 @@
+/* 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/. */
+
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to other public APIs.
+
+package mozilla.components.support.ktx.android.view
+
+import android.graphics.drawable.Drawable
+import android.widget.TextView
+
+/**
+ * Sets the [Drawable]s (if any) to appear to the start of, above, to the end of,
+ * and below the text. Use `null` if you do not want a Drawable there.
+ * The Drawables must already have had [Drawable.setBounds] called.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * [TextView.setCompoundDrawables] or related methods.
+ */
+inline fun TextView.putCompoundDrawablesRelative(
+ start: Drawable? = null,
+ top: Drawable? = null,
+ end: Drawable? = null,
+ bottom: Drawable? = null,
+) = setCompoundDrawablesRelative(start, top, end, bottom)
+
+/**
+ *
+ * Sets the [Drawable]s (if any) to appear to the start of, above, to the end of,
+ * and below the text. Use `null` if you do not want a Drawable there.
+ * The Drawables' bounds will be set to their intrinsic bounds.
+ *
+ * Calling this method will overwrite any Drawables previously set using
+ * [TextView.setCompoundDrawables] or related methods.
+ */
+inline fun TextView.putCompoundDrawablesRelativeWithIntrinsicBounds(
+ start: Drawable? = null,
+ top: Drawable? = null,
+ end: Drawable? = null,
+ bottom: Drawable? = null,
+) = setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom)
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt
new file mode 100644
index 0000000000..625eae2c96
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt
@@ -0,0 +1,187 @@
+/* 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.support.ktx.android.view
+
+import android.graphics.Rect
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.MainThread
+import androidx.core.content.getSystemService
+import androidx.core.view.ViewCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.ktx.android.util.dpToPx
+import java.lang.ref.WeakReference
+
+/**
+ * Is the horizontal layout direction of this view from Right to Left?
+ */
+val View.isRTL: Boolean
+ get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL
+
+/**
+ * Is the horizontal layout direction of this view from Left to Right?
+ */
+val View.isLTR: Boolean
+ get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR
+
+/**
+ * Tries to focus this view and show the soft input window for it.
+ *
+ * @param flags Provides additional operating flags to be used with InputMethodManager.showSoftInput().
+ * Currently may be 0, SHOW_IMPLICIT or SHOW_FORCED.
+ */
+fun View.showKeyboard(flags: Int = InputMethodManager.SHOW_IMPLICIT) {
+ ShowKeyboard(this, flags).post()
+}
+
+/**
+ * Hides the soft input window.
+ */
+fun View.hideKeyboard() {
+ val imm = context.getSystemService<InputMethodManager>()
+ imm?.hideSoftInputFromWindow(windowToken, 0)
+}
+
+/**
+ * Fills the given [Rect] with data about location view in the window.
+ *
+ * @see View.getLocationInWindow
+ */
+fun View.getRectWithViewLocation(): Rect {
+ val locationInWindow = IntArray(2).apply { getLocationInWindow(this) }
+ return Rect(
+ locationInWindow[0],
+ locationInWindow[1],
+ locationInWindow[0] + width,
+ locationInWindow[1] + height,
+ )
+}
+
+/**
+ * Set a padding using [Padding] object.
+ */
+fun View.setPadding(padding: Padding) {
+ with(resources) {
+ setPadding(
+ padding.left.dpToPx(displayMetrics),
+ padding.top.dpToPx(displayMetrics),
+ padding.right.dpToPx(displayMetrics),
+ padding.bottom.dpToPx(displayMetrics),
+ )
+ }
+}
+
+/**
+ * Creates a [CoroutineScope] that is active as long as this [View] is attached. Once this [View]
+ * gets detached this [CoroutineScope] gets cancelled automatically.
+ *
+ * By default coroutines dispatched on the created [CoroutineScope] run on the main dispatcher.
+ *
+ * Note: This scope gets only cancelled if the [View] gets detached. In cases where the [View] never
+ * gets attached this may create a scope that never gets cancelled!
+ */
+@MainThread
+fun View.toScope(): CoroutineScope {
+ val scope = MainScope()
+
+ addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(view: View) = Unit
+
+ override fun onViewDetachedFromWindow(view: View) {
+ scope.cancel()
+ view.removeOnAttachStateChangeListener(this)
+ }
+ },
+ )
+
+ return scope
+}
+
+/**
+ * Finds the first a view in the hierarchy, for which the provided predicate is true.
+ */
+fun View.findViewInHierarchy(predicate: (View) -> Boolean): View? {
+ if (predicate(this)) return this
+
+ if (this is ViewGroup) {
+ for (i in 0 until this.childCount) {
+ val childView = this.getChildAt(i).findViewInHierarchy(predicate)
+ if (childView != null) return childView
+ }
+ }
+
+ return null
+}
+
+/**
+ * Registers a one-time callback to be invoked when the global layout state
+ * or the visibility of views within the view tree changes.
+ */
+inline fun View.onNextGlobalLayout(crossinline callback: () -> Unit) {
+ var listener: ViewTreeObserver.OnGlobalLayoutListener? = null
+ listener = ViewTreeObserver.OnGlobalLayoutListener {
+ viewTreeObserver.removeOnGlobalLayoutListener(listener)
+ callback()
+ }
+ viewTreeObserver.addOnGlobalLayoutListener(listener)
+}
+
+private class ShowKeyboard(
+ view: View,
+ private val flags: Int = InputMethodManager.SHOW_IMPLICIT,
+) : Runnable {
+ private val weakReference: WeakReference<View> = WeakReference(view)
+ private val handler: Handler = Handler(Looper.getMainLooper())
+ private var tries: Int = TRIES
+
+ override fun run() {
+ weakReference.get()?.let { view ->
+ if (!view.isFocusable || !view.isFocusableInTouchMode) {
+ // The view is not focusable - we can't show the keyboard for it.
+ return
+ }
+
+ if (!view.requestFocus()) {
+ // Focus this view first.
+ post()
+ return
+ }
+
+ view.context?.getSystemService<InputMethodManager>()?.let { imm ->
+ if (!imm.isActive(view)) {
+ // This view is not the currently active view for the input method yet.
+ post()
+ return
+ }
+
+ if (!imm.showSoftInput(view, flags)) {
+ // Showing they keyboard failed. Try again later.
+ post()
+ }
+ }
+ }
+ }
+
+ fun post() {
+ tries--
+
+ if (tries > 0) {
+ handler.postDelayed(this, INTERVAL_MS)
+ }
+ }
+
+ companion object {
+ private const val INTERVAL_MS = 100L
+ private const val TRIES = 10
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt
new file mode 100644
index 0000000000..0e564cf6cf
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/Window.kt
@@ -0,0 +1,46 @@
+/* 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.support.ktx.android.view
+
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.view.Window
+import androidx.annotation.ColorInt
+import androidx.core.view.WindowInsetsControllerCompat
+import mozilla.components.support.utils.ColorUtils.isDark
+
+/**
+ * Sets the status bar background color. If the color is light enough, a light navigation bar with
+ * dark icons will be used.
+ */
+fun Window.setStatusBarTheme(@ColorInt color: Int) {
+ createWindowInsetsController().isAppearanceLightStatusBars = !isDark(color)
+ statusBarColor = color
+}
+
+/**
+ * Set the navigation bar background and divider colors. If the color is light enough, a light
+ * navigation bar with dark icons will be used.
+ */
+fun Window.setNavigationBarTheme(
+ @ColorInt navBarColor: Int? = null,
+ @ColorInt navBarDividerColor: Int? = null,
+) {
+ navBarColor?.let {
+ navigationBarColor = it
+ createWindowInsetsController().isAppearanceLightNavigationBars = !isDark(it)
+ }
+
+ if (SDK_INT >= Build.VERSION_CODES.P) {
+ navigationBarDividerColor = navBarDividerColor ?: 0
+ }
+}
+
+/**
+ * Creates a {@link WindowInsetsControllerCompat} for the top-level window decor view.
+ */
+fun Window.createWindowInsetsController(): WindowInsetsControllerCompat {
+ return WindowInsetsControllerCompat(this, this.decorView)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt
new file mode 100644
index 0000000000..00a0a80bc9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/widget/TextView.kt
@@ -0,0 +1,33 @@
+/* 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.support.ktx.android.widget
+
+import android.view.View
+import android.widget.TextView
+
+/* This is the sum of both the default ascender height and the default descender height in Android */
+private const val DEFAULT_FONT_PADDING = 6
+
+/**
+ * Adjusts the text size of the [TextView] according to the height restriction given to the
+ * [View.MeasureSpec] given in the parameter.
+ *
+ * This will take [TextView.getIncludeFontPadding] into account when calculating the available height
+ */
+fun TextView.adjustMaxTextSize(heightMeasureSpec: Int, ascenderPadding: Int = DEFAULT_FONT_PADDING) {
+ val maxHeight = View.MeasureSpec.getSize(heightMeasureSpec)
+
+ var availableHeight = maxHeight.toFloat()
+ if (this.includeFontPadding) {
+ availableHeight -= ascenderPadding * resources.displayMetrics.density
+ }
+
+ availableHeight -= (this.paddingBottom + this.paddingTop) *
+ resources.displayMetrics.density
+
+ if (availableHeight > 0 && this.textSize > availableHeight) {
+ this.textSize = availableHeight / resources.displayMetrics.density
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt
new file mode 100644
index 0000000000..c3e925fc06
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/java/io/File.kt
@@ -0,0 +1,26 @@
+/* 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.support.ktx.java.io
+
+import java.io.File
+
+/**
+ * Removes all files in the directory named by this abstract pathname. Does nothing if the [File] is not pointing to
+ * a directory.
+ */
+fun File.truncateDirectory() {
+ if (!isDirectory) {
+ return
+ }
+
+ listFiles()?.forEach { file ->
+ if (file.isDirectory) {
+ file.truncateDirectory()
+ file.delete()
+ } else {
+ file.delete()
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt
new file mode 100644
index 0000000000..0d12578d3d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/ByteArray.kt
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.ktx.kotlin
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import mozilla.components.support.base.log.logger.Logger
+import java.security.MessageDigest
+
+/**
+ * Checks whether the given [test] byte sequence exists at the [offset] of this [ByteArray]
+ */
+fun ByteArray.containsAtOffset(offset: Int, test: ByteArray): Boolean {
+ if (size - offset < test.size) {
+ return false
+ }
+
+ for (i in 0 until test.size) {
+ if (this[offset + i] != test[i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+fun ByteArray.toBitmap(opts: BitmapFactory.Options? = null): Bitmap? {
+ return toBitmap(0, size, opts)
+}
+
+fun ByteArray.toBitmap(
+ offset: Int,
+ length: Int,
+ opts: BitmapFactory.Options? = null,
+): Bitmap? {
+ if (length <= 0) {
+ return null
+ }
+
+ return try {
+ val bitmap = BitmapFactory.decodeByteArray(this, offset, length, opts)
+
+ if (bitmap == null) {
+ null
+ } else if (bitmap.width <= 0 || bitmap.height <= 0) {
+ Logger.warn("Decoded bitmap jas dimensions: ${bitmap.width} x ${bitmap.height}")
+ null
+ } else {
+ bitmap
+ }
+ } catch (e: OutOfMemoryError) {
+ Logger.warn("OutOfMemoryError while decoding byte array", e)
+ null
+ }
+}
+
+fun ByteArray.toSha256Digest(): ByteArray {
+ return MessageDigest.getInstance("SHA-256").digest(this)
+}
+
+/**
+ * @return A SHA-1 digest.
+ */
+fun ByteArray.toSha1Digest(): ByteArray {
+ return MessageDigest.getInstance("SHA-1").digest(this)
+}
+
+/**
+ * @return An unpadded byte array, according to PKCS#7.
+ */
+@Suppress("MagicNumber")
+fun ByteArray.pkcs7unpad(): ByteArray {
+ // Last byte tells us the padding length.
+ val paddingLength = this.last()
+ // Padding can't be more than 15 bytes.
+ if (paddingLength in 0..16) {
+ return this.copyOfRange(0, this.size - paddingLength)
+ }
+ return this
+}
+
+fun ByteArray.toHexString(): String {
+ return toHexString(2 * this.size)
+}
+
+@Suppress("MagicNumber")
+fun ByteArray.toHexString(hexLength: Int): String {
+ val hs = StringBuilder(Math.max(2 * this.size, hexLength))
+ var stmp: String
+
+ for (n in 0 until hexLength - 2 * this.size) {
+ hs.append("0")
+ }
+
+ for (n in this.indices) {
+ stmp = Integer.toHexString(this[n].toInt() and 0XFF)
+
+ if (stmp.length == 1) {
+ hs.append("0")
+ }
+ hs.append(stmp)
+ }
+
+ return hs.toString()
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt
new file mode 100644
index 0000000000..03425446ab
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Char.kt
@@ -0,0 +1,14 @@
+/* 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/. */
+
+@file:SuppressWarnings("TopLevelPropertyNaming")
+
+package mozilla.components.support.ktx.kotlin
+
+/**
+ * A series of dots (typically three, such as "…") that usually indicates an intentional omission of
+ * a word, sentence, or whole section from a text without altering its original meaning.
+ */
+val Char.Companion.ELLIPSIS: Char
+ get() = '…'
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt
new file mode 100644
index 0000000000..9cda9e3212
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/Collection.kt
@@ -0,0 +1,26 @@
+/* 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.support.ktx.kotlin
+
+/**
+ * Performs a cartesian product of all the elements in two collections and returns each pair to
+ * the [block] function.
+ *
+ * Example:
+ *
+ * ```kotlin
+ * val numbers = listOf(1, 2, 3)
+ * val letters = listOf('a', 'b', 'c')
+ * numbers.crossProduct(letters) { number, letter ->
+ * // Each combination of (1, a), (1, b), (1, c), (2, a), (2, b), etc.
+ * }
+ * ```
+ */
+inline fun <T, U, R> Collection<T>.crossProduct(
+ other: Collection<U>,
+ block: (T, U) -> R,
+) = flatMap { first ->
+ other.map { second -> first to second }.map { block(it.first, it.second) }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
new file mode 100644
index 0000000000..abcd1a741c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt
@@ -0,0 +1,442 @@
+/* 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/. */
+
+@file:Suppress("TooManyFunctions")
+
+package mozilla.components.support.ktx.kotlin
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.InetAddresses
+import android.net.Uri
+import android.os.Build
+import android.util.Base64
+import android.util.Patterns
+import android.webkit.URLUtil
+import androidx.annotation.VisibleForTesting
+import androidx.core.net.toUri
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.ktx.android.net.commonPrefixes
+import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
+import mozilla.components.support.ktx.util.URLStringUtils
+import java.io.File
+import java.net.IDN
+import java.net.MalformedURLException
+import java.net.URL
+import java.net.URLEncoder
+import java.security.MessageDigest
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlin.text.RegexOption.IGNORE_CASE
+
+/**
+ * A collection of regular expressions used in the `is*` methods below.
+ */
+private val re = object {
+ val phoneish = "^\\s*tel:\\S?\\d+\\S*\\s*$".toRegex(IGNORE_CASE)
+ val emailish = "^\\s*mailto:\\w+\\S*\\s*$".toRegex(IGNORE_CASE)
+ val geoish = "^\\s*geo:\\S*\\d+\\S*\\s*$".toRegex(IGNORE_CASE)
+}
+
+private const val MAILTO = "mailto:"
+
+// Number of last digits to be shown when credit card number is obfuscated.
+private const val LAST_VISIBLE_DIGITS_COUNT = 4
+
+// This is used for truncating URLs to prevent extreme cases from
+// slowing down UI rendering e.g. in case of a bookmarklet or a data URI.
+// https://github.com/mozilla-mobile/android-components/issues/5249
+const val MAX_URI_LENGTH = 25000
+
+private const val FILE_PREFIX = "file://"
+private const val MAX_VALID_PORT = 65_535
+
+/**
+ * Shortens URLs to be more user friendly.
+ *
+ * The algorithm used to generate these strings is a combination of FF desktop 'top sites',
+ * feedback from the security team, and documentation regarding url elision. See
+ * StringTest.kt for details.
+ *
+ * This method is complex because URLs have a lot of edge cases. Be sure to thoroughly unit
+ * test any changes you make to it.
+ */
+// Unused Parameter: We may resume stripping eTLD, depending on conversations between security and UX
+// Return count: This is a complex method, but it would not be more understandable if broken up
+// ComplexCondition: Breaking out the complex condition would make this logic harder to follow
+@Suppress("UNUSED_PARAMETER", "ReturnCount", "ComplexCondition")
+fun String.toShortUrl(publicSuffixList: PublicSuffixList): String {
+ val inputString = this
+ val uri = inputString.toUri()
+
+ if (
+ inputString.isEmpty() ||
+ !URLUtil.isValidUrl(inputString) ||
+ inputString.startsWith(FILE_PREFIX) ||
+ uri.port !in -1..MAX_VALID_PORT
+ ) {
+ return inputString
+ }
+
+ if (uri.host?.isIpv4OrIpv6() == true ||
+ // If inputString is just a hostname and not a FQDN, use the entire hostname.
+ uri.host?.contains(".") == false
+ ) {
+ return uri.host ?: inputString
+ }
+
+ fun String.stripUserInfo(): String {
+ val userInfo = this.toUri().encodedUserInfo
+ return if (userInfo != null) {
+ val infoIndex = this.indexOf(userInfo)
+ this.removeRange(infoIndex..infoIndex + userInfo.length)
+ } else {
+ this
+ }
+ }
+ fun String.stripPrefixes(): String = this.toUri().hostWithoutCommonPrefixes ?: this
+ fun String.toUnicode() = IDN.toUnicode(this)
+
+ return inputString
+ .stripUserInfo()
+ .lowercase(Locale.getDefault())
+ .stripPrefixes()
+ .toUnicode()
+}
+
+// impl via FFTV https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/main/java/org/mozilla/focus/utils/FormattedDomain.java#129
+@Suppress("DEPRECATION")
+internal fun String.isIpv4(): Boolean = Patterns.IP_ADDRESS.matcher(this).matches()
+
+// impl via FFiOS: https://github.com/mozilla-mobile/firefox-ios/blob/deb9736c905cdf06822ecc4a20152df7b342925d/Shared/Extensions/NSURLExtensions.swift#L292
+// True IPv6 validation is difficult. This is slightly better than nothing
+internal fun String.isIpv6(): Boolean {
+ return this.isNotEmpty() && this.contains(":")
+}
+
+/**
+ * Returns true if the string represents a valid Ipv4 or Ipv6 IP address.
+ * Note: does not validate a dual format Ipv6 ( "y:y:y:y:y:y:x.x.x.x" format).
+ *
+ */
+@Suppress("TooManyFunctions")
+fun String.isIpv4OrIpv6(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ InetAddresses.isNumericAddress(this)
+ } else {
+ this.isIpv4() || this.isIpv6()
+ }
+}
+
+/**
+ * Checks if this String is a URL.
+ */
+fun String.isUrl() = URLStringUtils.isURLLike(this)
+
+/**
+ * Checks if this String is a URL of an extension page.
+ */
+fun String.isExtensionUrl() = this.startsWith("moz-extension://")
+
+/**
+ * Checks if this String is a URL of a resource.
+ */
+fun String.isResourceUrl() = this.startsWith("resource://")
+
+/**
+ * Appends `http` scheme if no scheme is present in this String.
+ */
+fun String.toNormalizedUrl(): String {
+ val s = this.trim()
+ // Most commonly we'll encounter http or https schemes.
+ // For these, avoid running through toNormalizedURL as an optimization.
+ return if (!s.startsWith("http://") &&
+ !s.startsWith("https://")
+ ) {
+ URLStringUtils.toNormalizedURL(s)
+ } else {
+ s
+ }
+}
+
+fun String.isPhone() = re.phoneish.matches(this)
+
+fun String.isEmail() = re.emailish.matches(this)
+
+fun String.isGeoLocation() = re.geoish.matches(this)
+
+/**
+ * Converts a [String] to a [Date] object.
+ * @param format date format used for formatting the this given [String] object.
+ * @param locale the locale to use when converting the String, defaults to [Locale.ROOT].
+ * @return a [Date] object with the values in the provided in this string, if empty string was provided, a current date
+ * will be returned.
+ */
+fun String.toDate(format: String, locale: Locale = Locale.ROOT): Date {
+ val formatter = SimpleDateFormat(format, locale)
+ return if (isNotEmpty()) {
+ formatter.parse(this) ?: Date()
+ } else {
+ Date()
+ }
+}
+
+/**
+ * Calculates a SHA1 hash for this string.
+ */
+@Suppress("MagicNumber")
+fun String.sha1(): String {
+ val characters = "0123456789abcdef"
+ val digest = MessageDigest.getInstance("SHA-1").digest(toByteArray())
+ return digest.joinToString(
+ separator = "",
+ transform = { byte ->
+ String(charArrayOf(characters[byte.toInt() shr 4 and 0x0f], characters[byte.toInt() and 0x0f]))
+ },
+ )
+}
+
+/**
+ * Tries to convert a [String] to a [Date] using a list of [possibleFormats].
+ * @param possibleFormats one ore more possible format.
+ * @return a [Date] object with the values in the provided in this string,
+ * if the conversion is not possible null will be returned.
+ */
+fun String.toDate(
+ vararg possibleFormats: String = arrayOf(
+ "yyyy-MM-dd'T'HH:mm",
+ "yyyy-MM-dd",
+ "yyyy-'W'ww",
+ "yyyy-MM",
+ "HH:mm",
+ ),
+): Date? {
+ possibleFormats.forEach {
+ try {
+ return this.toDate(it)
+ } catch (pe: ParseException) {
+ // move to next possible format
+ }
+ }
+ return null
+}
+
+/**
+ * Tries to parse and get host part if this [String] is valid URL.
+ * Otherwise returns the string.
+ */
+fun String.tryGetHostFromUrl(): String = try {
+ URL(this).host
+} catch (e: MalformedURLException) {
+ this
+}
+
+/**
+ * Returns `true` if this string is a valid URL that contains [searchParameters] in its query parameters.
+ */
+fun String.urlContainsQueryParameters(searchParameters: String): Boolean = try {
+ URL(this).query?.split("&")?.any { it == searchParameters } ?: false
+} catch (e: MalformedURLException) {
+ false
+}
+
+/**
+ * Compares 2 URLs and returns true if they have the same origin,
+ * which means: same protocol, same host, same port.
+ * It will return false if either this or [other] is not a valid URL.
+ */
+fun String.isSameOriginAs(other: String): Boolean {
+ fun canonicalizeOrigin(urlStr: String): String {
+ val url = URL(urlStr)
+ val port = if (url.port == -1) url.defaultPort else url.port
+ val canonicalized = URL(url.protocol, url.host, port, "")
+ return canonicalized.toString()
+ }
+ return try {
+ canonicalizeOrigin(this) == canonicalizeOrigin(other)
+ } catch (e: MalformedURLException) {
+ false
+ }
+}
+
+/**
+ * Returns an origin (protocol, host and port) from an URL string.
+ */
+fun String.getOrigin(): String? {
+ return try {
+ val url = URL(this)
+ val port = if (url.port == -1) url.defaultPort else url.port
+ URL(url.protocol, url.host, port, "").toString()
+ } catch (e: MalformedURLException) {
+ null
+ }
+}
+
+/**
+ * Returns an origin without the default port.
+ * For example for an input of "https://mozilla.org:443" you will get "https://mozilla.org".
+ */
+fun String.stripDefaultPort(): String {
+ return try {
+ val url = URL(this)
+ val port = if (url.port == url.defaultPort) -1 else url.port
+ URL(url.protocol, url.host, port, "").toString()
+ } catch (e: MalformedURLException) {
+ this
+ }
+}
+
+/**
+ * Remove any unwanted character in url like spaces at the beginning or end.
+ */
+fun String.sanitizeURL(): String {
+ return this.trim()
+}
+
+/**
+ * Remove any unwanted character from string containing file name.
+ * For example for an input of "/../../../../../../directory/file.txt" you will get "file.txt"
+ */
+fun String.sanitizeFileName(): String {
+ val file = File(this.substringAfterLast(File.separatorChar))
+ // Remove unwanted subsequent dots in the file name.
+ return if (file.extension.trim().isNotEmpty() && file.nameWithoutExtension.isNotEmpty()) {
+ file.name.replace("\\.\\.+".toRegex(), ".")
+ } else {
+ file.name.replace(".", "")
+ }
+}
+
+/**
+ * Remove leading mailto from the string.
+ * For example for an input of "mailto:example@example.com" you will get "example@example.com"
+ */
+fun String.stripMailToProtocol(): String {
+ return if (this.startsWith(MAILTO)) {
+ this.replaceFirst(MAILTO, "")
+ } else {
+ this
+ }
+}
+
+/**
+ * Translates the string into {@code application/x-www-form-urlencoded} string.
+ */
+fun String.urlEncode(): String {
+ return URLEncoder.encode(this, Charsets.UTF_8.name())
+}
+
+/**
+ * Returns the string if it's length is not higher than @param[maximumLength] or
+ * a @param[replacement] string if String length is higher than @param[maximumLength]
+ */
+fun String.takeOrReplace(maximumLength: Int, replacement: String): String {
+ return if (this.length > maximumLength) replacement else this
+}
+
+/**
+ * Returns the extension (without ".") declared in the mime type of this data url.
+ * In the event that this data url does not contain a mime type or image extension could be read
+ * for any reason [defaultExtension] will be returned
+ *
+ * @param defaultExtension default extension if one could not be read from the mime type. Default is "jpg".
+ */
+fun String.getDataUrlImageExtension(defaultExtension: String = "jpg"): String {
+ return ("data:image\\/([a-zA-Z0-9-.+]+).*").toRegex()
+ .find(this)?.groups?.get(1)?.value ?: defaultExtension
+}
+
+/**
+ * Returns this char sequence if it's not null or empty
+ * or the result of calling [defaultValue] function if the char sequence is null or empty.
+ */
+inline fun <C, R> C?.ifNullOrEmpty(defaultValue: () -> R): C where C : CharSequence, R : C =
+ if (isNullOrEmpty()) defaultValue() else this
+
+/**
+ * Get the representative part of the URL. Usually this is the eTLD part of the host.
+ *
+ * For example this method will return "facebook.com" for "https://www.facebook.com/foobar".
+ */
+fun String.getRepresentativeSnippet(): String {
+ val uri = Uri.parse(this)
+
+ val host = uri.hostWithoutCommonPrefixes
+ if (!host.isNullOrEmpty()) {
+ return host
+ }
+
+ val path = uri.path
+ if (!path.isNullOrEmpty()) {
+ return path
+ }
+
+ return this
+}
+
+/**
+ * Get a representative character for the given URL.
+ *
+ * For example this method will return "f" for "https://m.facebook.com/foobar".
+ */
+fun String.getRepresentativeCharacter(): String {
+ val snippet = this.getRepresentativeSnippet()
+
+ snippet.forEach { character ->
+ if (character.isLetterOrDigit()) {
+ return character.uppercase()
+ }
+ }
+
+ return "?"
+}
+
+/**
+ * Strips common mobile subdomains from a [String].
+ */
+fun String.stripCommonSubdomains(): String {
+ for (prefix in commonPrefixes) {
+ if (this.startsWith(prefix)) return this.substring(prefix.length)
+ }
+ return this
+}
+
+/**
+ * Returns the last 4 digits from a formatted credit card number string.
+ */
+fun String.last4Digits(): String {
+ return this.takeLast(LAST_VISIBLE_DIGITS_COUNT)
+}
+
+/**
+ * Returns a trimmed string. This is used to prevent extreme cases
+ * from slowing down UI rendering with large strings.
+ */
+fun String.trimmed(): String {
+ return this.take(MAX_URI_LENGTH)
+}
+
+/**
+ * Returns a bitmap from its base64 representation.
+ * Returns null if the string is not a valid base64 representation of a bitmap
+ */
+fun String.base64ToBitmap(): Bitmap? =
+ extractBase6RawString()?.let { rawString ->
+ val raw = Base64.decode(rawString, Base64.DEFAULT)
+ BitmapFactory.decodeByteArray(raw, 0, raw.size)
+ }
+
+@VisibleForTesting
+internal fun String.extractBase6RawString(): String? {
+ // Regex that identifies if the strings starts with:
+ // "(data:image/[ANY_FORMAT];base64,"
+ // For example, "data:image/png;base64,"
+ val base64BitmapRegex = "(data:image/[^;]+;base64,)(.*)".toRegex()
+ return base64BitmapRegex.find(this)?.let {
+ val (_, contentString) = it.destructured
+ contentString
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt
new file mode 100644
index 0000000000..ca31cf7b87
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/Utils.kt
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.ktx.kotlinx.coroutines
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ *
+ * Returns a function that limits the executions of the [block] function, until the [skipTimeInMs]
+ * passes, then the latest value passed to [block] will be used. Any calls before [skipTimeInMs]
+ * passes will be ignored. All calls to the returned function must happen on the same thread.
+ *
+ * Credit to Terenfear https://gist.github.com/Terenfear/a84863be501d3399889455f391eeefe5
+ *
+ * @param skipTimeInMs the time to wait until the next call to [block] be processed.
+ * @param coroutineScope the coroutine scope where [block] will executed.
+ * @param block function to be execute.
+ */
+fun <T> throttleLatest(
+ skipTimeInMs: Long = 300L,
+ coroutineScope: CoroutineScope,
+ block: (T) -> Unit,
+): (T) -> Unit {
+ var throttleJob: Job? = null
+ var latestParam: T
+ return { param: T ->
+ latestParam = param
+ if (throttleJob?.isCompleted != false) {
+ throttleJob = coroutineScope.launch {
+ block(latestParam)
+ delay(skipTimeInMs)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt
new file mode 100644
index 0000000000..eeaf0afa85
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/Flow.kt
@@ -0,0 +1,98 @@
+/* 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.support.ktx.kotlinx.coroutines.flow
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapConcat
+
+/**
+ * Returns a [Flow] containing only changed elements of the lists of the original [Flow].
+ *
+ * ```
+ * Example: Identity function
+ * Transform: x -> x (transformed values are the same as original)
+ * Original Flow: list(0), list(0, 1), list(0, 1, 2, 3), list(4), list(5, 6, 7, 8)
+ * Transformed:
+ * (0) -> (0 emitted because it is a new value)
+ *
+ * (0, 1) -> (0 not emitted because same as previous value,
+ * 1 emitted because it is a new value),
+ *
+ * (0, 1, 2, 3) -> (0 and 1 not emitted because same as previous values,
+ * 2 and 3 emitted because they are new values),
+ *
+ * (4) -> (4 emitted because because it is a new value)
+ *
+ * (5, 6, 7, 8) -> (5, 6, 7, 8 emitted because they are all new values)
+ * Returned Flow: 0, 1, 2, 3, 4, 5, 6, 7, 8
+ * ---
+ *
+ * Example: Modulo 2
+ * Transform: x -> x % 2 (emit changed values if the result of modulo 2 changed)
+ * Original Flow: listOf(1), listOf(1, 2), listOf(3, 4, 5), listOf(3, 4)
+ * Transformed:
+ * (1) -> (1 emitted because it is a new value)
+ *
+ * (1, 0) -> (1 not emitted because same as previous value with the same transformed value,
+ * 2 emitted because it is a new value),
+ *
+ * (1, 0, 1) -> (3, 4, 5 emitted because they are all new values)
+ *
+ * (1, 0) -> (3, 4 not emitted because same as previous values with same transformed values)
+ *
+ * Returned Flow: 1, 2, 3, 4, 5
+ * ---
+ * ```
+ */
+fun <T, R> Flow<List<T>>.filterChanged(transform: (T) -> R): Flow<T> {
+ var lastMappedValues: Map<T, R>? = null
+ return flatMapConcat { values ->
+ val lastMapped = lastMappedValues
+ val changed = if (lastMapped == null) {
+ values
+ } else {
+ values.filter {
+ !lastMapped.containsKey(it) || lastMapped[it] != transform(it)
+ }
+ }
+ lastMappedValues = values.associateWith { transform(it) }
+ changed.asFlow()
+ }
+}
+
+/**
+ * Returns a [Flow] containing only values of the original [Flow] where the result array
+ * of calling [transform] contains at least one different value.
+ *
+ * Example:
+ * ```
+ * Block: x -> [x[0], x[1]] // Map to first two characters of input
+ * Original Flow: "banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home"
+ * Mapped: [b, a], [b, a], [b, u], [a, p], [b, i], [c, o], [c, i], [h, o]
+ * Returned Flow: "banana", "bus, "apple", "big", "coconut", "circle", "home"
+ * ``
+ */
+fun <T, R> Flow<T>.ifAnyChanged(transform: (T) -> Array<R>): Flow<T> {
+ var observedValueOnce = false
+ var lastMappedValues: Array<R>? = null
+
+ return filter { value ->
+ val mapped = transform(value)
+ val hasChanges = lastMappedValues
+ ?.asSequence()
+ ?.filterIndexed { i, r -> mapped[i] != r }
+ ?.any()
+
+ if (!observedValueOnce || hasChanges == true) {
+ lastMappedValues = mapped
+ observedValueOnce = true
+ true
+ } else {
+ false
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt
new file mode 100644
index 0000000000..fb7609db5f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/util/AtomicFile.kt
@@ -0,0 +1,104 @@
+/* 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.support.ktx.util
+
+import android.util.AtomicFile
+import android.util.JsonReader
+import android.util.JsonWriter
+import org.json.JSONException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStreamWriter
+
+/**
+ * Reads an [AtomicFile] and provides a deserialized version of its content.
+ * @param block A function to be executed after the file is read and provides the content as
+ * a [String]. It is expected that this function returns a deserialized version of the content
+ * of the file.
+ */
+inline fun <T> AtomicFile.readAndDeserialize(block: (String) -> T): T? {
+ return try {
+ openRead().use {
+ val text = it.bufferedReader().use { reader -> reader.readText() }
+ block(text)
+ }
+ } catch (_: IOException) {
+ null
+ } catch (_: JSONException) {
+ null
+ }
+}
+
+/**
+ * Writes an [AtomicFile] and indicates if the file was wrote.
+ * @param block A function with provides the content of the file as a [String]
+ * @return true if the file wrote otherwise false
+ */
+inline fun AtomicFile.writeString(block: () -> String): Boolean {
+ return stream { writer ->
+ writer.write(block())
+ }
+}
+
+/**
+ * Opens the [AtomicFile] for writing and provides a [JsonWriter] to [block] for writing JSON
+ * directly to the file.
+ *
+ * At the end of [block] the writer will be flushed and the file closed.
+ */
+inline fun AtomicFile.streamJSON(block: JsonWriter.() -> Unit): Boolean {
+ return stream { writer ->
+ val jsonWriter = JsonWriter(writer)
+ block(jsonWriter)
+ jsonWriter.flush()
+ }
+}
+
+/**
+ * Opens the [AtomicFile] for reading and provides a [JsonReader] to [block] for reading JSON from
+ * the file.
+ */
+inline fun <R> AtomicFile.readJSON(block: JsonReader.() -> R): R? {
+ var reader: InputStream? = null
+
+ return try {
+ reader = openRead()
+
+ val jsonReader = JsonReader(reader.bufferedReader())
+ block(jsonReader)
+ } catch (e: IOException) {
+ null
+ } finally {
+ reader?.close()
+ }
+}
+
+/**
+ * Opens the [AtomicFile] for writing and provides an [OutputStreamWriter] to [block] for writing
+ * directly to the file.
+ *
+ * At the end of [block] the writer will be flushed and the file closed.
+ */
+inline fun AtomicFile.stream(block: (OutputStreamWriter) -> Unit): Boolean {
+ var outputStream: FileOutputStream? = null
+ return try {
+ outputStream = startWrite()
+
+ outputStream.buffered().writer().apply {
+ block(this)
+ flush()
+ }
+
+ finishWrite(outputStream)
+ true
+ } catch (_: IOException) {
+ failWrite(outputStream)
+ false
+ } catch (_: JSONException) {
+ failWrite(outputStream)
+ false
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..f1dc322160
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-am/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ይደውሉ በ…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ኢሜይል በ…</string>
+ <string name="mozac_support_ktx_menu_share_with">ያጋሩ ከ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">በ በኩል አጋራ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..b8d3e21fae
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-an/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gritar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Ninviar un correu con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..329232b470
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ar/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">اتصل به عبر…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">أبرِد عبر…</string>
+ <string name="mozac_support_ktx_menu_share_with">شارِك مع…</string>
+ <string name="mozac_support_ktx_share_dialog_title">شارِك عبر</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..d99964cf72
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ast/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Unviar un corréu electrónicu con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..164b24b301
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-az/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bununla zəng et…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Bununla e-poçt göndər…</string>
+ <string name="mozac_support_ktx_menu_share_with">Paylaş…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..af07dcca5b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-azb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">… -دان تماس توت</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">… ایله ایمیل یوللا</string>
+ <string name="mozac_support_ktx_menu_share_with">… ایله پایلاش</string>
+ <string name="mozac_support_ktx_share_dialog_title">پایلاش</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..5702b6d03f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ban/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Panggil sareng…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Rerepél sareng…</string>
+ <string name="mozac_support_ktx_menu_share_with">Wagiang sareng…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Wagiang anggen</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..6a4bb46bb3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-be/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Тэлефанаваць праз…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Адправіць электроннай поштай праз…</string>
+ <string name="mozac_support_ktx_menu_share_with">Падзяліцца з…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Падзяліцца праз</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..aba8b7418d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bg/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Позвъняване чрез…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Имейл чрез…</string>
+ <string name="mozac_support_ktx_menu_share_with">Споделяне с …</string>
+ <string name="mozac_support_ktx_share_dialog_title">Споделяне чрез</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..e438eeb8a1
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bn/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">যার মাধ্যমে কল করবেন…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">যার মাধ্যমে ইমেইল করবেন…</string>
+ <string name="mozac_support_ktx_menu_share_with">যার মাধ্যমে শেয়ার করবেন…</string>
+ <string name="mozac_support_ktx_share_dialog_title">শেয়ারের মাধ্যম</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..ae6e869874
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-br/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gervel gant…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Kas ur postel gant…</string>
+ <string name="mozac_support_ktx_menu_share_with">Rannañ gant…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Rannañ dre</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..2f60e05edd
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-bs/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Nazovite sa…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Pošaljite email sa…</string>
+ <string name="mozac_support_ktx_menu_share_with">Podijeli sa…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Podijeli putem</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..3a4689607f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ca/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Truca amb…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Envia un correu electrònic amb…</string>
+ <string name="mozac_support_ktx_menu_share_with">Comparteix amb…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Comparteix mitjançant</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..4085d2da6e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cak/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Katoyon rik\'in…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Titaq rik\'in…</string>
+ <string name="mozac_support_ktx_menu_share_with">Tikomonïx rik\'in…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Tikomonïx rik\'in</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..06103d7468
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Tawag sa</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">i-Email sa</string>
+ <string name="mozac_support_ktx_menu_share_with">i-Share sa</string>
+ <string name="mozac_support_ktx_share_dialog_title">i-Share agi sa</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..1dffae627e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">پەیوەندی بکە بەهۆی…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ئیمێڵ بکە بەهۆی…</string>
+ <string name="mozac_support_ktx_menu_share_with">بڵاوەپێکردن لەگەڵ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">بڵاوکردنەوە لە ڕێگەی</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..09f88611fe
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-co/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chjamà cù…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mandà un messaghju cù…</string>
+ <string name="mozac_support_ktx_menu_share_with">Sparte cù…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Sparte via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..7af3f9ecf4
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cs/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Volat pomocí…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Poslat e-mail pomocí…</string>
+ <string name="mozac_support_ktx_menu_share_with">Sdílet pomocí…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Sdílet pomocí</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..434de6b1d5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-cy/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Galw gyda…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-bostio gyda…</string>
+ <string name="mozac_support_ktx_menu_share_with">Rhannu gyda…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Rhannu drwy</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..4521e128e9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-da/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Send mail med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Del med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Del via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..5a33b1ebf3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-de/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Anrufen mit…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Per E-Mail versenden mit…</string>
+ <string name="mozac_support_ktx_menu_share_with">Teilen mit…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Teilen über</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..b24d4b53ac
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Wołaś z …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mejlki słaś z …</string>
+ <string name="mozac_support_ktx_menu_share_with">Źěliś z…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Źěliś pśez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..c1348e15a0
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-el/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Κλήση με…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email με…</string>
+ <string name="mozac_support_ktx_menu_share_with">Κοινή χρήση με…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Κοινή χρήση μέσω</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..e22fa27c94
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Call with…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email with…</string>
+ <string name="mozac_support_ktx_menu_share_with">Share with…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Share via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..e22fa27c94
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Call with…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email with…</string>
+ <string name="mozac_support_ktx_menu_share_with">Share with…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Share via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..cb1ad4d987
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-eo/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Voki per…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Sendi retpoŝton per…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dividi kun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dividi per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..eec2afd893
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar por correo electrónico…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir vía</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..e0b2c75b94
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir mediante</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..0abf4d574f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir a través de</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..7b8ad93650
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir vía</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..0abf4d574f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-es/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Llamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir a través de</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..f795eeda04
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-et/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Helista äpiga…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Saada e-kiri äpiga…</string>
+ <string name="mozac_support_ktx_menu_share_with">Jaga…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Jagamine kasutades</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..7bf9f4b2b2
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-eu/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Deitu honekin…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Bidali mezu elektronikoa honekin…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partekatu honekin…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partekatu honen bidez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..2923d03c60
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fa/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">تماس با…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">رایانامه کردن با…</string>
+ <string name="mozac_support_ktx_menu_share_with">هم‌رسانی با…</string>
+ <string name="mozac_support_ktx_share_dialog_title">همرسانی از طریق</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml
new file mode 100644
index 0000000000..d5ee6e1699
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ff/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Lollin e…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Lollin rewrude e</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..571ff1bcd7
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fi/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Soita…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Sähköposti…</string>
+ <string name="mozac_support_ktx_menu_share_with">Jaa…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Jaa</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..5396ace76c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Appeler avec…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Envoyer un e-mail avec…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partager avec…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partager à l’aide de</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..4e4364b228
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fur/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Clame cun…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mande e-mail cun…</string>
+ <string name="mozac_support_ktx_menu_share_with">Condivît cun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condivît vie</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..67936f7e19
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Belje mei…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-maile mei…</string>
+ <string name="mozac_support_ktx_menu_share_with">Diele mei…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Diele fia</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml
new file mode 100644
index 0000000000..e215f3efce
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ga-rIE/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Comhroinn le…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Comhroinn trí</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..bc0227a440
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gd/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Cuir fòn le…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Cuir air a‘ phost-d le…</string>
+ <string name="mozac_support_ktx_menu_share_with">Co-roinn le…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Co-roinn slighe</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..6899c78f10
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chamar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar por correo con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir mediante</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..f3dd2ad99b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gn/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ehenói… ndive</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Emondo ñanduti veve… ndive</string>
+ <string name="mozac_support_ktx_menu_share_with">Emoherakuã…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Emoherakuã amóva rupi</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..50fec04dc8
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">… સાથે કૉલ કરો</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">… સાથે ઇમેલ કરો</string>
+ <string name="mozac_support_ktx_menu_share_with">સાથે શેર કરો…</string>
+ <string name="mozac_support_ktx_share_dialog_title">દ્વારા શેર કરો</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..6c5ef983bd
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">इसके साथ कॉल करें…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">इसके साथ ईमेल भेजें…</string>
+ <string name="mozac_support_ktx_menu_share_with">के साथ साझा करें…</string>
+ <string name="mozac_support_ktx_share_dialog_title">इससे साझा करें</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..03edef01db
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Nazovi s…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Pošalji e-poštu s…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dijeli s …</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dijeli putem</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..99e16cb09f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Wołać z …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Mejlki słać z …</string>
+ <string name="mozac_support_ktx_menu_share_with">Dźělić z…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dźělić přez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..a738cdeee9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hu/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Hívás ezzel…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-mail ezzel…</string>
+ <string name="mozac_support_ktx_menu_share_with">Megosztás…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Megosztás ezzel</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..7c11d7060f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Զանգահարել՝</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Էլ. նամակ ուղարկել՝</string>
+ <string name="mozac_support_ktx_menu_share_with">Տարածել հետևյալով…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Տարածել միջոցով</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..bf9d4a2856
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ia/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Appellar con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Inviar email con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartir con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartir per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..6e0376c98e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-in/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Panggil dengan…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Surelkan dengan…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bagikan dengan…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bagikan lewat</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..8006dd8621
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-is/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Hringja með…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Senda tölvupóst með…</string>
+ <string name="mozac_support_ktx_menu_share_with">Deila með…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Deila með</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..22d4fd0735
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-it/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chiama con…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Invia email con…</string>
+ <string name="mozac_support_ktx_menu_share_with">Condividi con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condividi con</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..f551cf77cf
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-iw/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">חיוג באמצעות…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">שליחה בדוא״ל באמצעות…</string>
+ <string name="mozac_support_ktx_menu_share_with">שיתוף עם…</string>
+ <string name="mozac_support_ktx_share_dialog_title">שיתוף באמצעות</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..13c7a83ef0
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ja/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">電話をかける…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">メール送信…</string>
+ <string name="mozac_support_ktx_menu_share_with">共有先…</string>
+ <string name="mozac_support_ktx_share_dialog_title">共有方法</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..2abbdb068d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ka/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">დარეკვა…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ელფოსტის გაგზავნა…</string>
+ <string name="mozac_support_ktx_menu_share_with">გაზიარება…</string>
+ <string name="mozac_support_ktx_share_dialog_title">გაზიარება პროგრამით</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..7bc33f728b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">…arqalı qońıraw etiw</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Elektron pochta járdeminde…</string>
+ <string name="mozac_support_ktx_menu_share_with">…járdeminde bólisiw</string>
+ <string name="mozac_support_ktx_share_dialog_title">Arqalı bólisiw</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..e64f651208
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kab/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Siwel s…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Azen imayl s…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bḍu d…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bḍu s</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..49590be901
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kk/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Көмегімен қоңырау шалу…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Көмегімен эл. пошта хатын жіберу…</string>
+ <string name="mozac_support_ktx_menu_share_with">Көмегімен бөлісу…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Арқылы бөлісу</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..c2ac17ebfc
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bigere bi…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Emaîl bi…</string>
+ <string name="mozac_support_ktx_menu_share_with">Parve bike bi…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Parve bike bi rêya</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..cdecfceeef
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-kn/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ಇದರೊಂದಿಗೆ ಕರೆ ಮಾಡಿ…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ಇದರೊಂದಿಗೆ ಇಮೇಲ್ ಮಾಡಿ…</string>
+ <string name="mozac_support_ktx_menu_share_with">ಜೊತೆ ಹಂಚಿಕೊ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ಇವುಗಳ ಮೂಲಕ ಹಂಚು</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..e942f9e254
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ko/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">통화 앱…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">이메일 앱…</string>
+ <string name="mozac_support_ktx_menu_share_with">공유…</string>
+ <string name="mozac_support_ktx_share_dialog_title">다음으로 공유</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml
new file mode 100644
index 0000000000..b516969167
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lij/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Condividdi con…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condividdi via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..e719b84028
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lo/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ໂທດ້ວຍ…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ອີເມລດ້ວຍ…</string>
+ <string name="mozac_support_ktx_menu_share_with">ແບ່ງປັນກັບ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ແບ່ງປັນຜ່ານທາງ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..e8cf2fc4f5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-lt/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Skambinti naudojant…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Siųsti el. laišką naudojant…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dalintis su…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dalintis per</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000000..8d8d7316e9
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ml/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">ഇവരുമായി പങ്കിടുക…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ഇതുവഴി പങ്കിടൂ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..ebb1cc7071
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-mr/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">यासह कॉल करा…</string>
+ <string name="mozac_support_ktx_menu_share_with">यासह शेअर करा…</string>
+ <string name="mozac_support_ktx_share_dialog_title">याद्वारे शेअर करा</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..8faa96afa1
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-my/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ခေါ်ဆိုခြင်း ဖြင့်…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">အီးမေး ဖြင့်…</string>
+ <string name="mozac_support_ktx_menu_share_with">… နှင့်မျှဝေပါ</string>
+ <string name="mozac_support_ktx_share_dialog_title">အခြားကနေ မျှဝေ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..a47a064ec3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Send e-post med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Del med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Del via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..423ed59fd2
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">यससँग कल गर्नुहोस्…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">यससँग इमेल गर्नुहोस्…</string>
+ <string name="mozac_support_ktx_menu_share_with">यससँग साझेदारी गर्नुहोस्…</string>
+ <string name="mozac_support_ktx_share_dialog_title">मार्फत साझेदार गर्नुहोस्</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..005f02df51
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bellen met…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">E-mailen met…</string>
+ <string name="mozac_support_ktx_menu_share_with">Delen met…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Delen via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..a47a064ec3
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Send e-post med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Del med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Del via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..11b8ea1372
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-oc/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Sonar amb…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar un corrièl amb…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partejar amb…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partejar via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..e13aee47b5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">…ਨਾਲ ਕਾਲ ਕਰੋ</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">…ਨਾਲ ਈਮੇਲ ਭੇਜੋ</string>
+ <string name="mozac_support_ktx_menu_share_with">…ਨਾਲ ਸਾਂਝਾ ਕਰੋ</string>
+ <string name="mozac_support_ktx_share_dialog_title">ਇਸ ਰਾਹੀਂ ਸਾਂਝਾ ਕਰੋ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..8ed66cb6de
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">کیہتھوں کال کرو…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">کیہتھوں ای‌میل بھیجو…</string>
+ <string name="mozac_support_ktx_menu_share_with">کیہنوں سانجھا کرو…</string>
+ <string name="mozac_support_ktx_share_dialog_title">کیہتھوں سانجھا کرو</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..ec1f77ca41
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Zadzwoń za pomocą…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Wyślij e-mail za pomocą…</string>
+ <string name="mozac_support_ktx_menu_share_with">Udostępnij za pomocą…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Udostępnij przez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..3027936bdc
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chamar com…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar email com…</string>
+ <string name="mozac_support_ktx_menu_share_with">Compartilhar com…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Compartilhar via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..395aa28f03
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Chamar com …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Enviar e-mail com…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partilhar com…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partilhar via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..529df8d789
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-rm/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Telefonar cun…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Trametter l\'e-mail cun…</string>
+ <string name="mozac_support_ktx_menu_share_with">Cundivider cun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Cundivider via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..58b1a2121d
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ro/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Sună cu…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Trimite mesaj pe e-mail cu…</string>
+ <string name="mozac_support_ktx_menu_share_with">Partajează cu…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Partajează prin</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..bd99e3a695
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ru/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Позвонить через…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Отправить по почте через…</string>
+ <string name="mozac_support_ktx_menu_share_with">Поделиться с помощью…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Поделиться через</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..6caae0a0b5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sat/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">ᱱᱚᱶᱟ ᱛᱮ ᱠᱚᱞ ᱢᱮ …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ᱱᱚᱶᱟ ᱛᱮ ᱤᱢᱮᱞ ᱢᱮ …</string>
+ <string name="mozac_support_ktx_menu_share_with">ᱱᱚᱶᱟ ᱛᱮ ᱦᱟᱹᱴᱤᱧ ᱢᱮ…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ᱫᱟᱨᱟᱭ ᱛᱮ ᱦᱟᱹᱴᱤᱧ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..57751f5529
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sc/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Muti cun…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Imbia cun posta eletrònica…</string>
+ <string name="mozac_support_ktx_menu_share_with">Cumpartzi cun…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Cumpartzi cun</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..639bdcbbb2
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-si/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">සමඟ අමතන්න…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">මගින් තැපෑලට…</string>
+ <string name="mozac_support_ktx_menu_share_with">සමඟ බෙදාගන්න…</string>
+ <string name="mozac_support_ktx_share_dialog_title">හරහා බෙදාගන්න</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..393a11ab4a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sk/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Zavolať pomocou…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Poslať e‑mail pomocou…</string>
+ <string name="mozac_support_ktx_menu_share_with">Zdieľať pomocou…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Zdieľať cez</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..e855302218
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-skr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">این٘دے نال فون کرو۔۔۔</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">این٘دے نال ای میل کرو۔۔۔</string>
+ <string name="mozac_support_ktx_menu_share_with">این٘دے نال شیئر کرو۔۔۔</string>
+ <string name="mozac_support_ktx_share_dialog_title">شیئر بذریعہ</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..827a16fc3c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Pokliči z …</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Pošlji po e-pošti z …</string>
+ <string name="mozac_support_ktx_menu_share_with">Deli z …</string>
+ <string name="mozac_support_ktx_share_dialog_title">Deli preko</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..1da3e21d02
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sq/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Thirre me…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Dërgoni Email me…</string>
+ <string name="mozac_support_ktx_menu_share_with">Ndajeni me të tjerët përmes…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Ndajeni përmes</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..e009ea8e49
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Позови са…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Пошаљи мејл са…</string>
+ <string name="mozac_support_ktx_menu_share_with">Дели са…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Дели преко</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..c46f025160
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-su/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gero maké…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Surélékan maké…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bagikeun sareng…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bagikeun kana</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..109c410ba7
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ring med…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Skicka e-post med…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dela med…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dela via</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..345e1f965a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ta/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">உடன் அழைக்கவும்…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">இதனுடன் மின்னஞ்சல்…</string>
+ <string name="mozac_support_ktx_menu_share_with">இதனுடன் பகிர்…</string>
+ <string name="mozac_support_ktx_share_dialog_title">இதன்வழியாக பகிரவும்</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..20d323b226
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-te/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">దీనితో కాల్ చేయి…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">దీనితో ఈమెయిలు చేయి…</string>
+ <string name="mozac_support_ktx_menu_share_with">దీనితో పంచుకో…</string>
+ <string name="mozac_support_ktx_share_dialog_title">దీని ద్వారా పంచుకోండి</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..81fcf1c654
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tg/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Занг задан тавассути…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Ирсоли паёми эл. тавассути…</string>
+ <string name="mozac_support_ktx_menu_share_with">Мубодила кардан тавассути…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Мубодила тавассути</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..6bda999442
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-th/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">โทรด้วย…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">อีเมลด้วย…</string>
+ <string name="mozac_support_ktx_menu_share_with">แบ่งปันด้วย…</string>
+ <string name="mozac_support_ktx_share_dialog_title">แบ่งปันผ่าน</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..ac8f553b1b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tl/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Tumawag gamit ang…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">i-Email kasama…</string>
+ <string name="mozac_support_ktx_menu_share_with">Ibahagi sa…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Ibahagi sa pamamagitan ng</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..fe7f209fc5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tr/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Bununla çağrı…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Bununla e-posta…</string>
+ <string name="mozac_support_ktx_menu_share_with">Paylaş…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Paylaş</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..8a1414fe57
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-trs/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ga\'mīn ngà…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Gā\'nïnj gān\'ānj korrêo ngà…</string>
+ <string name="mozac_support_ktx_menu_share_with">Dūyingô\' ngà…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Dūyingô\' riña</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..92ab1ac020
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tt/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">… ярдәмендә шалтырату</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">… ярдәмендә эл. почта җибәрү</string>
+ <string name="mozac_support_ktx_menu_share_with">Уртаклашу…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Аша уртаклашу</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml
new file mode 100644
index 0000000000..63e9286a19
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-tzm/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Ɣer s…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Azen imayl s…</string>
+ <string name="mozac_support_ktx_menu_share_with">Bḍu d…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Bḍu s</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..7b79f14b4a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ug/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">تېلېفون قىلىش…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">ئېلخەت يوللاش…</string>
+ <string name="mozac_support_ktx_menu_share_with">ھەمبەھىرلەش…</string>
+ <string name="mozac_support_ktx_share_dialog_title">ھەمبەھىرلەش</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..528ab00907
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-uk/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Зателефонувати через…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Надіслати електронним листом через…</string>
+ <string name="mozac_support_ktx_menu_share_with">Поділитися з…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Поділитись через</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..027be3feed
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-ur/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">اس کے ذریعہ کال کریں…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">اس کے ذریعہ ای میل بھیجیں…</string>
+ <string name="mozac_support_ktx_menu_share_with">… کے ساتھ شیئر کریں</string>
+ <string name="mozac_support_ktx_share_dialog_title">کے زریعے شیئر کریں</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..85303338d4
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-uz/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Qoʻngʻiroq qilish</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Xat yozish</string>
+ <string name="mozac_support_ktx_menu_share_with">Ulashish</string>
+ <string name="mozac_support_ktx_share_dialog_title">Ulashish:</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml
new file mode 100644
index 0000000000..908be2008a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-vec/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <string name="mozac_support_ktx_menu_share_with">Condividi co…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Condividi con</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..94efea291f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-vi/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Gọi với…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Gửi email với…</string>
+ <string name="mozac_support_ktx_menu_share_with">Chia sẻ với…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Chia sẻ qua</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..3fd0eac24b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-yo/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Pè pẹ̀lú…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Ímeelì pẹ̀lú…</string>
+ <string name="mozac_support_ktx_menu_share_with">Pín-in pẹ̀lú…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Pín-in nípasẹ̀</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..68e8298dca
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">使用下列程序拨打电话…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">使用下列程序发送邮件…</string>
+ <string name="mozac_support_ktx_menu_share_with">使用下列方式分享…</string>
+ <string name="mozac_support_ktx_share_dialog_title">分享到</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..8e97f07a1c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">使用下列程式撥打電話…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">使用下列程式發送郵件…</string>
+ <string name="mozac_support_ktx_menu_share_with">使用下列方式分享…</string>
+ <string name="mozac_support_ktx_share_dialog_title">分享到</string>
+</resources>
diff --git a/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml b/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..d20d60e1f4
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ This Source Code Form is subject to the terms of the Mozilla Public
+ ~ License, v. 2.0. If a copy of the MPL was not distributed with this
+ ~ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<resources xmlns:tools="http://schemas.android.com/tools" xmlns:moz="http://schemas.android.com/apk/res-auto">
+ <!-- Text displayed when choosing which app to call with after selecting a phone number-->
+ <string name="mozac_support_ktx_menu_call_with">Call with…</string>
+ <!-- Text displayed when choosing which app to email with after selecting an email address-->
+ <string name="mozac_support_ktx_menu_email_with">Email with…</string>
+ <string name="mozac_support_ktx_menu_share_with">Share with…</string>
+ <string name="mozac_support_ktx_share_dialog_title">Share via</string>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt
new file mode 100644
index 0000000000..205afd049b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/arch/lifecycle/LifecycleTest.kt
@@ -0,0 +1,26 @@
+/* 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.support.ktx.android.arch.lifecycle
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import mozilla.components.support.test.mock
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class LifecycleTest {
+
+ @Test
+ fun addObservers() {
+ val observer1: LifecycleObserver = mock()
+ val observer2: LifecycleObserver = mock()
+ val lifecycle: Lifecycle = mock()
+
+ lifecycle.addObservers(observer1, observer2)
+
+ verify(lifecycle).addObserver(observer1)
+ verify(lifecycle).addObserver(observer2)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt
new file mode 100644
index 0000000000..7288467a3e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextKtTest.kt
@@ -0,0 +1,55 @@
+/* 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.support.ktx.android.content
+
+import android.content.Context
+import android.view.accessibility.AccessibilityManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+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.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowAccessibilityManager
+
+@RunWith(AndroidJUnit4::class)
+class ContextKtTest {
+
+ lateinit var accessibilityManager: ShadowAccessibilityManager
+
+ @Before
+ fun setUp() {
+ accessibilityManager = shadowOf(
+ testContext
+ .getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager,
+ )
+ }
+
+ @Test
+ fun `screen reader enabled`() {
+ // Given
+ accessibilityManager.setTouchExplorationEnabled(true)
+
+ // When
+ val isEnabled = testContext.isScreenReaderEnabled
+
+ // Then
+ assertTrue(isEnabled)
+ }
+
+ @Test
+ fun `screen reader disabled`() {
+ // Given
+ accessibilityManager.setTouchExplorationEnabled(false)
+
+ // When
+ val isEnabled = testContext.isScreenReaderEnabled
+
+ // Then
+ assertFalse(isEnabled)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt
new file mode 100644
index 0000000000..aa6782f4f5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/ContextTest.kt
@@ -0,0 +1,304 @@
+/* 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.support.ktx.android.content
+
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.app.Activity
+import android.app.ActivityManager
+import android.content.ActivityNotFoundException
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.EXTRA_INTENT
+import android.content.Intent.EXTRA_STREAM
+import android.content.Intent.EXTRA_SUBJECT
+import android.content.Intent.EXTRA_TEXT
+import android.content.Intent.EXTRA_TITLE
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.hardware.camera2.CameraManager
+import android.net.Uri
+import android.os.Build
+import androidx.core.content.FileProvider
+import androidx.core.content.getSystemService
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.R
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.fakes.android.FakeContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+import org.robolectric.shadows.ShadowApplication
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowProcess
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class ContextTest {
+
+ @Before
+ fun setup() {
+ isMainProcess = null
+ }
+
+ @Test
+ fun `isOSOnLowMemory() should return the same as getMemoryInfo() lowMemory`() {
+ val extensionFunctionResult = testContext.isOSOnLowMemory()
+
+ val activityManager: ActivityManager? = testContext.getSystemService()
+
+ val normalMethodResult = ActivityManager.MemoryInfo().also { memoryInfo ->
+ activityManager?.getMemoryInfo(memoryInfo)
+ }.lowMemory
+
+ assertEquals(extensionFunctionResult, normalMethodResult)
+ }
+
+ @Test
+ fun `isPermissionGranted() returns same service as checkSelfPermission()`() {
+ val application = ShadowApplication()
+
+ assertEquals(
+ testContext.isPermissionGranted(WRITE_EXTERNAL_STORAGE),
+ testContext.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED,
+ )
+
+ application.grantPermissions(WRITE_EXTERNAL_STORAGE)
+
+ assertEquals(
+ testContext.isPermissionGranted(WRITE_EXTERNAL_STORAGE),
+ testContext.checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED,
+ )
+ }
+
+ @Test
+ fun `share invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.share("https://mozilla.org")
+
+ verify(context).startActivity(argCaptor.capture())
+
+ assertTrue(result)
+ assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class])
+ fun `shareMedia invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ verify(context).startActivity(argCaptor.capture())
+ assertTrue(result)
+ // verify all the properties we set for the share Intent
+ val chooserIntent = argCaptor.value
+ val chooserTitle: String = chooserIntent.extras!!.getString(EXTRA_TITLE) as String
+
+ @Suppress("DEPRECATION")
+ val shareIntent: Intent = chooserIntent.extras!!.get(EXTRA_INTENT) as Intent
+
+ assertTrue(chooserIntent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0)
+ assertTrue(chooserIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK != 0)
+ assertEquals(context.getString(R.string.mozac_support_ktx_menu_share_with), chooserTitle)
+ assertEquals(ACTION_SEND, shareIntent.action)
+
+ @Suppress("DEPRECATION")
+ assertEquals(ShadowFileProvider.FAKE_URI_RESULT, shareIntent.extras!![EXTRA_STREAM])
+ assertEquals("subject", shareIntent.extras!!.getString(EXTRA_SUBJECT))
+ assertEquals("message", shareIntent.extras!!.getString(EXTRA_TEXT))
+ assertTrue(shareIntent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0)
+ assertTrue(shareIntent.flags and Intent.FLAG_ACTIVITY_NEW_DOCUMENT != 0)
+ }
+
+ @Suppress("UNREACHABLE_CODE")
+ @Test
+ @Config(shadows = [ShadowFileProvider::class])
+ fun `shareMedia returns false if the chooser could not be shown`() {
+ val context = spy(
+ object : FakeContext() {
+ override fun startActivity(intent: Intent?) = throw ActivityNotFoundException()
+ override fun getApplicationContext() = testContext
+ },
+ )
+ doReturn(testContext.resources).`when`(context).resources
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ assertFalse(result)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class], sdk = [Build.VERSION_CODES.Q])
+ fun `shareMedia will show a thumbnail starting with Android 10`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ verify(context).startActivity(argCaptor.capture())
+ assertTrue(result)
+ // verify all the properties we set for the share Intent
+ val chooserIntent = argCaptor.value
+ assertEquals(1, chooserIntent.clipData!!.itemCount)
+ assertEquals(ShadowFileProvider.FAKE_URI_RESULT, chooserIntent.clipData!!.getItemAt(0).uri)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class], sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P])
+ fun `shareMedia will not show a thumbnail prior to Android 10`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.shareMedia("filePath", "*/*", "subject", "message")
+
+ verify(context).startActivity(argCaptor.capture())
+ assertTrue(result)
+ // verify all the properties we set for the share Intent
+ val chooserIntent = argCaptor.value
+ assertNull(chooserIntent.clipData)
+ }
+
+ @Test
+ @Config(shadows = [ShadowFileProvider::class])
+ fun `copyImage will copy the file URI to the clipboard & invoke the confirmation action`() {
+ val context = spy(testContext)
+ val confirmationAction = mock<() -> Unit>()
+
+ context.copyImage("filePath", confirmationAction)
+
+ val clipboardManager =
+ testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ assertEquals(
+ ShadowFileProvider.FAKE_URI_RESULT,
+ clipboardManager.primaryClip!!.getItemAt(0).uri,
+ )
+ verify(confirmationAction).invoke()
+ }
+
+ @Test
+ fun `email invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.email("test@mozilla.org")
+
+ verify(context).startActivity(argCaptor.capture())
+
+ assertTrue(result)
+ assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags)
+ }
+
+ @Test
+ fun `call invokes startActivity`() {
+ val context = spy(testContext)
+ val argCaptor = argumentCaptor<Intent>()
+
+ val result = context.call("555-5555")
+
+ verify(context).startActivity(argCaptor.capture())
+
+ assertTrue(result)
+ assertEquals(FLAG_ACTIVITY_NEW_TASK, argCaptor.value.flags)
+ }
+
+ @Test
+ fun `isMainProcess must only return true if we are in the main process`() {
+ val myPid = Int.MAX_VALUE
+
+ assertTrue(testContext.isMainProcess())
+
+ ShadowProcess.setPid(myPid)
+ isMainProcess = null
+
+ assertFalse(testContext.isMainProcess())
+ }
+
+ @Test
+ fun `runOnlyInMainProcess must only run if we are in the main process`() {
+ val myPid = Int.MAX_VALUE
+ var wasExecuted = false
+
+ testContext.runOnlyInMainProcess {
+ wasExecuted = true
+ }
+
+ assertTrue(wasExecuted)
+
+ wasExecuted = false
+ ShadowProcess.setPid(myPid)
+ isMainProcess = false
+
+ testContext.runOnlyInMainProcess {
+ wasExecuted = true
+ }
+
+ assertFalse(wasExecuted)
+ }
+
+ @Test
+ fun `hasCamera returns true if the device has a camera`() {
+ val context = Robolectric.buildActivity(Activity::class.java).get()
+ assertFalse(context.hasCamera())
+
+ val cameraManager: CameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ shadowOf(cameraManager).addCamera("camera0", ShadowCameraCharacteristics.newCameraCharacteristics())
+ assertTrue(context.hasCamera())
+ }
+
+ @Test
+ fun `hasCamera returns false if exception is thrown`() {
+ val context = spy(testContext)
+ val cameraManager: CameraManager = mock()
+ whenever(context.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+
+ whenever(cameraManager.cameraIdList).thenThrow(IllegalStateException("Test"))
+ assertFalse(context.hasCamera())
+ }
+
+ @Test
+ fun `hasCamera returns false if assertion is thrown`() {
+ val context = spy(testContext)
+ val cameraManager: CameraManager = mock()
+ whenever(context.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+
+ whenever(cameraManager.cameraIdList).thenThrow(AssertionError("Test"))
+ assertFalse(context.hasCamera())
+ }
+}
+
+@Implements(FileProvider::class)
+object ShadowFileProvider {
+ val FAKE_URI_RESULT: Uri = "fakeUri".toUri()
+
+ @Implementation
+ @JvmStatic
+ @Suppress("UNUSED_PARAMETER")
+ fun getUriForFile(
+ context: Context?,
+ authority: String?,
+ file: File,
+ ) = FAKE_URI_RESULT
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt
new file mode 100644
index 0000000000..20f32b799f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesStringTest.kt
@@ -0,0 +1,115 @@
+/* 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.support.ktx.android.content
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SharedPreferencesStringTest {
+ private val key = "key"
+ private val defaultValue = "defaultString"
+ private lateinit var preferencesHolder: StringTestPreferenceHolder
+ private lateinit var testPreferences: SharedPreferences
+
+ @Before
+ fun setup() {
+ testPreferences = testContext.getSharedPreferences("test", Context.MODE_PRIVATE)
+ }
+
+ @After
+ fun tearDown() {
+ testPreferences.edit().clear().apply()
+ }
+
+ @Test
+ fun `GIVEN string does not exist and asked to persist the default WHEN asked for it THEN persist the default and return it`() {
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = true,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals(defaultValue, result)
+ assertEquals(defaultValue, testPreferences.getString(key, null))
+ }
+
+ @Test
+ fun `GIVEN string does not exist and not asked to persist the default WHEN asked for it THEN return the default but not persist it`() {
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = false,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals(defaultValue, result)
+ assertNull(testPreferences.getString(key, null))
+ }
+
+ @Test
+ fun `GIVEN string exists and asked to persist the default WHEN asked for it THEN return the existing string and don't persist the default`() {
+ testPreferences.edit().putString(key, "test").apply()
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = true,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals("test", result)
+ }
+
+ @Test
+ fun `GIVEN string exists and not asked to persist the default WHEN asked for it THEN return the existing string and don't persist the default`() {
+ testPreferences.edit().putString(key, "test").apply()
+ preferencesHolder = StringTestPreferenceHolder(
+ persistDefaultIfNotExists = true,
+ )
+
+ val result = preferencesHolder.string
+
+ assertEquals("test", result)
+ }
+
+ @Test
+ fun `GIVEN a value exists WHEN asked to persist a new value THEN update the persisted value`() {
+ testPreferences.edit().putString(key, "test").apply()
+ preferencesHolder = StringTestPreferenceHolder()
+
+ preferencesHolder.string = "update"
+
+ assertEquals(
+ "update",
+ testPreferences.getString(key, null),
+ )
+ }
+
+ @Test
+ fun `GIVEN a value does not exist WHEN asked to persist a new value THEN persist the requested value`() {
+ preferencesHolder = StringTestPreferenceHolder()
+
+ preferencesHolder.string = "test"
+
+ assertEquals(
+ "test",
+ testPreferences.getString(key, null),
+ )
+ }
+
+ private inner class StringTestPreferenceHolder(
+ persistDefaultIfNotExists: Boolean = false,
+ ) : PreferencesHolder {
+ override val preferences = testPreferences
+
+ var string by stringPreference(key, defaultValue, persistDefaultIfNotExists)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt
new file mode 100644
index 0000000000..7ab36893ae
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/SharedPreferencesTest.kt
@@ -0,0 +1,229 @@
+/* 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.support.ktx.android.content
+
+import android.content.SharedPreferences
+import mozilla.components.support.test.any
+import mozilla.components.support.test.eq
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+
+class SharedPreferencesTest {
+ @Mock private lateinit var sharedPrefs: SharedPreferences
+
+ @Mock private lateinit var editor: SharedPreferences.Editor
+
+ @Before
+ fun setup() {
+ openMocks(this)
+
+ `when`(sharedPrefs.edit()).thenReturn(editor)
+ `when`(editor.putBoolean(anyString(), anyBoolean())).thenReturn(editor)
+ `when`(editor.putFloat(anyString(), anyFloat())).thenReturn(editor)
+ `when`(editor.putInt(anyString(), anyInt())).thenReturn(editor)
+ `when`(editor.putLong(anyString(), anyLong())).thenReturn(editor)
+ `when`(editor.putString(anyString(), anyString())).thenReturn(editor)
+ `when`(editor.putStringSet(anyString(), any())).thenReturn(editor)
+ }
+
+ @Test
+ fun `getter returns boolean from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getBoolean(eq("boolean"), anyBoolean())).thenReturn(true)
+
+ assertTrue(holder.boolean)
+ verify(sharedPrefs).getBoolean("boolean", false)
+ }
+
+ @Test
+ fun `setter applies boolean to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultBoolean = true)
+ holder.boolean = false
+
+ verify(editor).putBoolean("boolean", false)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default boolean value`() {
+ val holderFalse = MockPreferencesHolder(defaultBoolean = false)
+ // Call the getter for the test
+ holderFalse.boolean
+
+ verify(sharedPrefs).getBoolean("boolean", false)
+
+ val holderTrue = MockPreferencesHolder(defaultBoolean = true)
+ // Call the getter for the test
+ holderTrue.boolean
+
+ verify(sharedPrefs).getBoolean("boolean", true)
+ }
+
+ @Test
+ fun `getter returns float from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getFloat(eq("float"), anyFloat())).thenReturn(2.4f)
+
+ assertEquals(2.4f, holder.float)
+ verify(sharedPrefs).getFloat("float", 0f)
+ }
+
+ @Test
+ fun `setter applies float to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultFloat = 1f)
+ holder.float = 0f
+
+ verify(editor).putFloat("float", 0f)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default float value`() {
+ val holderDefault = MockPreferencesHolder(defaultFloat = 0f)
+ // Call the getter for the test
+ holderDefault.float
+
+ verify(sharedPrefs).getFloat("float", 0f)
+
+ val holderOther = MockPreferencesHolder(defaultFloat = 12f)
+ // Call the getter for the test
+ holderOther.float
+
+ verify(sharedPrefs).getFloat("float", 12f)
+ }
+
+ @Test
+ fun `getter returns int from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getInt(eq("int"), anyInt())).thenReturn(5)
+
+ assertEquals(5, holder.int)
+ verify(sharedPrefs).getInt("int", 0)
+ }
+
+ @Test
+ fun `setter applies int to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultInt = 1)
+ holder.int = 0
+
+ verify(editor).putInt("int", 0)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default int value`() {
+ val holderDefault = MockPreferencesHolder(defaultInt = 0)
+ // Call the getter for the test
+ holderDefault.int
+
+ verify(sharedPrefs).getInt("int", 0)
+
+ val holderOther = MockPreferencesHolder(defaultInt = 23)
+ // Call the getter for the test
+ holderOther.int
+
+ verify(sharedPrefs).getInt("int", 23)
+ }
+
+ @Test
+ fun `getter returns long from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getLong(eq("long"), anyLong())).thenReturn(200L)
+
+ assertEquals(200L, holder.long)
+ verify(sharedPrefs).getLong("long", 0)
+ }
+
+ @Test
+ fun `setter applies long to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultLong = 1)
+ holder.long = 0
+
+ verify(editor).putLong("long", 0)
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default long value`() {
+ val holderDefault = MockPreferencesHolder(defaultLong = 0)
+ // Call the getter for the test
+ holderDefault.long
+
+ verify(sharedPrefs).getLong("long", 0)
+
+ val holderOther = MockPreferencesHolder(defaultLong = 23)
+ // Call the getter for the test
+ holderOther.long
+
+ verify(sharedPrefs).getLong("long", 23)
+ }
+
+ @Test
+ fun `getter returns string set from shared preferences`() {
+ val holder = MockPreferencesHolder()
+ `when`(sharedPrefs.getStringSet(eq("string_set"), any())).thenReturn(setOf("foo"))
+
+ assertEquals(setOf("foo"), holder.stringSet)
+ verify(sharedPrefs).getStringSet("string_set", emptySet())
+ }
+
+ @Test
+ fun `setter applies string set to shared preferences`() {
+ val holder = MockPreferencesHolder(defaultString = "foo")
+ holder.stringSet = setOf("bar")
+
+ verify(editor).putStringSet("string_set", setOf("bar"))
+ verify(editor).apply()
+ }
+
+ @Test
+ fun `getter uses default string set value`() {
+ val holderDefault = MockPreferencesHolder()
+ // Call the getter for the test
+ holderDefault.stringSet
+
+ verify(sharedPrefs).getStringSet("string_set", emptySet())
+
+ val holderOther = MockPreferencesHolder(defaultSet = setOf("hello", "world"))
+ // Call the getter for the test
+ holderOther.stringSet
+
+ verify(sharedPrefs).getStringSet("string_set", setOf("hello", "world"))
+ }
+
+ private inner class MockPreferencesHolder(
+ defaultBoolean: Boolean = false,
+ defaultFloat: Float = 0f,
+ defaultInt: Int = 0,
+ defaultLong: Long = 0L,
+ defaultString: String = "",
+ defaultSet: Set<String> = emptySet(),
+ ) : PreferencesHolder {
+ override val preferences = sharedPrefs
+
+ var boolean by booleanPreference("boolean", default = defaultBoolean)
+
+ var float by floatPreference("float", default = defaultFloat)
+
+ var int by intPreference("int", default = defaultInt)
+
+ var long by longPreference("long", default = defaultLong)
+
+ var string by stringPreference("string", default = defaultString)
+
+ var stringSet by stringSetPreference("string_set", default = defaultSet)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt
new file mode 100644
index 0000000000..4e15aa6dbc
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/pm/PackageManagerTest.kt
@@ -0,0 +1,49 @@
+/* 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.support.ktx.android.content.pm
+
+import android.content.Context
+import android.content.pm.PackageInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+
+@RunWith(AndroidJUnit4::class)
+class PackageManagerTest {
+ private fun createContext(
+ installedApps: List<String> = emptyList(),
+ ): Context {
+ val pm = testContext.packageManager
+ val packageManager = shadowOf(pm)
+ val context = mock<Context>()
+ `when`(context.packageManager).thenReturn(pm)
+ installedApps.forEach { name ->
+ val packageInfo = PackageInfo().apply {
+ packageName = name
+ }
+ packageManager.addPackageNoDefaults(packageInfo)
+ }
+
+ return context
+ }
+
+ /**
+ * Verify that PackageManager.isPackageInstalled works correctly.
+ */
+ @Test
+ fun `isPackageInstalled() returns true when package is installed, false otherwise`() {
+ val context = createContext(listOf("com.example", "com.test"))
+
+ assert(context.packageManager.isPackageInstalled("com.example"))
+ assert(context.packageManager.isPackageInstalled("com.test"))
+ assertFalse(context.packageManager.isPackageInstalled("com.mozilla"))
+ assertFalse(context.packageManager.isPackageInstalled("com.example.com"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt
new file mode 100644
index 0000000000..dcb087ccae
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/AssetManagerTest.kt
@@ -0,0 +1,78 @@
+/* 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.support.ktx.android.content.res
+
+import android.content.Context
+import android.content.res.AssetManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import java.io.ByteArrayInputStream
+
+@RunWith(AndroidJUnit4::class)
+class AssetManagerTest {
+
+ /**
+ * Verify that AssetManager.readJSONObject() closes its stream.
+ */
+ @Test
+ fun readJSONObjectClosesStream() {
+ // Setup
+
+ val stream = Mockito.spy(ByteArrayInputStream("{}".toByteArray()))
+
+ val assetManager = mock<AssetManager>()
+ Mockito.`when`(assetManager.open(ArgumentMatchers.anyString())).thenReturn(stream)
+
+ val context = mock<Context>()
+ Mockito.`when`(context.assets).thenReturn(assetManager)
+
+ // Now use our mock classes to call readJSONObject()
+
+ context.assets.readJSONObject("test.txt")
+
+ // Verify that the stream was opened and closed
+
+ Mockito.verify(assetManager).open("test.txt")
+ Mockito.verify(stream).close()
+ }
+
+ /**
+ * The stream returned by the AssetManager will be read and converted into a JSONObject instance.
+ */
+ @Test
+ fun streamsIsTransformedIntoJSONObject() {
+ // Setup
+
+ val stream = Mockito.spy(ByteArrayInputStream("{'firstName': 'John', 'lastName': 'Smith'}".toByteArray()))
+
+ val assetManager = mock<AssetManager>()
+ Mockito.`when`(assetManager.open(ArgumentMatchers.anyString())).thenReturn(stream)
+
+ val context = mock<Context>()
+ Mockito.`when`(context.assets).thenReturn(assetManager)
+
+ // Now read the stream into an JSONObject
+
+ val data = context.assets.readJSONObject("test.txt")
+
+ // Assert that the JSONObject was constructed correctly.
+
+ Mockito.verify(assetManager).open("test.txt")
+
+ assertNotNull(data)
+ assertEquals(2, data.length())
+ assertTrue(data.has("firstName"))
+ assertTrue(data.has("lastName"))
+ assertEquals("John", data.getString("firstName"))
+ assertEquals("Smith", data.getString("lastName"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt
new file mode 100644
index 0000000000..c8be2da46c
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/content/res/ResourcesTest.kt
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.ktx.android.content.res
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Typeface.BOLD
+import android.graphics.Typeface.ITALIC
+import android.os.Build
+import android.os.LocaleList
+import android.text.Html
+import android.text.style.StyleSpan
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.robolectric.annotation.Config
+import java.util.Locale
+
+@RunWith(AndroidJUnit4::class)
+class ResourcesTest {
+
+ private lateinit var resources: Resources
+ private lateinit var configuration: Configuration
+
+ @Before
+ fun setup() {
+ resources = mock()
+ configuration = spy(Configuration())
+
+ whenever(resources.configuration).thenReturn(configuration)
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.N])
+ @Test
+ fun `locale returns first item in locales list`() {
+ whenever(configuration.locales).thenReturn(LocaleList(Locale.CANADA, Locale.ENGLISH))
+ assertEquals(Locale.CANADA, resources.locale)
+ }
+
+ @Suppress("Deprecation")
+ @Config(sdk = [Build.VERSION_CODES.M])
+ @Test
+ fun `locale returns locale from configuration`() {
+ configuration.locale = Locale.FRENCH
+ assertEquals(Locale.FRENCH, resources.locale)
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.N])
+ @Test
+ fun `getSpanned formats corresponding string`() {
+ val id = 100
+ whenever(configuration.locales).thenReturn(LocaleList(Locale.ROOT))
+ whenever(resources.getString(id)).thenReturn("Allow %1\$s to open %2\$s")
+
+ assertEquals(
+ "<p dir=\"ltr\">Allow <b>App</b> to open <i>Website</i></p>\n",
+ Html.toHtml(
+ resources.getSpanned(
+ id,
+ "App" to StyleSpan(BOLD),
+ "Website" to StyleSpan(ITALIC),
+ ),
+ Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE,
+ ),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt
new file mode 100644
index 0000000000..acd57c1194
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/graphics/BitmapKtTest.kt
@@ -0,0 +1,78 @@
+/* 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.support.ktx.android.graphics
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BitmapKtTest {
+
+ private lateinit var subject: Bitmap
+
+ @Before
+ fun setUp() {
+ subject = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
+ }
+
+ @Ignore("convert to integration test. Robolectric's shadows are incomplete and cause this to fail.")
+ @Test
+ fun `WHEN withRoundedCorners is called THEN returned bitmap's corners should be transparent and center with color`() {
+ val dimen = 200
+ val fillColor = Color.RED
+
+ val bitmap = Bitmap.createBitmap(dimen, dimen, Bitmap.Config.ARGB_8888).apply {
+ eraseColor(fillColor)
+ }
+ val roundedBitmap = bitmap.withRoundedCorners(40f)
+
+ fun assertCornersAreTransparent() {
+ val cornerLocations = listOf(0, dimen - 1)
+
+ cornerLocations.forEach { x ->
+ cornerLocations.forEach { y ->
+ assertEquals(Color.TRANSPARENT, roundedBitmap.getPixel(x, y))
+ }
+ }
+ }
+
+ fun assertCenterIsFilled() {
+ val center = dimen / 2
+ assertEquals(fillColor, roundedBitmap.getPixel(center, center))
+ }
+
+ assertNotSame(bitmap, roundedBitmap)
+ assertCornersAreTransparent()
+ assertCenterIsFilled()
+ }
+
+ @Test
+ fun `GIVEN an all red bitmap THEN pixels are all the same`() {
+ subject.eraseColor(Color.RED)
+ assertTrue(subject.arePixelsAllTheSame())
+ }
+
+ @Test
+ fun `GIVEN an all transparent bitmap THEN pixels are all the same`() {
+ subject.eraseColor(Color.TRANSPARENT)
+ assertTrue(subject.arePixelsAllTheSame())
+ }
+
+ @Test
+ fun `GIVEN an all red bitmap with one pixel not red THEN pixels are not all the same`() {
+ subject.eraseColor(Color.RED)
+ subject.setPixel(0, 1, Color.rgb(244, 0, 0))
+ assertFalse(subject.arePixelsAllTheSame())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt
new file mode 100644
index 0000000000..213e440e56
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/net/UriTest.kt
@@ -0,0 +1,243 @@
+/* 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.support.ktx.android.net
+
+import android.content.ContentResolver
+import android.database.Cursor
+import android.webkit.MimeTypeMap
+import androidx.core.net.toUri
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doReturn
+import org.robolectric.Shadows
+
+@RunWith(AndroidJUnit4::class)
+class UriTest {
+
+ @Test
+ fun hostWithoutCommonPrefixes() {
+ assertEquals(
+ "mozilla.org",
+ "https://www.mozilla.org".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertEquals(
+ "twitter.com",
+ "https://mobile.twitter.com/home".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertNull("".toUri().hostWithoutCommonPrefixes)
+
+ assertEquals(
+ "",
+ "http://".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertEquals(
+ "facebook.com",
+ "https://m.facebook.com/".toUri().hostWithoutCommonPrefixes,
+ )
+
+ assertEquals(
+ "github.com",
+ "https://github.com/mozilla-mobile/android-components".toUri().hostWithoutCommonPrefixes,
+ )
+ }
+
+ @Test
+ fun testIsHttpOrHttps() {
+ // No value
+ assertFalse("".toUri().isHttpOrHttps)
+
+ // Garbage
+ assertFalse("lksdjflasuf".toUri().isHttpOrHttps)
+
+ // URLs with http/https
+ assertTrue("https://www.google.com".toUri().isHttpOrHttps)
+ assertTrue("http://www.facebook.com".toUri().isHttpOrHttps)
+ assertTrue("https://mozilla.org/en-US/firefox/products/".toUri().isHttpOrHttps)
+
+ // IP addresses
+ assertTrue("https://192.168.0.1".toUri().isHttpOrHttps)
+ assertTrue("http://63.245.215.20/en-US/firefox/products".toUri().isHttpOrHttps)
+
+ // Other protocols
+ assertFalse("ftp://people.mozilla.org".toUri().isHttpOrHttps)
+ assertFalse("javascript:window.google.com".toUri().isHttpOrHttps)
+ assertFalse("tel://1234567890".toUri().isHttpOrHttps)
+
+ // No scheme
+ assertFalse("google.com".toUri().isHttpOrHttps)
+ assertFalse("git@github.com:mozilla/gecko-dev.git".toUri().isHttpOrHttps)
+ }
+
+ @Test
+ fun testIsInScope() {
+ val url = "https://mozilla.github.io/my-app/".toUri()
+ val prefix = "https://mozilla.github.io/prefix-of/resource.html".toUri()
+ assertFalse(url.isInScope(emptyList()))
+ assertTrue(url.isInScope(listOf("https://mozilla.github.io/my-app/".toUri())))
+ assertFalse(url.isInScope(listOf("https://firefox.com/out-of-scope/".toUri())))
+ assertFalse(url.isInScope(listOf("https://mozilla.github.io/my-app-almost-in-scope".toUri())))
+ assertTrue(prefix.isInScope(listOf("https://mozilla.github.io/prefix".toUri())))
+ assertTrue(prefix.isInScope(listOf("https://mozilla.github.io/prefix-of/".toUri())))
+ }
+
+ @Test
+ fun testSameSchemeAndHostAs() {
+ // Host mismatch.
+ assertFalse("https://foo.bar".toUri().sameSchemeAndHostAs("https://foo.baz".toUri()))
+ // Scheme mismatch.
+ assertFalse("http://127.0.0.1".toUri().sameSchemeAndHostAs("https://127.0.0.1".toUri()))
+ // Port mismatch.
+ assertTrue("https://foo.bar:444".toUri().sameSchemeAndHostAs("https://foo.bar:555".toUri()))
+ // Port OK but scheme different.
+ assertFalse("https://foo.bar:443".toUri().sameSchemeAndHostAs("ftp://foo.bar:443".toUri()))
+
+ assertTrue("https://foo.bar/bobo".toUri().sameSchemeAndHostAs("https://foo.bar:443/obob".toUri()))
+ assertTrue("https://foo.bar:333".toUri().sameSchemeAndHostAs("https://foo.bar:443:333".toUri()))
+ }
+
+ @Test
+ fun testSameOriginAs() {
+ // Host mismatch.
+ assertFalse("https://foo.bar".toUri().sameOriginAs("https://foo.baz".toUri()))
+ // Scheme mismatch.
+ assertFalse("http://127.0.0.1".toUri().sameOriginAs("https://127.0.0.1".toUri()))
+ // Port mismatch.
+ assertFalse("https://foo.bar:444".toUri().sameOriginAs("https://foo.bar:555".toUri()))
+ // Port OK but scheme different.
+ assertFalse("https://foo.bar:443".toUri().sameOriginAs("ftp://foo.bar:443".toUri()))
+
+ assertTrue("https://foo.bar:443/bobo".toUri().sameOriginAs("https://foo.bar:443/obob".toUri()))
+ assertTrue("https://foo.bar:333".toUri().sameOriginAs("https://foo.bar:333".toUri()))
+ }
+
+ @Test
+ fun testGenerateFileName() {
+ val fileExtension = "txt"
+ var fileName = generateFileName(fileExtension)
+
+ assertTrue(fileName.contains(fileExtension))
+
+ fileName = generateFileName()
+
+ assertFalse(fileName.contains("."))
+ }
+
+ @Test
+ fun testGetFileExtension() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ assertEquals("txt", uri.getFileExtension(resolver))
+ }
+
+ @Test
+ fun `getFileNameForContentUris for urls with DISPLAY_NAME`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(1).`when`(cursor).getColumnIndex(any())
+ doReturn("myFile.txt").`when`(cursor).getString(anyInt())
+
+ assertEquals("myFile.txt", uri.getFileNameForContentUris(resolver))
+ }
+
+ @Test
+ fun `getFileNameForContentUris for urls without DISPLAY_NAME`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(-1).`when`(cursor).getColumnIndex(any())
+
+ val fileName = uri.getFileNameForContentUris(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+
+ @Test
+ fun `getFileNameForContentUris for urls with null DISPLAY_NAME`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(1).`when`(cursor).getColumnIndex(any())
+ doReturn(null).`when`(cursor).getString(anyInt())
+
+ val fileName = uri.getFileNameForContentUris(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+
+ @Test
+ fun `getFileName for file uri schemes`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "file:///home/user/myfile.html".toUri()
+
+ assertEquals("myfile.html", uri.getFileName(resolver))
+ }
+
+ @Test
+ fun `getFileName for content uri schemes`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "content://media/external/file/37162".toUri()
+ val cursor = mock<Cursor>()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ doReturn(cursor).`when`(resolver).query(any(), any(), any(), any(), any())
+ doReturn(1).`when`(cursor).getColumnIndex(any())
+ doReturn(null).`when`(cursor).getString(anyInt())
+
+ val fileName = uri.getFileName(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+
+ @Test
+ fun `getFileName for UNKNOWN uri schemes will generate file name`() {
+ val resolver = mock<ContentResolver>()
+ val uri = "UNKNOWN://media/external/file/37162".toUri()
+
+ Shadows.shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypeMapping("txt", "text/plain")
+ doReturn("text/plain").`when`(resolver).getType(any())
+
+ val fileName = uri.getFileName(resolver)
+
+ assertTrue(fileName.contains(".txt"))
+ assertTrue(fileName.isNotEmpty())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt
new file mode 100644
index 0000000000..5b1a53bce6
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONArrayTest.kt
@@ -0,0 +1,137 @@
+/* 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.support.ktx.android.org.json
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class JSONArrayTest {
+
+ private lateinit var testData2Elements: JSONArray
+
+ @Before
+ fun setUp() {
+ testData2Elements = JSONArray().apply {
+ put(JSONObject("""{"a": 1}"""))
+ put(JSONObject("""{"b": 2}"""))
+ }
+ }
+
+ @Test
+ fun itCanBeIterated() {
+ val array = JSONArray("[1, 2, 3]")
+
+ val sum = array.asSequence()
+ .map { it as Int }
+ .sum()
+
+ assertEquals(6, sum)
+ }
+
+ @Test
+ fun toListNull() {
+ val jsonArray: JSONArray? = null
+ val list = jsonArray.toList<Any>()
+ assertEquals(0, list.size)
+ }
+
+ @Test
+ fun toListEmpty() {
+ val jsonArray = JSONArray()
+ val list = jsonArray.toList<Any>()
+ assertEquals(0, list.size)
+ }
+
+ @Test
+ fun toListNotEmpty() {
+ val jsonArray = JSONArray()
+ jsonArray.put("value")
+ jsonArray.put("another-value")
+ val list = jsonArray.toList<String>()
+ assertEquals(2, list.size)
+ assertTrue(list.contains("value"))
+ assertTrue(list.contains("another-value"))
+ }
+
+ @Test
+ fun `WHEN mapNotNull on an empty jsonArray THEN an empty list is returned`() {
+ assertEquals(emptyList<Int>(), JSONArray().mapNotNull(JSONArray::getJSONObject) { 1 })
+ }
+
+ @Test
+ fun `WHEN mapNotNull getFromArray throws a JSONException THEN that item is ignored`() {
+ val expected = listOf("a", "b")
+ testData2Elements.put(404)
+ val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) { it.keys().asSequence().first() }
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `WHEN mapNotNull transform throws a JSONException THEN that item is ignored`() {
+ val actual = testData2Elements.mapNotNull(JSONArray::getJSONObject) {
+ it.get("b") // key not found for first item: throws an exception.
+ }
+ assertEquals(1, actual.size)
+ assertEquals(2, actual[0])
+ }
+
+ @Test
+ fun `WHEN mapNotNull getFromArray uses casted classes THEN data is mapped`() {
+ val expected = listOf(JSONArrayTest())
+ val input = JSONArray().apply { put(expected[0]) }
+ val actual = input.mapNotNull(getFromArray = { i -> get(i) as JSONArrayTest }) { it }
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `WHEN mapNotNull on an array of Int THEN nulls are removed and data is mapped`() {
+ val expected = listOf(2, 4, 6, 8, 10)
+
+ // Convert expected to input: [2, null, 4, null, ...]
+ val input = JSONArray()
+ expected.forEach {
+ input.put(it)
+ input.put(null)
+ }
+ val actual = input.mapNotNull(JSONArray::getInt) { it }
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `WHEN mapNotNull on an array of JSONObject THEN nulls are removed and data is mapped`() {
+ val expected = listOf(
+ "a" to 1,
+ "b" to 2,
+ "c" to 3,
+ )
+
+ // Convert expected to input: [JSONObject("a" to 1), null, JSONObject("b" to 2), null, ...]
+ val input = JSONArray()
+ expected.forEach {
+ val obj = JSONObject().apply { put(it.first, it.second) }
+ input.put(obj)
+ input.put(null)
+ }
+
+ val actual = input.mapNotNull(JSONArray::getJSONObject) {
+ val keys = it.keys().asSequence().toList()
+ assertEquals(it.toString(), 1, keys.size)
+
+ val key = keys.first()
+ val value = it.get(key) as Int
+ Pair(key, value)
+ }
+
+ assertEquals(expected, actual)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt
new file mode 100644
index 0000000000..1f724f0b55
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/org/json/JSONObjectTest.kt
@@ -0,0 +1,131 @@
+/* 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.support.ktx.android.org.json
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.json.JSONArray
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class JSONObjectTest {
+
+ @Test
+ fun sortKeys() {
+ val jsonObject = JSONObject()
+ jsonObject.put("second-key", "second-value")
+ jsonObject.put(
+ "third-key",
+ JSONArray().apply {
+ put(1)
+ put(2)
+ put(3)
+ },
+ )
+ jsonObject.put(
+ "first-key",
+ JSONObject().apply {
+ put("one-key", "one-value")
+ put("a-key", "a-value")
+ put("second-key", "second")
+ },
+ )
+ assertEquals("""{"first-key":{"a-key":"a-value","one-key":"one-value","second-key":"second"},"second-key":"second-value","third-key":[1,2,3]}""", jsonObject.sortKeys().toString())
+ }
+
+ @Test
+ fun putIfNotNull() {
+ val jsonObject = JSONObject()
+ assertEquals(0, jsonObject.length())
+ jsonObject.putIfNotNull("key", null)
+ assertEquals(0, jsonObject.length())
+ jsonObject.putIfNotNull("key", "value")
+ assertEquals(1, jsonObject.length())
+ assertEquals("value", jsonObject["key"])
+ }
+
+ @Test
+ fun tryGetNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGet("key"))
+ assertNull(jsonObject.tryGet("another-key"))
+ }
+
+ @Test
+ fun tryGetNotNull() {
+ val jsonObject = JSONObject("""{"key":"value"}""")
+ assertEquals("value", jsonObject.tryGet("key"))
+ }
+
+ @Test
+ fun tryGetStringNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGetString("key"))
+ assertNull(jsonObject.tryGetString("another-key"))
+ }
+
+ @Test
+ fun tryGetStringNotNull() {
+ val jsonObject = JSONObject("""{"key":"value"}""")
+ assertEquals("value", jsonObject.tryGetString("key"))
+ }
+
+ @Test
+ fun tryGetLongNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGetLong("key"))
+ assertNull(jsonObject.tryGetLong("another-key"))
+ }
+
+ @Test
+ fun tryGetLongNotNull() {
+ val jsonObject = JSONObject("""{"key":218728173837192717}""")
+ assertEquals(218728173837192717, jsonObject.tryGetLong("key"))
+ }
+
+ @Test
+ fun tryGetIntNull() {
+ val jsonObject = JSONObject("""{"key":null}""")
+ assertNull(jsonObject.tryGetInt("key"))
+ assertNull(jsonObject.tryGetInt("another-key"))
+ }
+
+ @Test
+ fun tryGetIntNotNull() {
+ val jsonObject = JSONObject("""{"key":3}""")
+ assertEquals(3, jsonObject.tryGetInt("key"))
+ }
+
+ @Test
+ fun mergeWith() {
+ val merged = JSONObject(
+ mapOf(
+ "toKeep" to 3,
+ "toOverride" to "OHNOZ",
+ ),
+ )
+
+ merged.mergeWith(
+ JSONObject(
+ mapOf(
+ "newKey" to 5,
+ "toOverride" to "YAY",
+ ),
+ ),
+ )
+
+ val expectedObject = JSONObject(
+ mapOf(
+ "toKeep" to 3,
+ "toOverride" to "YAY",
+ "newKey" to 5,
+ ),
+ )
+ assertEquals(expectedObject.toString(), merged.toString())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt
new file mode 100644
index 0000000000..b41997bc19
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/BundleTest.kt
@@ -0,0 +1,66 @@
+/* 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.support.ktx.android.os
+
+import androidx.core.os.bundleOf
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.base.crash.Breadcrumb
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Date
+
+@RunWith(AndroidJUnit4::class)
+class BundleTest {
+
+ @Test
+ fun `bundles with different sizes should not be equals`() {
+ val small = bundleOf(
+ "hello" to "world",
+ )
+ val big = bundleOf(
+ "hello" to "world",
+ "foo" to "bar",
+ )
+ assertFalse(small.contentEquals(big))
+ }
+
+ @Test
+ fun `bundles with arrays should be equal`() {
+ val (bundle1, bundle2) = (0..1).map {
+ bundleOf(
+ "str" to "world",
+ "int" to 0,
+ "boolArray" to booleanArrayOf(true, false),
+ "byteArray" to "test".toByteArray(),
+ "charArray" to "test".toCharArray(),
+ "doubleArray" to doubleArrayOf(0.0, 1.1),
+ "floatArray" to floatArrayOf(1f, 2f),
+ "intArray" to intArrayOf(0, 1, 2),
+ "longArray" to longArrayOf(0L, 1L),
+ "shortArray" to shortArrayOf(1, 2),
+ "typedArray" to arrayOf("foo", "bar"),
+ "nestedBundle" to bundleOf(),
+ )
+ }
+ assertTrue(bundle1.contentEquals(bundle2))
+ }
+
+ @Test
+ fun `bundles with parcelables should be equal`() {
+ val date = Date()
+ val (bundle1, bundle2) = (0..1).map {
+ bundleOf(
+ "crumbs" to Breadcrumb(
+ message = "msg",
+ level = Breadcrumb.Level.DEBUG,
+ date = date,
+ ),
+ )
+ }
+ assertTrue(bundle1.contentEquals(bundle2))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt
new file mode 100644
index 0000000000..4854110c2f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/StrictModeTest.kt
@@ -0,0 +1,64 @@
+/* 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.support.ktx.android.os
+
+import android.os.StrictMode
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class StrictModeTest {
+
+ @Test
+ fun `strict mode policy should be restored`() {
+ StrictMode.setThreadPolicy(
+ StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .penaltyLog()
+ .build(),
+ )
+ val policy = StrictMode.getThreadPolicy()
+ assertEquals(
+ 27,
+ StrictMode.allowThreadDiskReads().resetAfter {
+ // Comparing via toString because StrictMode.ThreadPolicy does not redefine equals() and each time
+ // setThreadPolicy is called a new ThreadPolicy object is created (although the mask is the same)
+ assertNotEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ 27
+ },
+ )
+ assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ }
+
+ @Test
+ fun `strict mode policy should be restored if function block throws an error`() {
+ val policy = StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .penaltyLog()
+ .build().apply {
+ StrictMode.setThreadPolicy(this)
+ }
+
+ val exceptionCaught: Boolean
+
+ assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+
+ try {
+ StrictMode.allowThreadDiskReads().resetAfter {
+ assertNotEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ throw RuntimeException("Boing!")
+ }
+ } catch (e: RuntimeException) {
+ exceptionCaught = true
+ }
+
+ assertTrue(exceptionCaught)
+ assertEquals(policy.toString(), StrictMode.getThreadPolicy().toString())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt
new file mode 100644
index 0000000000..5661998034
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/os/VibratorTest.kt
@@ -0,0 +1,45 @@
+/* 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.support.ktx.android.os
+
+import android.os.Build
+import android.os.VibrationEffect
+import android.os.VibrationEffect.DEFAULT_AMPLITUDE
+import android.os.Vibrator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class VibratorTest {
+
+ private lateinit var vibrator: Vibrator
+
+ @Before
+ fun setup() {
+ vibrator = mock()
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.O])
+ @Test
+ fun `vibrateOneShot uses VibrationEffect on new APIs`() {
+ vibrator.vibrateOneShot(50L)
+
+ verify(vibrator).vibrate(VibrationEffect.createOneShot(50L, DEFAULT_AMPLITUDE))
+ }
+
+ @Suppress("Deprecation")
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Test
+ fun `vibrateOneShot uses vibrate on new APIs`() {
+ vibrator.vibrateOneShot(100L)
+
+ verify(vibrator).vibrate(100L)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt
new file mode 100644
index 0000000000..353761d7ba
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/Base64Test.kt
@@ -0,0 +1,21 @@
+/* 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.support.ktx.android.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class Base64Test {
+
+ @Test
+ fun `encodeToUriString contains required data URI format`() {
+ val s = Base64.encodeToUriString("foo")
+ Assert.assertTrue(s.contains("data:text/html;base64,"))
+ Assert.assertTrue(s.contains("Zm9v"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt
new file mode 100644
index 0000000000..fe92f15c4b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/DisplayMetricsTest.kt
@@ -0,0 +1,27 @@
+/* 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.support.ktx.android.util
+
+import android.util.DisplayMetrics
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class DisplayMetricsTest {
+
+ @Test
+ fun `dp returns same value as manual conversion`() {
+ val metrics = testContext.resources.displayMetrics
+
+ for (i in 1..500) {
+ val px = (i * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
+ Assert.assertEquals(px, i.dpToPx(metrics))
+ Assert.assertNotEquals(0, i.dpToPx(metrics))
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt
new file mode 100644
index 0000000000..341588f695
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/util/JsonReaderKtTest.kt
@@ -0,0 +1,72 @@
+/* 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.support.ktx.android.util
+
+import android.util.JsonReader
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class JsonReaderKtTest {
+ @Test
+ fun `nextStringOrNull - string`() {
+ val json = """{ "key": "value" }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertEquals("value", reader.nextStringOrNull())
+ }
+
+ @Test
+ fun `nextStringOrNull - null`() {
+ val json = """{ "key": null }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertNull(reader.nextStringOrNull())
+ }
+
+ @Test
+ fun `nextBooleanOrNull - true`() {
+ val json = """{ "key": true }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertTrue(reader.nextBooleanOrNull()!!)
+ }
+
+ @Test
+ fun `nextBooleanOrNull - false`() {
+ val json = """{ "key": false }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertFalse(reader.nextBooleanOrNull()!!)
+ }
+
+ @Test
+ fun `nextBooleanOrNull - null`() {
+ val json = """{ "key": null }"""
+ val reader = JsonReader(json.reader())
+
+ reader.beginObject()
+ reader.nextName()
+
+ assertNull(reader.nextBooleanOrNull())
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt
new file mode 100644
index 0000000000..f438039b2b
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ActivityTest.kt
@@ -0,0 +1,143 @@
+/* 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.support.ktx.android.view
+
+import android.app.Activity
+import android.os.Build
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.Window
+import android.view.WindowInsets
+import android.view.WindowManager
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.annotation.Config
+
+@Suppress("DEPRECATION")
+@RunWith(AndroidJUnit4::class)
+class ActivityTest {
+
+ private lateinit var activity: Activity
+ private lateinit var window: Window
+ private lateinit var decorView: View
+ private lateinit var viewTreeObserver: ViewTreeObserver
+ private lateinit var windowInsets: WindowInsets
+ private lateinit var insetsController: WindowInsetsControllerCompat
+ private lateinit var layoutParams: WindowManager.LayoutParams
+
+ @Before
+ fun setup() {
+ activity = mock()
+ window = mock()
+ decorView = mock()
+ viewTreeObserver = mock()
+ windowInsets = mock()
+ insetsController = mock()
+ layoutParams = WindowManager.LayoutParams()
+
+ `when`(activity.window).thenReturn(window)
+ `when`(window.decorView).thenReturn(decorView)
+ `when`(window.decorView.viewTreeObserver).thenReturn(viewTreeObserver)
+ `when`(window.decorView.onApplyWindowInsets(windowInsets)).thenReturn(windowInsets)
+ `when`(window.attributes).thenReturn(layoutParams)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.P])
+ fun `GIVEN Android version P WHEN enterImmersiveMode is called THEN systems bars are hidden, inset listener is set and notch flags are set to extend view into notch area`() {
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ verify(window.decorView).setOnApplyWindowInsetsListener(any())
+
+ verify(window).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, layoutParams.layoutInDisplayCutoutMode)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O_MR1])
+ fun `GIVEN Android version O_MR1 WHEN enterImmersiveMode is called THEN systems bars are hidden, inset listener is set and notch flags are not being set`() {
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ verify(window.decorView).setOnApplyWindowInsetsListener(any())
+
+ verify(window, never()).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ }
+
+ @Test
+ fun `GIVEN enterImmersiveMode was called WHEN window insets are changed THEN insetsController hides system bars and sets bars behaviour again`() {
+ val insetListenerCaptor = argumentCaptor<View.OnApplyWindowInsetsListener>()
+ doReturn(30).`when`(windowInsets).systemWindowInsetTop
+
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+
+ verify(window.decorView).setOnApplyWindowInsetsListener(insetListenerCaptor.capture())
+ insetListenerCaptor.value.onApplyWindowInsets(window.decorView, windowInsets)
+
+ verify(insetsController, times(2)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(2)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+
+ @Test
+ fun `GIVEN enterImmersiveMode was called WHEN window insets are not changed THEN insetsController does nothing`() {
+ val insetListenerCaptor = argumentCaptor<View.OnApplyWindowInsetsListener>()
+ doReturn(0).`when`(windowInsets).systemWindowInsetTop
+
+ activity.enterImmersiveMode(insetsController)
+
+ verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+
+ verify(window.decorView).setOnApplyWindowInsetsListener(insetListenerCaptor.capture())
+ insetListenerCaptor.value.onApplyWindowInsets(window.decorView, windowInsets)
+
+ verify(insetsController, times(1)).hide(WindowInsetsCompat.Type.systemBars())
+ verify(insetsController, times(1)).systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ }
+
+ @Test
+ fun `WHEN exitImmersiveMode is called THEN insetsController shows system bars and OnApplyWindowInsetsListener is cleared`() {
+ activity.exitImmersiveMode(insetsController)
+
+ verify(insetsController).show(WindowInsetsCompat.Type.systemBars())
+ verify(window.decorView).setOnApplyWindowInsetsListener(null)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.P])
+ fun `GIVEN Android version P WHEN exitImmersiveMode is called THEN notch flags are reset to defaults`() {
+ activity.exitImmersiveMode()
+
+ verify(window).clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ assertEquals(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, layoutParams.layoutInDisplayCutoutMode)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O_MR1])
+ fun `GIVEN Android version O_MR1 WHEN exitImmersiveMode is called THEN notch flags were not being set`() {
+ activity.exitImmersiveMode()
+
+ verify(window, never()).setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt
new file mode 100644
index 0000000000..355ae9f9bb
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/MotionEventKtTest.kt
@@ -0,0 +1,57 @@
+/* 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.support.ktx.android.view
+
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MotionEventKtTest {
+
+ private lateinit var subject: MotionEvent
+ private lateinit var subjectSpy: MotionEvent
+
+ @Before
+ fun setUp() {
+ subject = MotionEvent.obtain(100, 100, ACTION_DOWN, 0f, 0f, 0)
+ subjectSpy = spy(subject)
+ }
+
+ @Test
+ fun `WHEN use is called without an exception THEN the object is recycled`() {
+ subjectSpy.use {}
+ verify(subjectSpy, times(1)).recycle()
+ }
+
+ @Test
+ fun `WHEN use is called with an exception THEN the object is recycled`() {
+ try { subjectSpy.use { throw IllegalStateException("Catch me!") } } catch (e: Exception) { /* Do nothing */ }
+ verify(subjectSpy, times(1)).recycle()
+ }
+
+ @Test
+ fun `WHEN use is called and its function returns a value THEN that value is returned`() {
+ val expected = 47
+ assertEquals(expected, subject.use { expected })
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `WHEN use is called and its function throws an exception THEN that exception is thrown`() {
+ subject.use { throw IllegalStateException() }
+ }
+
+ @Test
+ fun `WHEN use is called THEN the use function's argument is the use receiver`() {
+ subject.use { assertEquals(subject, it) }
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt
new file mode 100644
index 0000000000..e8c4c46321
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/TextViewTest.kt
@@ -0,0 +1,98 @@
+/* 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.support.ktx.android.view
+
+import android.graphics.drawable.Drawable
+import android.widget.EditText
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertArrayEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class TextViewTest {
+
+ @Test
+ fun `putCompoundDrawablesRelative defaults to all null`() {
+ val view = TextView(testContext)
+
+ view.putCompoundDrawablesRelative()
+
+ assertArrayEquals(
+ arrayOf<Drawable?>(null, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelativeWithIntrinsicBounds defaults to all null`() {
+ val view = TextView(testContext)
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds()
+
+ assertArrayEquals(
+ arrayOf<Drawable?>(null, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelative should set drawableStart and drawableEnd`() {
+ val view = EditText(testContext)
+ val drawable: Drawable = mock()
+
+ view.putCompoundDrawablesRelative(start = drawable)
+
+ assertArrayEquals(
+ arrayOf(drawable, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+
+ view.putCompoundDrawablesRelative(end = drawable)
+
+ assertArrayEquals(
+ arrayOf(null, null, drawable, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelativeWithIntrinsicBounds should set drawableStart and drawableEnd`() {
+ val view = EditText(testContext)
+ val drawable: Drawable = mock()
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds(start = drawable)
+
+ assertArrayEquals(
+ arrayOf(drawable, null, null, null),
+ view.compoundDrawablesRelative,
+ )
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds(end = drawable)
+
+ assertArrayEquals(
+ arrayOf(null, null, drawable, null),
+ view.compoundDrawablesRelative,
+ )
+ }
+
+ @Test
+ fun `putCompoundDrawablesRelative should call setCompoundDrawablesRelative`() {
+ val view: TextView = mock()
+ val drawable: Drawable = mock()
+
+ view.putCompoundDrawablesRelative(top = drawable)
+
+ verify(view).setCompoundDrawablesRelative(null, drawable, null, null)
+
+ view.putCompoundDrawablesRelativeWithIntrinsicBounds(bottom = drawable)
+
+ verify(view).setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, drawable)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt
new file mode 100644
index 0000000000..d2e537232f
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/ViewTest.kt
@@ -0,0 +1,221 @@
+/* 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.support.ktx.android.view
+
+import android.app.Activity
+import android.content.Context
+import android.os.Looper.getMainLooper
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import mozilla.components.support.base.android.Padding
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Robolectric
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowLooper
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `showKeyboard should request focus`() {
+ val view = EditText(testContext)
+ assertFalse(view.hasFocus())
+
+ view.showKeyboard()
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
+
+ assertTrue(view.hasFocus())
+ }
+
+ @Test
+ fun `hideKeyboard should hide soft keyboard`() {
+ val view = mock<View>()
+ val context = mock<Context>()
+ val imm = mock<InputMethodManager>()
+ `when`(view.context).thenReturn(context)
+ `when`(context.getSystemService(InputMethodManager::class.java)).thenReturn(imm)
+
+ view.hideKeyboard()
+
+ verify(imm).hideSoftInputFromWindow(view.windowToken, 0)
+ }
+
+ @Test
+ fun `setPadding should set padding`() {
+ val view = TextView(testContext)
+
+ assertEquals(view.paddingLeft, 0)
+ assertEquals(view.paddingTop, 0)
+ assertEquals(view.paddingRight, 0)
+ assertEquals(view.paddingBottom, 0)
+
+ view.setPadding(Padding(16, 20, 24, 28))
+
+ assertEquals(view.paddingLeft, 16)
+ assertEquals(view.paddingTop, 20)
+ assertEquals(view.paddingRight, 24)
+ assertEquals(view.paddingBottom, 28)
+ }
+
+ @Test
+ fun `getRectWithViewLocation should transform getLocationInWindow method values`() {
+ val view = spy(View(testContext))
+ doAnswer { invocation ->
+ val locationInWindow = (invocation.getArgument(0) as IntArray)
+ locationInWindow[0] = 100
+ locationInWindow[1] = 200
+ locationInWindow
+ }.`when`(view).getLocationInWindow(any())
+
+ `when`(view.width).thenReturn(150)
+ `when`(view.height).thenReturn(250)
+
+ val outRect = view.getRectWithViewLocation()
+
+ assertEquals(100, outRect.left)
+ assertEquals(200, outRect.top)
+ assertEquals(250, outRect.right)
+ assertEquals(450, outRect.bottom)
+ }
+
+ @Test
+ fun `called after next layout`() {
+ val view = View(testContext)
+
+ var callbackInvoked = false
+ view.onNextGlobalLayout {
+ callbackInvoked = true
+ }
+
+ assertFalse(callbackInvoked)
+
+ view.viewTreeObserver.dispatchOnGlobalLayout()
+
+ assertTrue(callbackInvoked)
+ }
+
+ @Test
+ fun `remove listener after next layout`() {
+ val view = spy(View(testContext))
+ val viewTreeObserver = spy(view.viewTreeObserver)
+ doReturn(viewTreeObserver).`when`(view).viewTreeObserver
+
+ view.onNextGlobalLayout {}
+
+ verify(viewTreeObserver, never()).removeOnGlobalLayoutListener(any())
+
+ viewTreeObserver.dispatchOnGlobalLayout()
+
+ verify(viewTreeObserver).removeOnGlobalLayoutListener(any())
+ }
+
+ @Test
+ fun `can dispatch coroutines to view scope`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(view.isAttachedToWindow)
+
+ val latch = CountDownLatch(1)
+ var coroutineExecuted = false
+
+ view.toScope().launch {
+ coroutineExecuted = true
+ latch.countDown()
+ }
+
+ latch.await(10, TimeUnit.SECONDS)
+
+ assertTrue(coroutineExecuted)
+ }
+
+ @Test
+ fun `scope is cancelled when view is detached`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).create().get()
+ val view = View(testContext)
+ activity.windowManager.addView(view, WindowManager.LayoutParams(100, 100))
+ shadowOf(getMainLooper()).idle()
+
+ val scope = view.toScope()
+
+ assertTrue(view.isAttachedToWindow)
+ assertTrue(scope.isActive)
+
+ activity.windowManager.removeView(view)
+ shadowOf(getMainLooper()).idle()
+
+ assertFalse(view.isAttachedToWindow)
+ assertFalse(scope.isActive)
+
+ val latch = CountDownLatch(1)
+ var coroutineExecuted = false
+
+ scope.launch {
+ coroutineExecuted = true
+ latch.countDown()
+ }
+
+ assertFalse(latch.await(5, TimeUnit.SECONDS))
+ assertFalse(coroutineExecuted)
+ }
+
+ @Test
+ fun `correct view is found in the hierarchy matching the predicate`() {
+ val root = LinearLayout(testContext)
+ val layout = RelativeLayout(testContext)
+ val testView = TestView(testContext)
+
+ layout.addView(testView)
+ root.addView(layout)
+
+ val rootFound = root.findViewInHierarchy { it is LinearLayout }
+
+ assertNotNull(rootFound)
+ assertTrue(rootFound is LinearLayout)
+
+ val layoutFound = root.findViewInHierarchy { it is RelativeLayout }
+
+ assertNotNull(layoutFound)
+ assertTrue(layoutFound is RelativeLayout)
+
+ val testViewFound = root.findViewInHierarchy { it is TestView }
+
+ assertNotNull(testViewFound)
+ assertTrue(testViewFound is TestView)
+ }
+
+ private class TestView(context: Context) : View(context)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt
new file mode 100644
index 0000000000..24e49b9ce5
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/view/WindowTest.kt
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.ktx.android.view
+
+import android.graphics.Color
+import android.os.Build
+import android.view.View
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations.openMocks
+import org.robolectric.util.ReflectionHelpers.setStaticField
+import kotlin.reflect.jvm.javaField
+
+/**
+ * **Note** Tests for isAppearanceLightStatusBars are in WindowKtTest.
+ */
+@RunWith(AndroidJUnit4::class)
+class WindowTest {
+
+ @Mock private lateinit var window: Window
+
+ @Mock private lateinit var decorView: View
+
+ @Before
+ fun setup() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, 0)
+
+ openMocks(this)
+
+ `when`(window.decorView).thenAnswer { decorView }
+ }
+
+ @After
+ fun teardown() = setStaticField(Build.VERSION::SDK_INT.javaField, 0)
+
+ @Test
+ fun `GIVEN a color WHEN setStatusBarTheme THEN sets the status bar color`() {
+ window.setStatusBarTheme(Color.BLUE)
+ verify(window).statusBarColor = Color.BLUE
+
+ window.setStatusBarTheme(Color.RED)
+ verify(window).statusBarColor = Color.RED
+ }
+
+ @Test
+ fun `GIVEN Android 8 & no args WHEN setNavigationBarTheme THEN no colors are set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme()
+
+ verifyNoMoreInteractions(window)
+ }
+
+ @Test
+ fun `GIVEN Android 8 & has nav bar color WHEN setNavigationBarTheme THEN only the nav bar color is set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme(navBarColor = Color.MAGENTA)
+
+ // We can't verify against the navigationBarDividerColor directly due to using SDK O_MR1 so we'll verify using ordering.
+ val inOrder = inOrder(window)
+ inOrder.verify(window).navigationBarColor = Color.MAGENTA
+ // Called for createWindowInsetsController()
+ inOrder.verify(window, times(2)).decorView
+ inOrder.verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun `GIVEN Android 8 & has nav bar divider color WHEN setNavigationBarTheme THEN no colors are set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme(navBarDividerColor = Color.DKGRAY)
+
+ verifyNoMoreInteractions(window)
+ }
+
+ @Test
+ fun `GIVEN Android 8 & all args WHEN setNavigationBarTheme THEN only the nav bar color is set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.O_MR1)
+ window.setNavigationBarTheme(navBarColor = Color.MAGENTA, navBarDividerColor = Color.DKGRAY)
+
+ // We can't verify against the navigationBarDividerColor directly due to using SDK O_MR1 so we'll verify using ordering.
+ val inOrder = inOrder(window)
+ inOrder.verify(window).navigationBarColor = Color.MAGENTA
+ // Called for createWindowInsetsController()
+ inOrder.verify(window, times(2)).decorView
+ inOrder.verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun `GIVEN Android 9 & no args WHEN setNavigationBarTheme THEN the nav bar divider color is set to default`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme()
+
+ verify(window, never()).navigationBarColor
+ verify(window).navigationBarDividerColor = 0
+ }
+
+ @Test
+ fun `GIVEN Android 9 has nav bar color WHEN setNavigationBarTheme THEN the nav bar color is set & nav bar divider color set to default`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme(navBarColor = Color.BLUE)
+
+ verify(window).navigationBarColor = Color.BLUE
+ verify(window).navigationBarDividerColor = 0
+ }
+
+ @Test
+ fun `GIVEN Android 9 has nav bar divider color WHEN setNavigationBarTheme THEN only the nav bar divider color is set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme(navBarDividerColor = Color.GREEN)
+
+ verify(window, never()).navigationBarColor
+ verify(window).navigationBarDividerColor = Color.GREEN
+ }
+
+ @Test
+ fun `GIVEN Android 9 & all args WHEN setNavigationBarTheme THEN the nav bar & nav bar divider colors are set`() {
+ setStaticField(Build.VERSION::SDK_INT.javaField, Build.VERSION_CODES.P)
+ window.setNavigationBarTheme(navBarColor = Color.YELLOW, navBarDividerColor = Color.CYAN)
+
+ verify(window).navigationBarColor = Color.YELLOW
+ verify(window).navigationBarDividerColor = Color.CYAN
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt
new file mode 100644
index 0000000000..a03e88ba87
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/android/widget/TextViewTest.kt
@@ -0,0 +1,142 @@
+/* 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.support.ktx.android.widget
+
+import android.view.View
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TextViewTest {
+
+ @Test
+ fun `check text size is set to the maximum allowable size specified by the height`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 200f
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(94f, textView.textSize)
+ }
+
+ @Test
+ fun `check text size is not adjusted if it is not larger than the allowable size`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 10f
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(10f, textView.textSize)
+ }
+
+ @Test
+ fun `check adjusted text size takes the default ascender padding into account`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(50f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(44f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.adjustMaxTextSize(heightSpec, 25)
+ assertEquals(25f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.adjustMaxTextSize(heightSpec, 25)
+ assertEquals(50f, textView.textSize)
+ }
+
+ @Test
+ fun `check text size is the same as the maximum adjusted text size`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(56, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 50f
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(50f, textView.textSize)
+
+ textView.textSize = 56f
+ textView.includeFontPadding = false
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(56f, textView.textSize)
+ }
+
+ @Test
+ fun `check custom padding affects text size`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.setPadding(5, 5, 5, 5)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(40f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 5, 0, 5)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(34f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.setPadding(5, 0, 5, 0)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(44f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.setPadding(0, 5, 0, 0)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(39f, textView.textSize)
+ }
+
+ @Test
+ fun `check negative available height results in text size 0`() {
+ val textView = TextView(testContext)
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.setPadding(0, 25, 0, 25)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 0, 0, 0)
+ textView.adjustMaxTextSize(heightSpec, 51)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = false
+ textView.setPadding(0, 26, 0, 25)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 25, 0, 25)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+
+ textView.textSize = 100f
+ textView.includeFontPadding = true
+ textView.setPadding(0, 1000, 0, 1000)
+ textView.adjustMaxTextSize(heightSpec)
+ assertEquals(100f, textView.textSize)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt
new file mode 100644
index 0000000000..2122ed9562
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/java/io/FileKtTest.kt
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.ktx.java.io
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.File
+import java.util.*
+class FileKtTest {
+
+ @Test
+ fun truncateDirectory() {
+ val root = File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString())
+ assertTrue(root.mkdir())
+
+ val file1 = File(root, "file1")
+ assertTrue(file1.createNewFile())
+
+ val file2 = File(root, "file2")
+ assertTrue(file2.createNewFile())
+
+ val dir1 = File(root, "dir1")
+ assertTrue(dir1.mkdir())
+
+ val dir2 = File(root, "dir2")
+ assertTrue(dir2.mkdir())
+
+ val file3 = File(dir2, "file3")
+ file3.createNewFile()
+
+ assertEquals(4, root.listFiles()?.size)
+
+ root.truncateDirectory()
+
+ assertEquals(0, root.listFiles()?.size)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt
new file mode 100644
index 0000000000..34b9b15e5e
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/ByteArrayTest.kt
@@ -0,0 +1,29 @@
+/* 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.support.ktx.kotlin
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ByteArrayTest {
+
+ @Test
+ fun toHexString() {
+ val stringValue = "Android Components"
+ assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString())
+ assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(-1))
+ assertEquals("416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(36))
+ assertEquals("00416e64726f696420436f6d706f6e656e7473", stringValue.toByteArray().toHexString(38))
+ }
+
+ @Test
+ fun toSha256Digest() {
+ val stringValue = "Android Components"
+ assertEquals(
+ "d2d01f10a9700b60740bdd20c60839dcf6b82be9e6a02719d564146cbe32d68f",
+ stringValue.toByteArray().toSha256Digest().toHexString(),
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt
new file mode 100644
index 0000000000..669dc01088
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/CollectionKtTest.kt
@@ -0,0 +1,70 @@
+/* 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.support.ktx.kotlin
+
+import org.junit.Assert
+import org.junit.Test
+
+class CollectionKtTest {
+
+ @Test
+ fun `cross product of each element is called exactly`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ var counter = 0
+ numbers.crossProduct(letters) { _, _ ->
+ counter++
+ }
+
+ Assert.assertEquals(numbers.size * letters.size, counter)
+ }
+
+ @Test
+ fun `cross product of each element is of the same type`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ numbers.crossProduct(letters) { number, letter ->
+ Assert.assertEquals(Int::class, number::class)
+ Assert.assertEquals(Char::class, letter::class)
+ }
+ }
+
+ @Test
+ fun `cross product of each pair is in order`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ val assertions = arrayOf(
+ 1 to 'a',
+ 1 to 'b',
+ 1 to 'c',
+ 2 to 'a',
+ 2 to 'b',
+ 2 to 'c',
+ 3 to 'a',
+ 3 to 'b',
+ 3 to 'c',
+ )
+ var position = 0
+ numbers.crossProduct(letters) { number, letter ->
+ Assert.assertEquals(assertions[position].first, number)
+ Assert.assertEquals(assertions[position].second, letter)
+ position++
+ }
+ }
+
+ @Suppress("USELESS_IS_CHECK")
+ @Test
+ fun `cross product result is list of return type`() {
+ val numbers = listOf(1, 2, 3)
+ val letters = listOf('a', 'b', 'c')
+ val result = numbers.crossProduct(letters) { number, letter ->
+ number to letter
+ }
+ Assert.assertTrue(result is List)
+ Assert.assertEquals(Pair::class, result[0]::class)
+ Assert.assertEquals(Int::class, result[0].first::class)
+ Assert.assertEquals(Char::class, result[0].second::class)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
new file mode 100644
index 0000000000..89ade2aace
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt
@@ -0,0 +1,595 @@
+/* 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.support.ktx.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Calendar
+import java.util.Calendar.MILLISECOND
+
+const val PUNYCODE = "xn--kpry57d"
+const val IDN = "台灣"
+
+@RunWith(AndroidJUnit4::class)
+class StringTest {
+
+ private val publicSuffixList = PublicSuffixList(testContext)
+
+ @Test
+ fun isUrl() {
+ assertTrue("mozilla.org".isUrl())
+ assertTrue(" mozilla.org ".isUrl())
+ assertTrue("http://mozilla.org".isUrl())
+ assertTrue("https://mozilla.org".isUrl())
+ assertTrue("file://somefile.txt".isUrl())
+ assertTrue("http://mozilla".isUrl())
+ assertTrue("http://192.168.255.255".isUrl())
+ assertTrue("about:crashcontent".isUrl())
+ assertTrue(" about:crashcontent ".isUrl())
+ assertTrue("sample:about ".isUrl())
+
+ assertFalse("mozilla".isUrl())
+ assertFalse("mozilla android".isUrl())
+ assertFalse(" mozilla android ".isUrl())
+ assertFalse("Tweet:".isUrl())
+ assertFalse("inurl:mozilla.org advanced search".isUrl())
+ assertFalse("what is about:crashes".isUrl())
+
+ val extraText = "Check out @asa’s Tweet: https://twitter.com/asa/status/123456789?s=09"
+ val url = extraText.split(" ").find { it.isUrl() }
+ assertNotEquals("Tweet:", url)
+ }
+
+ @Test
+ fun toNormalizedUrl() {
+ val expectedUrl = "http://mozilla.org"
+ assertEquals(expectedUrl, "http://mozilla.org".toNormalizedUrl())
+ assertEquals(expectedUrl, " http://mozilla.org ".toNormalizedUrl())
+ assertEquals(expectedUrl, "mozilla.org".toNormalizedUrl())
+ }
+
+ @Test
+ fun isPhone() {
+ assertTrue("tel:+1234567890".isPhone())
+ assertTrue(" tel:+1234567890".isPhone())
+ assertTrue("tel:+1234567890 ".isPhone())
+ assertTrue("tel:+1234567890 ".isPhone())
+ assertTrue("TEL:+1234567890".isPhone())
+ assertTrue("Tel:+1234567890".isPhone())
+
+ assertFalse("tel:word".isPhone())
+ }
+
+ @Test
+ fun isEmail() {
+ assertTrue("mailto:asa@mozilla.com".isEmail())
+ assertTrue(" mailto:asa@mozilla.com".isEmail())
+ assertTrue("mailto:asa@mozilla.com ".isEmail())
+ assertTrue("MAILTO:asa@mozilla.com".isEmail())
+ assertTrue("Mailto:asa@mozilla.com".isEmail())
+ }
+
+ @Test
+ fun geoLocation() {
+ assertTrue("geo:1,-1".isGeoLocation())
+ assertTrue("geo:1,-1;u=1".isGeoLocation())
+ assertTrue("geo:1,-1,0.5;u=1".isGeoLocation())
+ assertTrue(" geo:1,-1".isGeoLocation())
+ assertTrue("geo:1,-1 ".isGeoLocation())
+ assertTrue("GEO:1,-1".isGeoLocation())
+ assertTrue("Geo:1,-1".isGeoLocation())
+ }
+
+ @Test
+ fun toDate() {
+ val calendar = Calendar.getInstance()
+ calendar.set(2019, 10, 29, 0, 0, 0)
+ calendar[MILLISECOND] = 0
+ assertEquals(calendar.time, "2019-11-29".toDate("yyyy-MM-dd"))
+ calendar.set(2019, 10, 28, 0, 0, 0)
+ assertEquals(calendar.time, "2019-11-28".toDate("yyyy-MM-dd"))
+ assertNotNull("".toDate("yyyy-MM-dd"))
+ }
+
+ @Test
+ fun `string to date conversion using multiple formats`() {
+ assertEquals("2019-08-16T01:02".toDate("yyyy-MM-dd'T'HH:mm"), "2019-08-16T01:02".toDate())
+
+ assertEquals("2019-08-16T01:02:03".toDate("yyyy-MM-dd'T'HH:mm"), "2019-08-16T01:02:03".toDate())
+
+ assertEquals("2019-08-16".toDate("yyyy-MM-dd"), "2019-08-16".toDate())
+ }
+
+ @Test
+ fun sha1() {
+ assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", "".sha1())
+
+ assertEquals("0a4d55a8d778e5022fab701977c5d840bbc486d0", "Hello World".sha1())
+
+ assertEquals("8de545c123907e9f886ba2313560a0abef530594", "ßüöä@!§\$".sha1())
+ }
+
+ @Test
+ fun `Try Get Host From Url`() {
+ val urlTest = "http://www.example.com:1080/docs/resource1.html"
+ val new = urlTest.tryGetHostFromUrl()
+ assertEquals(new, "www.example.com")
+ }
+
+ @Test
+ fun `Try Get Host From Malformed Url`() {
+ val urlTest = "notarealurl"
+ val new = urlTest.tryGetHostFromUrl()
+ assertEquals(new, "notarealurl")
+ }
+
+ @Test
+ fun isSameOriginAs() {
+ // Host mismatch.
+ assertFalse("https://foo.bar".isSameOriginAs("https://foo.baz"))
+ // Scheme mismatch.
+ assertFalse("http://127.0.0.1".isSameOriginAs("https://127.0.0.1"))
+ // Port mismatch (implicit + explicit).
+ assertFalse("https://foo.bar:444".isSameOriginAs("https://foo.bar"))
+ // Port mismatch (explicit).
+ assertFalse("https://foo.bar:444".isSameOriginAs("https://foo.bar:555"))
+ // Port OK but scheme different.
+ assertFalse("https://foo.bar".isSameOriginAs("http://foo.bar:443"))
+ // Port OK (explicit) but scheme different.
+ assertFalse("https://foo.bar:443".isSameOriginAs("ftp://foo.bar:443"))
+
+ assertTrue("https://foo.bar".isSameOriginAs("https://foo.bar"))
+ assertTrue("https://foo.bar/bobo".isSameOriginAs("https://foo.bar/obob"))
+ assertTrue("https://foo.bar".isSameOriginAs("https://foo.bar:443"))
+ assertTrue("https://foo.bar:333".isSameOriginAs("https://foo.bar:333"))
+ }
+
+ @Test
+ fun isExtensionUrl() {
+ assertTrue("moz-extension://1232-abcd".isExtensionUrl())
+ assertFalse("mozilla.org".isExtensionUrl())
+ assertFalse("https://mozilla.org".isExtensionUrl())
+ assertFalse("http://mozilla.org".isExtensionUrl())
+ }
+
+ @Test
+ fun sanitizeURL() {
+ val expectedUrl = "http://mozilla.org"
+ assertEquals(expectedUrl, "\nhttp://mozilla.org\n".sanitizeURL())
+ }
+
+ @Test
+ fun isResourceUrl() {
+ assertTrue("resource://1232-abcd".isResourceUrl())
+ assertFalse("mozilla.org".isResourceUrl())
+ assertFalse("https://mozilla.org".isResourceUrl())
+ assertFalse("http://mozilla.org".isResourceUrl())
+ }
+
+ @Test
+ fun sanitizeFileName() {
+ var file = "/../../../../../../../../../../directory/file.......txt"
+ val fileName = "file.txt"
+
+ assertEquals(fileName, file.sanitizeFileName())
+
+ file = "/root/directory/file.txt"
+
+ assertEquals(fileName, file.sanitizeFileName())
+
+ assertEquals("file", "file".sanitizeFileName())
+
+ assertEquals("file", "file..".sanitizeFileName())
+
+ assertEquals("file", "file.".sanitizeFileName())
+
+ assertEquals("file", ".file".sanitizeFileName())
+
+ assertEquals("test.2020.12.01.txt", "test.2020.12.01.txt".sanitizeFileName())
+ }
+
+ @Test
+ fun `getDataUrlImageExtension returns a default extension if one cannot be extracted from the data url`() {
+ val base64Image = "data:;base64,testImage"
+
+ val result = base64Image.getDataUrlImageExtension()
+
+ assertEquals("jpg", result)
+ }
+
+ @Test
+ fun `getDataUrlImageExtension returns an extension based on the media type included in the the data url`() {
+ val base64Image = "data:image/gif;base64,testImage"
+
+ val result = base64Image.getDataUrlImageExtension()
+
+ assertEquals("gif", result)
+ }
+
+ @Test
+ fun `ifNullOrEmpty returns the same if this CharSequence is not null and not empty`() {
+ val randomString = "something"
+
+ assertSame(randomString, randomString.ifNullOrEmpty { "something else" })
+ }
+
+ @Test
+ fun `ifNullOrEmpty returns the invocation of the passed in argument if this CharSequence is null`() {
+ val nullString: String? = null
+ val validResult = "notNullString"
+
+ assertSame(validResult, nullString.ifNullOrEmpty { validResult })
+ }
+
+ @Test
+ fun `ifNullOrEmpty returns the invocation of the passed in argument if this CharSequence is empty`() {
+ val nullString = ""
+ val validResult = "notEmptyString"
+
+ assertSame(validResult, nullString.ifNullOrEmpty { validResult })
+ }
+
+ @Test
+ fun `getRepresentativeCharacter returns the correct representative character for the given urls`() {
+ assertEquals("M", "https://mozilla.org".getRepresentativeCharacter())
+ assertEquals("W", "http://wikipedia.org".getRepresentativeCharacter())
+ assertEquals("P", "http://plus.google.com".getRepresentativeCharacter())
+ assertEquals("E", "https://en.m.wikipedia.org/wiki/Main_Page".getRepresentativeCharacter())
+
+ // Stripping common prefixes
+ assertEquals("T", "http://www.theverge.com".getRepresentativeCharacter())
+ assertEquals("F", "https://m.facebook.com".getRepresentativeCharacter())
+ assertEquals("T", "https://mobile.twitter.com".getRepresentativeCharacter())
+
+ // Special urls
+ assertEquals("?", "file:///".getRepresentativeCharacter())
+ assertEquals("S", "file:///system/".getRepresentativeCharacter())
+ assertEquals("P", "ftp://people.mozilla.org/test".getRepresentativeCharacter())
+
+ // No values
+ assertEquals("?", "".getRepresentativeCharacter())
+
+ // Rubbish
+ assertEquals("Z", "zZz".getRepresentativeCharacter())
+ assertEquals("Ö", "ölkfdpou3rkjaslfdköasdfo8".getRepresentativeCharacter())
+ assertEquals("?", "_*+*'##".getRepresentativeCharacter())
+ assertEquals("ツ", "¯\\_(ツ)_/¯".getRepresentativeCharacter())
+ assertEquals("ಠ", "ಠ_ಠ Look of Disapproval".getRepresentativeCharacter())
+
+ // Non-ASCII
+ assertEquals("Ä", "http://www.ätzend.de".getRepresentativeCharacter())
+ assertEquals("名", "http://名がドメイン.com".getRepresentativeCharacter())
+ assertEquals("C", "http://√.com".getRepresentativeCharacter())
+ assertEquals("SS", "http://ß.de".getRepresentativeCharacter())
+ assertEquals("Ԛ", "http://ԛәлп.com/".getRepresentativeCharacter()) // cyrillic
+
+ // Punycode
+ assertEquals("X", "http://xn--tzend-fra.de".getRepresentativeCharacter()) // ätzend.de
+ assertEquals("X", "http://xn--V8jxj3d1dzdz08w.com".getRepresentativeCharacter()) // 名がドメイン.com
+
+ // Numbers
+ assertEquals("1", "https://www.1and1.com/".getRepresentativeCharacter())
+
+ // IP
+ assertEquals("1", "https://192.168.0.1".getRepresentativeCharacter())
+ }
+
+ @Test
+ fun `last4Digits returns a string with only last 4 digits `() {
+ assertEquals("8431", "371449635398431".last4Digits())
+ assertEquals("2345", "12345".last4Digits())
+ assertEquals("1234", "1234".last4Digits())
+ assertEquals("123", "123".last4Digits())
+ assertEquals("1", "1".last4Digits())
+ assertEquals("", "".last4Digits())
+ }
+
+ @Test
+ fun `when the full hostname cannot be displayed, elide labels starting from the front`() {
+ // See https://url.spec.whatwg.org/#url-rendering-elision
+ // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#eliding-urls
+
+ val display = "http://1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.com"
+ .shortened()
+
+ val split = display.split(".")
+
+ // If the list ends with 25.com...
+ assertEquals("25", split.dropLast(1).last())
+ // ...and each value is 1 larger than the last...
+ split.dropLast(1)
+ .map { it.toInt() }
+ .windowed(2, 1)
+ .forEach { (prev, next) ->
+ assertEquals(next, prev + 1)
+ }
+ // ...that means that all removed values came from the front of the list
+ }
+
+ @Test
+ fun `the registrable domain is always displayed`() {
+ // https://url.spec.whatwg.org/#url-rendering-elision
+ // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#eliding-urls
+
+ val bigRegistrableDomain = "evil-but-also-shockingly-long-registrable-domain.com"
+ assertTrue(
+ "https://login.your-bank.com.$bigRegistrableDomain/enter/your/password".shortened()
+ .contains(bigRegistrableDomain),
+ )
+ }
+
+ @Test
+ fun `url username and password fields should not be displayed`() {
+ // See https://url.spec.whatwg.org/#url-rendering-simplification
+ // See https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md#simplify
+
+ assertFalse("https://examplecorp.com@attacker.example/".shortened().contains("examplecorp"))
+ assertFalse("https://examplecorp.com@attacker.example/".shortened().contains("com"))
+ assertFalse("https://user:password@example.com/".shortened().contains("user"))
+ assertFalse("https://user:password@example.com/".shortened().contains("password"))
+ }
+
+ @Test
+ fun `eTLDs should not be dropped`() {
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11
+ "http://mozfreddyb.github.io/" shortenedShouldBecome "mozfreddyb.github.io"
+ "http://web.security.plumbing/" shortenedShouldBecome "web.security.plumbing"
+ }
+
+ @Test
+ fun `ipv4 addresses should be returned as is`() {
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11
+ "192.168.85.1" shortenedShouldBecome "192.168.85.1"
+ }
+
+ @Test
+ fun `about buildconfig should not be modified`() {
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11
+ "about:buildconfig" shortenedShouldBecome "about:buildconfig"
+ }
+
+ @Test
+ fun `encoded userinfo should still be considered userinfo`() {
+ "https://user:password%40really.evil.domain%2F@mail.google.com" shortenedShouldBecome "mail.google.com"
+ }
+
+ @Test
+ @Ignore("This would be more correct, but does not appear to be an attack vector")
+ fun `should decode DWORD IP addresses`() {
+ "https://16843009" shortenedShouldBecome "1.1.1.1"
+ }
+
+ @Test
+ @Ignore("This would be more correct, but does not appear to be an attack vector")
+ fun `should decode octal IP addresses`() {
+ "https://000010.000010.000010.000010" shortenedShouldBecome "8.8.8.8"
+ }
+
+ @Test
+ @Ignore("This would be more correct, but does not appear to be an attack vector")
+ fun `should decode hex IP addresses`() {
+ "http://0x01010101" shortenedShouldBecome "1.1.1.1"
+ }
+
+ // BEGIN test cases borrowed from desktop (shortUrl is used for Top Sites on new tab)
+ // Test cases are modified, as we show the eTLD
+ // (https://searchfox.org/mozilla-central/source/browser/components/newtab/test/unit/lib/ShortUrl.test.js)
+ @Test
+ fun `should return a blank string if url is blank`() {
+ "" shortenedShouldBecome ""
+ }
+
+ @Test
+ fun `should return the 'url' if not a valid url`() {
+ "something" shortenedShouldBecome "something"
+ "http:" shortenedShouldBecome "http:"
+ "http::double-colon" shortenedShouldBecome "http::double-colon"
+ // The largest allowed port is 65,535
+ "http://badport:65536/" shortenedShouldBecome "http://badport:65536/"
+ }
+
+ @Test
+ fun `should convert host to idn when calling shortURL`() {
+ "http://$PUNYCODE.blah.com" shortenedShouldBecome "$IDN.blah.com"
+ }
+
+ @Test
+ fun `should get the hostname from url`() {
+ "http://bar.com" shortenedShouldBecome "bar.com"
+ }
+
+ @Test
+ fun `should not strip out www if not first subdomain`() {
+ "http://foo.www.com" shortenedShouldBecome "foo.www.com"
+ "http://www.foo.www.com" shortenedShouldBecome "foo.www.com"
+ }
+
+ @Test
+ fun `should convert to lowercase`() {
+ "HTTP://FOO.COM" shortenedShouldBecome "foo.com"
+ }
+
+ @Test
+ fun `should not include the port`() {
+ "http://foo.com:8888" shortenedShouldBecome "foo.com"
+ }
+
+ @Test
+ fun `should return hostname for localhost`() {
+ "http://localhost:8000/" shortenedShouldBecome "localhost"
+ }
+
+ @Test
+ fun `should return hostname for ip address`() {
+ "http://127.0.0.1/foo" shortenedShouldBecome "127.0.0.1"
+ }
+
+ @Test
+ fun `should return etld for www gov uk (www-only non-etld)`() {
+ "https://www.gov.uk/countersigning" shortenedShouldBecome "gov.uk"
+ }
+
+ @Test
+ fun `should return idn etld for www-only non-etld`() {
+ "https://www.$PUNYCODE/foo" shortenedShouldBecome IDN
+ }
+
+ @Test
+ fun `file uri should return input`() {
+ "file:///foo/bar.txt" shortenedShouldBecome "file:///foo/bar.txt"
+ }
+
+ @Test
+ @Ignore("This behavior conflicts with https://bugzilla.mozilla.org/show_bug.cgi?id=1554984#c11")
+ fun `should return not the protocol for about`() {
+ "about:newtab" shortenedShouldBecome "newtab"
+ }
+
+ @Test
+ fun `should fall back to full url as a last resort`() {
+ "about:" shortenedShouldBecome "about:"
+ }
+ // END test cases borrowed from desktop
+
+ // BEGIN test cases borrowed from FFTV
+ // (https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/test/java/org/mozilla/focus/utils/TestFormattedDomain.java#228)
+ @Test
+ fun testIsIPv4RealAddress() {
+ assertTrue("192.168.1.1".isIpv4())
+ assertTrue("8.8.8.8".isIpv4())
+ assertTrue("63.245.215.20".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithProtocol() {
+ assertFalse("http://8.8.8.8".isIpv4())
+ assertFalse("https://8.8.8.8".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithPort() {
+ assertFalse("8.8.8.8:400".isIpv4())
+ assertFalse("8.8.8.8:1337".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithPath() {
+ assertFalse("8.8.8.8/index.html".isIpv4())
+ assertFalse("8.8.8.8/".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv4WithIPv6() {
+ assertFalse("2001:db8::1 ".isIpv4())
+ assertFalse("2001:db8:0:1:1:1:1:1".isIpv4())
+ assertFalse("[2001:db8:a0b:12f0::1]".isIpv4())
+ assertFalse("2001:db8: 3333:4444:5555:6666:1.2.3.4".isIpv4())
+ }
+
+ @Test
+ fun testIsIPv6WithIPv6() {
+ assertTrue("2001:db8::1".isIpv6())
+ assertTrue("2001:db8:0:1:1:1:1:1".isIpv6())
+ }
+
+ @Test
+ fun testIsIPv6WithIPv4() {
+ assertFalse("192.168.1.1".isIpv6())
+ assertFalse("8.8.8.8".isIpv6())
+ assertFalse("63.245.215.20".isIpv6())
+ }
+ // END test cases borrowed from FFTV
+
+ @Test
+ fun testStripCommonSubdomains() {
+ assertEquals("mozilla.org", ("mozilla.org").stripCommonSubdomains())
+ assertEquals("mozilla.org", ("www.mozilla.org").stripCommonSubdomains())
+ assertEquals("mozilla.org", ("m.mozilla.org").stripCommonSubdomains())
+ assertEquals("mozilla.org", ("mobile.mozilla.org").stripCommonSubdomains())
+ assertEquals("random.mozilla.org", ("random.mozilla.org").stripCommonSubdomains())
+ }
+
+ @Test
+ fun `GIVEN an invalid base64 image string WHEN converting it into bitmap THEN the result is null`() {
+ val invalidBase64BitmapString = "aa"
+ assertNull(invalidBase64BitmapString.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN a valid base64 png string WHEN converting it into bitmap THEN the result is not null and no exception is thrown`() {
+ val validBase64BitmapString = "data:image/png;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertNotNull(validBase64BitmapString.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN a valid base64 image string WHEN converting it into bitmap THEN the result is not null and no exception is thrown`() {
+ val validBase64JpegString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64JpgString = "data:image/jpg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64AnythingString = "data:image/anything;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertNotNull(validBase64JpegString.base64ToBitmap())
+ assertNotNull(validBase64JpgString.base64ToBitmap())
+ assertNotNull(validBase64AnythingString.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN invalid base64 image strings WHEN converting them into bitmap THEN the result is null`() {
+ val invalidBase64String = "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String2 = "data:image/jpg;base64;R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String3 = "image/jpg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertNull(invalidBase64String.base64ToBitmap())
+ assertNull(invalidBase64String2.base64ToBitmap())
+ assertNull(invalidBase64String3.base64ToBitmap())
+ }
+
+ @Test
+ fun `GIVEN a valid or invalid base64 image string WHEN extracting its raw content string THEN the result is correct`() {
+ val validBase64JpegString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64JpgString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val validBase64PngString = "data:image/jpeg;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String = "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ val invalidBase64String2 = "data:image/jpeg;base64;R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
+ assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64JpegString.extractBase6RawString())
+ assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64JpgString.extractBase6RawString())
+ assertEquals("R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=", validBase64PngString.extractBase6RawString())
+ assertNull(invalidBase64String.extractBase6RawString())
+ assertNull(invalidBase64String2.extractBase6RawString())
+ }
+
+ @Test
+ fun `GIVEN a URL with matching parameters WHEN testing if a URL contains query parameters THEN the result is true`() {
+ assertTrue("http://example.com?a".urlContainsQueryParameters("a"))
+ assertTrue("http://example.com?a&b&c".urlContainsQueryParameters("b"))
+ assertTrue("http://example.com?a=b".urlContainsQueryParameters("a=b"))
+ assertTrue("http://example.com?a=b&c=d&e=f".urlContainsQueryParameters("c=d"))
+ assertTrue("http://example.com?a=b&c=d&e=f#g=h".urlContainsQueryParameters("e=f"))
+ }
+
+ @Test
+ fun `GIVEN a URL without matching parameters WHEN testing if a URL contains query parameters THEN the result is false`() {
+ assertFalse("".urlContainsQueryParameters("a"))
+ assertFalse("!@#$%^&*()-+".urlContainsQueryParameters("a"))
+ assertFalse("http://example.com".urlContainsQueryParameters("a"))
+ assertFalse("http://example.com?a&b".urlContainsQueryParameters("c"))
+ assertFalse("http://example.com?a=b".urlContainsQueryParameters("a"))
+ assertFalse("http://example.com?a=b&c=d&e=f#g=h".urlContainsQueryParameters("g=h"))
+ }
+
+ private infix fun String.shortenedShouldBecome(expect: String) {
+ assertEquals(expect, this.shortened())
+ }
+
+ private fun String.shortened() = this.toShortUrl(publicSuffixList)
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt
new file mode 100644
index 0000000000..6c987b9a67
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/UtilsKtTest.kt
@@ -0,0 +1,38 @@
+/* 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.support.ktx.kotlinx.coroutines
+
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Test
+
+class UtilsKtTest {
+
+ @Test
+ fun throttle() = runTest(UnconfinedTestDispatcher()) {
+ val skipTime = 300L
+ var value = 0
+ val throttleBlock = throttleLatest<Int>(skipTime, coroutineScope = this) {
+ value = it
+ }
+
+ for (n in 1..300) {
+ throttleBlock(n)
+ }
+ assertNotEquals(300, value)
+
+ value = 0
+
+ for (n in 1..300) {
+ delay(skipTime)
+ throttleBlock(n)
+ }
+
+ assertEquals(300, value)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt
new file mode 100644
index 0000000000..11156c780a
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlinx/coroutines/flow/FlowKtTest.kt
@@ -0,0 +1,90 @@
+/* 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.support.ktx.kotlinx.coroutines.flow
+
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class FlowKtTest {
+
+ data class CharState(val value: Char)
+ data class IntState(val value: Int)
+
+ @Test
+ fun `ifAnyChanged operator with block`() = runTest {
+ val originalFlow = flowOf("banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home")
+
+ val items = originalFlow.ifAnyChanged { item -> arrayOf(item[0], item[1]) }.toList()
+
+ assertEquals(
+ listOf("banana", "bus", "apple", "big", "coconut", "circle", "home"),
+ items,
+ )
+ }
+
+ @Test
+ fun `ifAnyChanged operator uses structural equality`() = runTest {
+ val originalFlow = flowOf("banana", "bandanna", "bus", "apple", "big", "coconut", "circle", "home")
+
+ val items =
+ originalFlow.ifAnyChanged {
+ item ->
+ arrayOf(CharState(item[0]), CharState(item[1]))
+ }.toList()
+
+ assertEquals(
+ listOf("banana", "bus", "apple", "big", "coconut", "circle", "home"),
+ items,
+ )
+ }
+
+ @Test
+ fun `filterChanged operator`() = runTest {
+ val intFlow = flowOf(listOf(0), listOf(0, 1), listOf(0, 1, 2, 3), listOf(4), listOf(5, 6, 7, 8))
+ val identityItems = intFlow.filterChanged { item -> item }.toList()
+ assertEquals(listOf(0, 1, 2, 3, 4, 5, 6, 7, 8), identityItems)
+
+ val moduloFlow = flowOf(listOf(1), listOf(1, 2), listOf(3, 4, 5), listOf(3, 4))
+ val moduloItems = moduloFlow.filterChanged { item -> item % 2 }.toList()
+ assertEquals(listOf(1, 2, 3, 4, 5), moduloItems)
+
+ // Here we simulate a non-pure transform function (a function with a side-effect), causing
+ // the transformed values to be different for the same input.
+ var counter = 0
+ val sideEffectFlow = flowOf(listOf(0), listOf(0, 1), listOf(0, 1, 2, 3), listOf(4), listOf(5, 6, 7, 8))
+ val sideEffectItems = sideEffectFlow.filterChanged { item -> item + counter++ }.toList()
+ assertEquals(listOf(0, 0, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8), sideEffectItems)
+ }
+
+ @Test
+ fun `filterChanged operator check for structural equality`() = runTest {
+ val intFlow = flowOf(
+ listOf(IntState(0)),
+ listOf(IntState(0), IntState(1)),
+ listOf(IntState(0), IntState(1), IntState(2), IntState(3)),
+ listOf(IntState(4)),
+ listOf(IntState(5), IntState(6), IntState(7), IntState(8)),
+ )
+
+ val identityItems = intFlow.filterChanged { item -> item }.toList()
+ assertEquals(
+ listOf(
+ IntState(0),
+ IntState(1),
+ IntState(2),
+ IntState(3),
+ IntState(4),
+ IntState(5),
+ IntState(6),
+ IntState(7),
+ IntState(8),
+ ),
+ identityItems,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt
new file mode 100644
index 0000000000..a842a05c83
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/notification/NotificationTest.kt
@@ -0,0 +1,72 @@
+/* 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/. */
+
+@file:Suppress("SameParameterValue")
+
+package mozilla.components.support.ktx.notification
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.getSystemService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import mozilla.components.support.ktx.android.notification.ChannelData
+import mozilla.components.support.ktx.android.notification.ensureNotificationChannelExists
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.runner.RunWith
+
+internal const val NOTIFICATION_CHANNEL_ID = "NOTIFICATION_CHANNEL_ID"
+
+@RunWith(AndroidJUnit4::class)
+class NotificationTest {
+
+ @Test
+ fun `ensureChannelExists - creates a notification channel`() {
+ var setupChannelWasCalled = false
+ var afterCreatedChannelWasCalled = false
+
+ assertFalse(exists(channelId = NOTIFICATION_CHANNEL_ID))
+
+ val channelData = ChannelData(
+ NOTIFICATION_CHANNEL_ID,
+ android.R.string.ok,
+ NotificationManagerCompat.IMPORTANCE_LOW,
+ )
+
+ val setupChannel: NotificationChannel.() -> Unit = {
+ assertFalse(exists(channelId = NOTIFICATION_CHANNEL_ID))
+ setupChannelWasCalled = true
+ lockscreenVisibility = NotificationCompat.VISIBILITY_SECRET
+ }
+
+ val afterCreatedChannel: NotificationManager.() -> Unit = {
+ assertTrue(exists(channelId = NOTIFICATION_CHANNEL_ID))
+ afterCreatedChannelWasCalled = true
+
+ val channel = getChannel(NOTIFICATION_CHANNEL_ID)!!
+
+ assertTrue(channel.lockscreenVisibility == NotificationCompat.VISIBILITY_SECRET)
+ }
+
+ val channelId =
+ ensureNotificationChannelExists(testContext, channelData, setupChannel, afterCreatedChannel)
+
+ assertTrue(setupChannelWasCalled)
+ assertTrue(afterCreatedChannelWasCalled)
+ assertTrue(exists(channelId = NOTIFICATION_CHANNEL_ID))
+ assertEquals(channelId, NOTIFICATION_CHANNEL_ID)
+ }
+
+ private fun exists(channelId: String) = getChannel(channelId) != null
+
+ private fun getChannel(channelId: String): NotificationChannel? {
+ val notificationManager = testContext.getSystemService<NotificationManager>()!!
+ return notificationManager.getNotificationChannel(channelId)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt
new file mode 100644
index 0000000000..48cd9137f8
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/AtomicFileTest.kt
@@ -0,0 +1,94 @@
+/* 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.support.ktx.util
+
+import android.util.AtomicFile
+import androidx.core.util.writeText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.json.JSONException
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.verify
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class AtomicFileTest {
+
+ @Test
+ fun `writeString - Fails write on IOException`() {
+ val mockedFile: AtomicFile = mock()
+ doThrow(IOException::class.java).`when`(mockedFile).startWrite()
+
+ val result = mockedFile.writeString { "file_content" }
+
+ assertFalse(result)
+ verify(mockedFile).failWrite(any())
+ }
+
+ @Test
+ fun `writeString - Fails write on JSONException`() {
+ val mockedFile: AtomicFile = mock()
+ whenever(mockedFile.startWrite()).thenAnswer {
+ throw JSONException("")
+ }
+
+ val result = mockedFile.writeString { "file_content" }
+
+ assertFalse(result)
+ verify(mockedFile).failWrite(any())
+ }
+
+ @Test
+ fun `writeString - writes the content of the file`() {
+ val tempFile = File.createTempFile("temp", ".tmp")
+ val atomicFile = AtomicFile(tempFile)
+ atomicFile.writeString { "file_content" }
+
+ val result = atomicFile.writeString { "file_content" }
+ assertTrue(result)
+ }
+
+ @Test
+ fun `readAndDeserialize - Returns the content of the file`() {
+ val tempFile = File.createTempFile("temp", ".tmp")
+ val atomicFile = AtomicFile(tempFile)
+ atomicFile.writeText("file_content")
+
+ val fileContent = atomicFile.readAndDeserialize { it }
+ assertNotNull(fileContent)
+ assertEquals("file_content", fileContent)
+ }
+
+ @Test
+ fun `readAndDeserialize - Returns null on FileNotFoundException`() {
+ val mockedFile: AtomicFile = mock()
+ doThrow(FileNotFoundException::class.java).`when`(mockedFile).openRead()
+
+ val content = mockedFile.readAndDeserialize { it }
+ assertNull(content)
+ }
+
+ @Test
+ fun `readAndDeserialize - Returns null on JSONException`() {
+ val mockedFile: AtomicFile = mock()
+ whenever(mockedFile.openRead()).thenAnswer {
+ throw JSONException("")
+ }
+
+ val content = mockedFile.readAndDeserialize { it }
+ assertNull(content)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt
new file mode 100644
index 0000000000..b07e11f698
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/util/DisplayMetricsTest.kt
@@ -0,0 +1,51 @@
+/* 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.support.ktx.util
+
+import android.content.res.Resources
+import android.util.DisplayMetrics
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.android.util.spToPx
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.`when`
+
+@RunWith(AndroidJUnit4::class)
+class DisplayMetricsTest {
+ private lateinit var metrics: DisplayMetrics
+
+ @Before
+ fun setUp() {
+ metrics = mock(DisplayMetrics::class.java)
+ metrics.density = 3f
+ metrics.setToDefaults()
+
+ val resources: Resources = mock(Resources::class.java)
+ `when`(resources.displayMetrics).thenReturn(metrics)
+ }
+
+ @Test
+ fun `Float dpToPx returns correct value`() {
+ val floatValue = 10f
+
+ val result = floatValue.dpToPx(metrics)
+
+ assertEquals(metrics.density * floatValue, result)
+ }
+
+ @Test
+ fun `Float spToPx returns correct value`() {
+ val floatValue = 10f
+
+ val result = floatValue.spToPx(metrics)
+
+ @Suppress("DEPRECATION")
+ assertEquals(metrics.scaledDensity * floatValue, result)
+ }
+}
diff --git a/mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/support/ktx/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/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)
diff --git a/mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/ktx/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28