summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/feature/qr
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/feature/qr
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/feature/qr')
-rw-r--r--mobile/android/android-components/components/feature/qr/README.md42
-rw-r--r--mobile/android/android-components/components/feature/qr/build.gradle46
-rw-r--r--mobile/android/android-components/components/feature/qr/proguard-rules.pro21
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/AndroidManifest.xml7
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt145
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt786
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt71
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt298
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webpbin0 -> 148 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webpbin0 -> 100 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webpbin0 -> 104 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webpbin0 -> 140 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webpbin0 -> 202 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webpbin0 -> 140 bytes
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml35
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml7
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml10
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml8
-rw-r--r--mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml13
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt269
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt786
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt82
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.kt102
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/android-components/components/feature/qr/src/test/resources/robolectric.properties1
129 files changed, 3768 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/feature/qr/README.md b/mobile/android/android-components/components/feature/qr/README.md
new file mode 100644
index 0000000000..531cd6f8e4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/README.md
@@ -0,0 +1,42 @@
+# [Android Components](../../../README.md) > Libraries > QR
+
+A component that provides functionality for scanning QR coes.
+
+## Usage
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
+
+```Groovy
+implementation "org.mozilla.components:feature-qr:{latest-version}"
+```
+
+### Integration
+
+Initializing the feature:
+
+```kotlin
+qrFeature = QrFeature(
+ context,
+ fragmentManager = supportFragmentManager,
+ onNeedToRequestPermissions = { permissions ->
+ requestPermissions(this, permissions, REQUEST_CODE_CAMERA_PERMISSIONS)
+ },
+ onScanResult = { result ->
+ // result is a String (e.g. a URL) returned by the QR scanner.
+ }
+)
+```
+
+When ready to scan use the following:
+
+```kotlin
+qrFeature.scan()
+```
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/components/feature/qr/build.gradle b/mobile/android/android-components/components/feature/qr/build.gradle
new file mode 100644
index 0000000000..f491a3e0a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/build.gradle
@@ -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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ namespace 'mozilla.components.feature.qr'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+
+ implementation ComponentsDependencies.androidx_appcompat
+
+ implementation project(':support-ktx')
+ implementation project(':support-base')
+
+ implementation ComponentsDependencies.thirdparty_zxing
+ testImplementation ComponentsDependencies.thirdparty_zxing
+
+ testImplementation project(':support-test')
+ testImplementation ComponentsDependencies.androidx_test_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation ComponentsDependencies.testing_robolectric
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
diff --git a/mobile/android/android-components/components/feature/qr/proguard-rules.pro b/mobile/android/android-components/components/feature/qr/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/mobile/android/android-components/components/feature/qr/src/main/AndroidManifest.xml b/mobile/android/android-components/components/feature/qr/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..592249325c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<!-- 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">
+
+ <uses-permission android:name="android.permission.CAMERA" />
+</manifest>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt
new file mode 100644
index 0000000000..87a55dbcc4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFeature.kt
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.qr
+
+import android.Manifest.permission.CAMERA
+import android.content.Context
+import androidx.annotation.MainThread
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.fragment.app.FragmentManager
+import mozilla.components.support.base.feature.LifecycleAwareFeature
+import mozilla.components.support.base.feature.OnNeedToRequestPermissions
+import mozilla.components.support.base.feature.PermissionsFeature
+import mozilla.components.support.base.feature.UserInteractionHandler
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+
+typealias OnScanResult = (result: String) -> Unit
+
+/**
+ * Feature implementation that provides QR scanning functionality via the [QrFragment].
+ *
+ * @property context a reference to the context.
+ * @property fragmentManager a reference to a [FragmentManager], used to start
+ * the [QrFragment].
+ * @property onScanResult a callback invoked with the result of the QR scan.
+ * The callback will always be invoked on the main thread.
+ * @property onNeedToRequestPermissions a callback invoked when permissions
+ * need to be requested before a QR scan can be performed. Once the request
+ * is completed, [onPermissionsResult] needs to be invoked. This feature
+ * will request [android.Manifest.permission.CAMERA].
+ * @property scanMessage (Optional) String resource for an optional message
+ * to be laid out below the QR scan viewfinder
+ */
+class QrFeature(
+ private val context: Context,
+ private val fragmentManager: FragmentManager,
+ private val onScanResult: OnScanResult = { },
+ override val onNeedToRequestPermissions: OnNeedToRequestPermissions = { },
+ @StringRes
+ private var scanMessage: Int? = null,
+) : LifecycleAwareFeature, UserInteractionHandler, PermissionsFeature {
+ private var containerViewId: Int = 0
+
+ private val qrFragment
+ get() = fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG) as? QrFragment
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ val isScanInProgress
+ get() = qrFragment != null
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val scanCompleteListener: QrFragment.OnScanCompleteListener = object : QrFragment.OnScanCompleteListener {
+ @MainThread
+ override fun onScanComplete(result: String) {
+ setScanCompleteListener(null)
+ removeQrFragment()
+ onScanResult(result)
+ }
+ }
+
+ override fun start() {
+ setScanCompleteListener(scanCompleteListener)
+ }
+
+ override fun stop() {
+ // Prevent an already in progress qr decode operation informing us later of a result
+ // and so triggering an IllegalStateException when trying to remove the qr fragment.
+ setScanCompleteListener(null)
+ }
+
+ override fun onBackPressed(): Boolean {
+ return removeQrFragment()
+ }
+
+ /**
+ * Starts the QR scanner fragment and listens for scan results.
+ *
+ * @param containerViewId optional id of the container this fragment is to
+ * be placed in, defaults to [android.R.id.content].
+ *
+ * @return true if the scanner was started or false if permissions still
+ * need to be requested.
+ */
+ fun scan(containerViewId: Int = android.R.id.content): Boolean {
+ this.containerViewId = containerViewId
+
+ return if (context.isPermissionGranted(CAMERA)) {
+ when (isScanInProgress) {
+ true -> qrFragment?.startScanning()
+ false -> fragmentManager.beginTransaction()
+ .add(containerViewId, QrFragment.newInstance(scanCompleteListener, scanMessage), QR_FRAGMENT_TAG)
+ .commit()
+ }
+ true
+ } else {
+ onNeedToRequestPermissions(arrayOf(CAMERA))
+ false
+ }
+ }
+
+ /**
+ * Notifies the feature that the permission request was completed. If the
+ * requested permissions were granted it will open the QR scanner.
+ */
+ override fun onPermissionsResult(permissions: Array<String>, grantResults: IntArray) {
+ if (context.isPermissionGranted(CAMERA)) {
+ scan(containerViewId)
+ } else {
+ // It is possible that we started scanning then the user is will update
+ // the camera permission in Android settings.
+ // The client app is expected to ask again for the camera permission when the app is resumed
+ // and this request can be denied by the user so we should interrupt the in-progress scanning.
+ removeQrFragment()
+ }
+ }
+
+ /**
+ * Removes the QR fragment.
+ *
+ * @return true if the fragment was removed, otherwise false.
+ */
+ internal fun removeQrFragment(): Boolean {
+ qrFragment?.let {
+ fragmentManager.beginTransaction().remove(it).commit()
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Set a callback for when a qr code has been successfully scanned and decoded.
+ */
+ @VisibleForTesting
+ internal fun setScanCompleteListener(listener: QrFragment.OnScanCompleteListener?) {
+ (fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG) as? QrFragment)?.let {
+ it.scanCompleteListener = listener
+ }
+ }
+
+ companion object {
+ internal const val QR_FRAGMENT_TAG = "MOZAC_QR_FRAGMENT"
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt
new file mode 100644
index 0000000000..e92e37cb7b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/QrFragment.kt
@@ -0,0 +1,786 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. */
+
+package mozilla.components.feature.qr
+
+import android.Manifest.permission
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.OutputConfiguration
+import android.hardware.camera2.params.SessionConfiguration
+import android.media.Image
+import android.media.ImageReader
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.util.Size
+import android.view.LayoutInflater
+import android.view.Surface
+import android.view.TextureView
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.ContextCompat.getColor
+import androidx.core.view.WindowInsetsCompat
+import androidx.fragment.app.Fragment
+import com.google.zxing.BinaryBitmap
+import com.google.zxing.LuminanceSource
+import com.google.zxing.MultiFormatReader
+import com.google.zxing.PlanarYUVLuminanceSource
+import com.google.zxing.common.HybridBinarizer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.feature.qr.views.AutoFitTextureView
+import mozilla.components.feature.qr.views.CustomViewFinder
+import mozilla.components.support.base.log.logger.Logger
+import mozilla.components.support.ktx.android.content.hasCamera
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+import java.io.Serializable
+import java.util.ArrayList
+import java.util.Collections
+import java.util.Comparator
+import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.RejectedExecutionException
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * A [Fragment] that displays a QR scanner.
+ *
+ * This class is based on Camera2BasicFragment from:
+ *
+ * https://github.com/googlesamples/android-Camera2Basic
+ * https://github.com/kismkof/camera2basic
+ */
+@Suppress("LargeClass", "TooManyFunctions")
+class QrFragment : Fragment() {
+ private val logger = Logger("mozac-qr")
+
+ @VisibleForTesting
+ internal var multiFormatReader = MultiFormatReader()
+ private val coroutineScope = CoroutineScope(Dispatchers.Default)
+
+ /**
+ * [TextureView.SurfaceTextureListener] handles several lifecycle events on a [TextureView].
+ */
+ private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+
+ override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
+ tryOpenCamera(width, height)
+ }
+
+ override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {
+ configureTransform(width, height)
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ override fun onSurfaceTextureUpdated(texture: SurfaceTexture) { }
+
+ override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
+ return true
+ }
+ }
+
+ internal lateinit var textureView: AutoFitTextureView
+ internal lateinit var customViewFinder: CustomViewFinder
+ internal lateinit var cameraErrorView: TextView
+
+ @StringRes
+ internal var scanMessage: Int? = null
+ internal var cameraId: String? = null
+ private var captureSession: CameraCaptureSession? = null
+ internal var cameraDevice: CameraDevice? = null
+ internal var previewSize: Size? = null
+
+ /**
+ * Listener invoked when the QR scan completed successfully.
+ */
+ interface OnScanCompleteListener : Serializable {
+ /**
+ * Invoked to provide access to the result of the QR scan.
+ */
+ fun onScanComplete(result: String)
+ }
+
+ @Volatile internal var scanCompleteListener: OnScanCompleteListener? = null
+ set(value) {
+ field = object : OnScanCompleteListener {
+ override fun onScanComplete(result: String) {
+ Handler(Looper.getMainLooper()).apply {
+ post {
+ context?.let {
+ customViewFinder.setViewFinderColor(
+ getColor(it, R.color.mozac_feature_qr_scan_success_color),
+ )
+ }
+ value?.onScanComplete(result)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * [CameraDevice.StateCallback] is called when [CameraDevice] changes its state.
+ */
+ internal val stateCallback = object : CameraDevice.StateCallback() {
+
+ override fun onOpened(cameraDevice: CameraDevice) {
+ cameraOpenCloseLock.release()
+ this@QrFragment.cameraDevice = cameraDevice
+ createCameraPreviewSession()
+ }
+
+ override fun onDisconnected(cameraDevice: CameraDevice) {
+ cameraOpenCloseLock.release()
+ cameraDevice.close()
+ this@QrFragment.cameraDevice = null
+ }
+
+ override fun onError(cameraDevice: CameraDevice, error: Int) {
+ cameraOpenCloseLock.release()
+ cameraDevice.close()
+ this@QrFragment.cameraDevice = null
+ }
+ }
+
+ /**
+ * An additional thread for running tasks that shouldn't block the UI.
+ * A [Handler] for running tasks in the background.
+ */
+ @VisibleForTesting
+ internal var backgroundThread: HandlerThread? = null
+
+ @VisibleForTesting
+ internal var backgroundHandler: Handler? = null
+
+ @VisibleForTesting
+ internal var backgroundExecutor: ExecutorService? = null
+ private var previewRequestBuilder: CaptureRequest.Builder? = null
+ private var previewRequest: CaptureRequest? = null
+
+ /**
+ * A [Semaphore] to prevent the app from exiting before closing the camera.
+ */
+ private val cameraOpenCloseLock = Semaphore(1)
+
+ /**
+ * Orientation of the camera sensor
+ */
+ private var sensorOrientation: Int = 0
+
+ /**
+ * An [ImageReader] that handles still image capture.
+ * This is the output file for our picture.
+ */
+ private var imageReader: ImageReader? = null
+ private val imageAvailableListener = object : ImageReader.OnImageAvailableListener {
+
+ private var image: Image? = null
+
+ override fun onImageAvailable(reader: ImageReader) {
+ try {
+ image = reader.acquireNextImage()
+ val availableImage = image
+ if (availableImage != null && scanCompleteListener != null) {
+ val source = readImageSource(availableImage)
+ if (qrState == STATE_FIND_QRCODE) {
+ qrState = STATE_DECODE_PROGRESS
+
+ coroutineScope.launch {
+ tryScanningSource(source)
+ }
+ }
+ }
+ } finally {
+ image?.close()
+ }
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_layout, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ textureView = view.findViewById<View>(R.id.texture) as AutoFitTextureView
+ customViewFinder = view.findViewById<View>(R.id.view_finder) as CustomViewFinder
+ cameraErrorView = view.findViewById<View>(R.id.camera_error) as TextView
+
+ CustomViewFinder.setMessage(scanMessage)
+ qrState = STATE_FIND_QRCODE
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // It's possible that the Fragment is resumed to a scanning state
+ // while in the meantime the camera permission was removed. Avoid any issues.
+ if (requireContext().isPermissionGranted(permission.CAMERA)) {
+ startScanning()
+ }
+ }
+
+ override fun onPause() {
+ closeCamera()
+ stopBackgroundThread()
+ stopExecutorService()
+ super.onPause()
+ }
+
+ override fun onStop() {
+ // Ensure we'll continue tracking qr codes when the user returns to the application
+ qrState = STATE_FIND_QRCODE
+
+ super.onStop()
+ }
+
+ internal fun maybeStartBackgroundThread() {
+ if (backgroundThread == null) {
+ backgroundThread = HandlerThread("CameraBackground")
+ }
+
+ backgroundThread?.let {
+ if (!it.isAlive) {
+ it.start()
+ backgroundHandler = Handler(it.looper)
+ }
+ }
+ }
+
+ internal fun stopBackgroundThread() {
+ backgroundThread?.quitSafely()
+ try {
+ backgroundThread?.join()
+ backgroundThread = null
+ backgroundHandler = null
+ } catch (e: InterruptedException) {
+ logger.debug("Interrupted while stopping background thread", e)
+ }
+ }
+
+ internal fun maybeStartExecutorService() {
+ if (backgroundExecutor == null) {
+ backgroundExecutor = Executors.newSingleThreadExecutor()
+ }
+ }
+
+ internal fun stopExecutorService() {
+ backgroundExecutor?.shutdownNow()
+ backgroundExecutor = null
+ }
+
+ /**
+ * Open the camera and start the qr scanning functionality.
+ * Assumes the camera permission is granted for the app.
+ * If any issues occur this will fail gracefully and show an error message.
+ */
+ fun startScanning() {
+ maybeStartBackgroundThread()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ maybeStartExecutorService()
+ }
+ // When the screen is turned off and turned back on, the SurfaceTexture is already
+ // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
+ // a camera and start preview from here (otherwise, we wait until the surface is ready in
+ // the SurfaceTextureListener).
+ if (textureView.isAvailable) {
+ tryOpenCamera(textureView.width, textureView.height)
+ } else {
+ textureView.surfaceTextureListener = surfaceTextureListener
+ }
+ }
+
+ /**
+ * Sets up member variables related to camera.
+ *
+ * @param width The width of available size for camera preview
+ * @param height The height of available size for camera preview
+ */
+ @Suppress("ComplexMethod")
+ internal fun setUpCameraOutputs(width: Int, height: Int) {
+ val displayRotation = getScreenRotation()
+
+ val manager = activity?.getSystemService(Context.CAMERA_SERVICE) as CameraManager? ?: return
+
+ for (cameraId in manager.cameraIdList) {
+ val characteristics = manager.getCameraCharacteristics(cameraId)
+
+ val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
+ if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
+ continue
+ }
+
+ val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+ ?: continue
+ val largest = Collections.max(map.getOutputSizes(ImageFormat.YUV_420_888).asList(), CompareSizesByArea())
+ imageReader = ImageReader.newInstance(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT, ImageFormat.YUV_420_888, 2)
+ .apply { setOnImageAvailableListener(imageAvailableListener, backgroundHandler) }
+
+ // Find out if we need to swap dimension to get the preview size relative to sensor coordinate.
+
+ sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) as Int
+
+ @Suppress("MagicNumber")
+ val swappedDimensions = when (displayRotation) {
+ Surface.ROTATION_0, Surface.ROTATION_180 -> sensorOrientation == 90 || sensorOrientation == 270
+ Surface.ROTATION_90, Surface.ROTATION_270 -> sensorOrientation == 0 || sensorOrientation == 180
+ else -> false
+ }
+
+ val displaySize = activity?.windowManager?.getDisplaySize() ?: Point()
+
+ var rotatedPreviewWidth = width
+ var rotatedPreviewHeight = height
+ var maxPreviewWidth = displaySize.x
+ var maxPreviewHeight = displaySize.y
+
+ if (swappedDimensions) {
+ rotatedPreviewWidth = height
+ rotatedPreviewHeight = width
+ maxPreviewWidth = displaySize.y
+ maxPreviewHeight = displaySize.x
+ }
+
+ maxPreviewWidth = min(maxPreviewWidth, MAX_PREVIEW_WIDTH)
+ maxPreviewHeight = min(maxPreviewHeight, MAX_PREVIEW_HEIGHT)
+
+ val optimalSize = chooseOptimalSize(
+ map.getOutputSizes(SurfaceTexture::class.java),
+ rotatedPreviewWidth,
+ rotatedPreviewHeight,
+ maxPreviewWidth,
+ maxPreviewHeight,
+ largest,
+ )
+
+ adjustPreviewSize(optimalSize)
+ this.cameraId = cameraId
+ return
+ }
+ }
+
+ internal fun adjustPreviewSize(optimalSize: Size) {
+ // We're seeing slow unreliable scans with distorted screens on some devices
+ // so we're making the preview and scan area a square of the optimal size
+ // to prevent that.
+ val length = min(optimalSize.height, optimalSize.width)
+ textureView.setAspectRatio(length, length)
+ this.previewSize = Size(length, length)
+ }
+
+ /**
+ * Tries to open the camera and displays an error message in case
+ * there's no camera available or we fail to open it. Applications
+ * should ideally check for camera availability, but we use this
+ * as a fallback in case they don't.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ internal fun tryOpenCamera(width: Int, height: Int, skipCheck: Boolean = false) {
+ try {
+ if (context?.hasCamera() == true || skipCheck) {
+ openCamera(width, height)
+ hideNoCameraAvailableError()
+ } else {
+ showNoCameraAvailableError()
+ }
+ } catch (e: Exception) {
+ showNoCameraAvailableError()
+ }
+ }
+
+ private fun showNoCameraAvailableError() {
+ cameraErrorView.visibility = View.VISIBLE
+ customViewFinder.visibility = View.GONE
+ }
+
+ private fun hideNoCameraAvailableError() {
+ cameraErrorView.visibility = View.GONE
+ customViewFinder.visibility = View.VISIBLE
+ }
+
+ /**
+ * Opens the camera specified by [QrFragment.cameraId].
+ */
+ @SuppressLint("MissingPermission")
+ @Suppress("ThrowsCount")
+ internal fun openCamera(width: Int, height: Int) {
+ try {
+ setUpCameraOutputs(width, height)
+ if (cameraId == null) {
+ throw IllegalStateException("No camera found on device")
+ }
+
+ configureTransform(width, height)
+
+ val activity = activity
+ val manager = activity?.getSystemService(Context.CAMERA_SERVICE) as CameraManager?
+
+ if (!cameraOpenCloseLock.tryAcquire(CAMERA_CLOSE_LOCK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ throw IllegalStateException("Time out waiting to lock camera opening.")
+ }
+ manager?.openCamera(cameraId as String, stateCallback, backgroundHandler)
+ } catch (e: InterruptedException) {
+ throw IllegalStateException("Interrupted while trying to lock camera opening.", e)
+ } catch (e: CameraAccessException) {
+ logger.error("Failed to open camera", e)
+ }
+ }
+
+ /**
+ * Closes the current [CameraDevice].
+ */
+ internal fun closeCamera() {
+ try {
+ cameraOpenCloseLock.acquire()
+ cameraDevice?.close()
+ cameraDevice = null
+ imageReader?.close()
+ imageReader = null
+
+ // captureSession should be closed as a last step in case background executor terminated
+ captureSession?.close()
+ captureSession = null
+ } catch (e: InterruptedException) {
+ throw IllegalStateException("Interrupted while trying to lock camera closing.", e)
+ } catch (e: RejectedExecutionException) { // This exception was found in automated testing
+ logger.error("backgroundExecutor terminated", e)
+ } finally {
+ cameraOpenCloseLock.release()
+ }
+ }
+
+ /**
+ * Configures the necessary [android.graphics.Matrix] transformation to `textureView`.
+ * This method should be called after the camera preview size is determined in
+ * [setUpCameraOutputs] and also the size of `textureView` is fixed.
+ *
+ * @param viewWidth The width of `textureView`
+ * @param viewHeight The height of `textureView`
+ */
+ @VisibleForTesting
+ internal fun configureTransform(viewWidth: Int, viewHeight: Int) {
+ val size = previewSize ?: return
+
+ val rotation = getScreenRotation()
+ val matrix = Matrix()
+ val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
+ val bufferRect = RectF(0f, 0f, size.height.toFloat(), size.width.toFloat())
+ val centerX = viewRect.centerX()
+ val centerY = viewRect.centerY()
+
+ @Suppress("MagicNumber")
+ if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
+ bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
+ matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
+ val scale = max(viewHeight.toFloat() / size.height, viewWidth.toFloat() / size.width)
+ matrix.postScale(scale, scale, centerX, centerY)
+ matrix.postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY)
+ } else if (Surface.ROTATION_180 == rotation) {
+ matrix.postRotate(180f, centerX, centerY)
+ }
+ textureView.setTransform(matrix)
+ }
+
+ /**
+ * Creates a new [CameraCaptureSession] for camera preview.
+ */
+ @Suppress("ComplexMethod")
+ internal fun createCameraPreviewSession() {
+ val texture = textureView.surfaceTexture
+
+ val size = previewSize as Size
+ // We configure the size of default buffer to be the size of camera preview we want.
+ texture?.setDefaultBufferSize(size.width, size.height)
+
+ val surface = Surface(texture)
+ val mImageSurface = imageReader?.surface
+
+ handleCaptureException("Failed to create camera preview session") {
+ cameraDevice?.let {
+ previewRequestBuilder = it.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
+ addTarget(mImageSurface as Surface)
+ addTarget(surface)
+ }
+
+ val captureCallback = object : CameraCaptureSession.CaptureCallback() {}
+ val stateCallback = object : CameraCaptureSession.StateCallback() {
+ override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {
+ if (null == cameraDevice) return
+
+ previewRequestBuilder?.set(
+ CaptureRequest.CONTROL_AF_MODE,
+ CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+ )
+
+ previewRequest = previewRequestBuilder?.build()
+ captureSession = cameraCaptureSession
+
+ handleCaptureException("Failed to request capture") {
+ cameraCaptureSession.setRepeatingRequest(
+ previewRequest as CaptureRequest,
+ captureCallback,
+ backgroundHandler,
+ )
+ }
+ }
+
+ override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) {
+ logger.error("Failed to configure CameraCaptureSession")
+ }
+ }
+ createCaptureSessionCompat(it, mImageSurface as Surface, surface, stateCallback)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createCaptureSessionCompat(
+ camera: CameraDevice,
+ imageSurface: Surface,
+ surface: Surface,
+ stateCallback: CameraCaptureSession.StateCallback,
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ if (shouldStartExecutorService()) {
+ maybeStartExecutorService()
+ }
+ val sessionConfig = SessionConfiguration(
+ SessionConfiguration.SESSION_REGULAR,
+ listOf(OutputConfiguration(imageSurface), OutputConfiguration(surface)),
+ backgroundExecutor as Executor,
+ stateCallback,
+ )
+ camera.createCaptureSession(sessionConfig)
+ } else {
+ @Suppress("DEPRECATION")
+ camera.createCaptureSession(listOf(imageSurface, surface), stateCallback, null)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun shouldStartExecutorService(): Boolean = backgroundExecutor == null
+
+ @Suppress("TooGenericExceptionCaught")
+ private fun handleCaptureException(msg: String, block: () -> Unit) {
+ try {
+ block()
+ } catch (e: Exception) {
+ when (e) {
+ is CameraAccessException, is IllegalStateException -> {
+ logger.error(msg, e)
+ }
+ else -> throw e
+ }
+ }
+ }
+
+ /**
+ * Compares two `Size`s based on their areas.
+ */
+ internal class CompareSizesByArea : Comparator<Size> {
+ override fun compare(lhs: Size, rhs: Size): Int {
+ return java.lang.Long.signum(lhs.width.toLong() * lhs.height - rhs.width.toLong() * rhs.height)
+ }
+ }
+
+ companion object {
+ internal const val STATE_FIND_QRCODE = 0
+ internal const val STATE_DECODE_PROGRESS = 1
+ internal const val STATE_QRCODE_EXIST = 2
+
+ internal const val MAX_PREVIEW_WIDTH = 786
+ internal const val MAX_PREVIEW_HEIGHT = 786
+
+ private const val CAMERA_CLOSE_LOCK_TIMEOUT_MS = 2500L
+
+ /**
+ * Returns a new instance of QR Fragment
+ * @param listener Listener invoked when the QR scan completed successfully.
+ * @param scanMessage (Optional) Scan message to be displayed.
+ */
+ fun newInstance(listener: OnScanCompleteListener, scanMessage: Int? = null): QrFragment {
+ return QrFragment().apply {
+ scanCompleteListener = listener
+ this.scanMessage = scanMessage
+ }
+ }
+
+ /**
+ * Given `choices` of `Size`s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
+ *
+ * @param choices The list of sizes that the camera supports for the intended output class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
+ * @return The optimal `Size`, or an arbitrary one if none were big enough.
+ */
+ @Suppress("ComplexMethod")
+ internal fun chooseOptimalSize(
+ choices: Array<Size>,
+ textureViewWidth: Int,
+ textureViewHeight: Int,
+ maxWidth: Int,
+ maxHeight: Int,
+ aspectRatio: Size,
+ ): Size {
+ // Collect the supported resolutions that are at least as big as the preview Surface
+ val bigEnough = ArrayList<Size>()
+ // Collect the supported resolutions that are smaller than the preview Surface
+ val notBigEnough = ArrayList<Size>()
+ val w = aspectRatio.width
+ val h = aspectRatio.height
+ for (option in choices) {
+ if (option.width <= maxWidth && option.height <= maxHeight &&
+ option.height == option.width * h / w
+ ) {
+ if (option.width >= textureViewWidth && option.height >= textureViewHeight) {
+ bigEnough.add(option)
+ } else {
+ notBigEnough.add(option)
+ }
+ }
+ }
+
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
+ return when {
+ bigEnough.size > 0 -> Collections.min(bigEnough, CompareSizesByArea())
+ notBigEnough.size > 0 -> Collections.max(notBigEnough, CompareSizesByArea())
+ else -> choices[0]
+ }
+ }
+
+ internal fun readImageSource(image: Image): PlanarYUVLuminanceSource {
+ val plane = image.planes[0]
+ val buffer = plane.buffer
+ val data = ByteArray(buffer.remaining()).also { buffer.get(it) }
+
+ val height = image.height
+ val width = image.width
+ val dataWidth = width + ((plane.rowStride - plane.pixelStride * width) / plane.pixelStride)
+ return PlanarYUVLuminanceSource(data, dataWidth, height, 0, 0, width, height, false)
+ }
+
+ @Volatile internal var qrState: Int = 0
+ }
+
+ @VisibleForTesting
+ internal fun tryScanningSource(source: LuminanceSource) {
+ if (qrState != STATE_DECODE_PROGRESS) {
+ return
+ }
+ val result = decodeSource(source) ?: decodeSource(source.invert())
+ result?.let {
+ scanCompleteListener?.onScanComplete(it)
+ }
+ }
+
+ @VisibleForTesting
+ @Suppress("TooGenericExceptionCaught")
+ internal fun decodeSource(source: LuminanceSource): String? {
+ return try {
+ val bitmap = createBinaryBitmap(source)
+ val rawResult = multiFormatReader.decodeWithState(bitmap)
+ if (rawResult != null) {
+ qrState = STATE_QRCODE_EXIST
+ rawResult.toString()
+ } else {
+ qrState = STATE_FIND_QRCODE
+ null
+ }
+ } catch (e: Exception) {
+ qrState = STATE_FIND_QRCODE
+ null
+ } finally {
+ multiFormatReader.reset()
+ }
+ }
+
+ @VisibleForTesting
+ internal fun createBinaryBitmap(source: LuminanceSource) =
+ BinaryBitmap(HybridBinarizer(source))
+
+ /**
+ * Returns the screen rotation
+ *
+ * @return the actual rotation of the device is one of these values:
+ * [Surface.ROTATION_0], [Surface.ROTATION_90], [Surface.ROTATION_180], [Surface.ROTATION_270]
+ */
+ @VisibleForTesting
+ internal fun getScreenRotation(): Int? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ this.context?.display?.rotation
+ } else {
+ @Suppress("DEPRECATION")
+ activity?.windowManager?.defaultDisplay?.rotation
+ }
+ }
+}
+
+/**
+ * Returns the size of the display, in pixels.
+ */
+@VisibleForTesting
+internal fun WindowManager.getDisplaySize(): Point {
+ val size = Point()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Tests for this branch will be added after
+ // https://github.com/mozilla-mobile/android-components/issues/9684 is implemented.
+ val windowMetrics = this.currentWindowMetrics
+ val windowInsets: WindowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(windowMetrics.windowInsets)
+
+ val insets = windowInsets.getInsetsIgnoringVisibility(
+ WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.displayCutout(),
+ )
+ val insetsWidth = insets.right + insets.left
+ val insetsHeight = insets.top + insets.bottom
+
+ val bounds: Rect = windowMetrics.bounds
+ size.set(bounds.width() - insetsWidth, bounds.height() - insetsHeight)
+ } else {
+ @Suppress("DEPRECATION")
+ this.defaultDisplay.getSize(size)
+ }
+ return size
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt
new file mode 100644
index 0000000000..51c6759d0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/AutoFitTextureView.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. */
+
+package mozilla.components.feature.qr.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.TextureView
+import android.view.View
+import androidx.annotation.VisibleForTesting
+
+/**
+ * A [TextureView] that can be adjusted to a specified aspect ratio.
+ */
+open class AutoFitTextureView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0,
+) : TextureView(context, attrs, defStyle) {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var mRatioWidth = 0
+ private set
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var mRatioHeight = 0
+ private set
+
+ /**
+ * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
+ * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
+ * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
+ *
+ * @param width Relative horizontal size
+ * @param height Relative vertical size
+ */
+ fun setAspectRatio(width: Int, height: Int) {
+ if (width < 0 || height < 0) {
+ throw IllegalArgumentException("Size cannot be negative.")
+ }
+ mRatioWidth = width
+ mRatioHeight = height
+ requestLayout()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val width = View.MeasureSpec.getSize(widthMeasureSpec)
+ val height = View.MeasureSpec.getSize(heightMeasureSpec)
+ if (0 == mRatioWidth || 0 == mRatioHeight) {
+ setMeasuredDimension(width, height)
+ } else {
+ if (width < height * mRatioWidth / mRatioHeight) {
+ setMeasuredDimension(width, width * mRatioHeight / mRatioWidth)
+ } else {
+ setMeasuredDimension(height * mRatioWidth / mRatioHeight, height)
+ }
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt
new file mode 100644
index 0000000000..47051f3f20
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/java/mozilla/components/feature/qr/views/CustomViewFinder.kt
@@ -0,0 +1,298 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.qr.views
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.Rect
+import android.os.Build
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.ColorInt
+import androidx.annotation.Px
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.content.ContextCompat
+import androidx.core.text.HtmlCompat
+import mozilla.components.support.ktx.android.util.dpToPx
+import mozilla.components.support.ktx.android.util.spToPx
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/**
+ * A [View] that shows a ViewFinder positioned in center of the camera view and draws an Overlay
+ */
+@Suppress("LargeClass")
+class CustomViewFinder @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+) : AppCompatImageView(context, attrs) {
+ private var messageResource: Int? = null
+ private val overlayPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ internal val viewFinderPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private var viewFinderPath: Path = Path()
+ private var viewFinderPathSaved: Boolean = false
+ private var overlayPath: Path = Path()
+ private var overlayPathSaved: Boolean = false
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal lateinit var viewFinderRectangle: Rect
+ private var viewFinderCornersSize: Float = 0f
+ private var viewFinderCornersRadius: Float = 0f
+ private var viewFinderTop: Float = 0f
+ private var viewFinderLeft: Float = 0f
+ private var viewFinderRight: Float = 0f
+ private var viewFinderBottom: Float = 0f
+ private var normalizedRadius: Float = 0f
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var scanMessageLayout: StaticLayout? = null
+ private lateinit var messageTextPaint: TextPaint
+
+ init {
+ isSaveEnabled = true
+ overlayPaint.style = Paint.Style.FILL
+ viewFinderPaint.style = Paint.Style.STROKE
+ overlayPath.fillType = Path.FillType.EVEN_ODD
+ viewFinderPath.fillType = Path.FillType.EVEN_ODD
+
+ this.layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ )
+ this.setOverlayColor(DEFAULT_OVERLAY_COLOR)
+ this.setViewFinderColor(DEFAULT_VIEWFINDER_COLOR)
+ this.setViewFinderStroke(DEFAULT_VIEWFINDER_THICKNESS_DP.dpToPx(resources.displayMetrics))
+ this.setViewFinderCornerRadius(DEFAULT_VIEWFINDER_CORNERS_RADIUS_DP.dpToPx(resources.displayMetrics))
+ }
+
+ /** Calculates viewfinder rectangle and triggers calculating message layout that depends on it */
+ internal fun computeViewFinderRect(width: Int, height: Int) {
+ if (width > 0 && height > 0) {
+ val minimumDimension = min(width.toFloat(), height.toFloat())
+
+ val viewFinderSide = (minimumDimension * DEFAULT_VIEWFINDER_WIDTH_RATIO).roundToInt()
+
+ val viewFinderLeftOrRight = (width - viewFinderSide) / 2
+ val viewFinderTopOrBottom = (height - viewFinderSide) / 2
+ viewFinderRectangle = Rect(
+ viewFinderLeftOrRight,
+ viewFinderTopOrBottom,
+ viewFinderLeftOrRight + viewFinderSide,
+ viewFinderTopOrBottom + viewFinderSide,
+ )
+
+ this.setViewFinderCornerSize(DEFAULT_VIEWFINDER_CORNER_SIZE_RATIO * viewFinderRectangle.width())
+
+ viewFinderTop = viewFinderRectangle.top.toFloat()
+ viewFinderLeft = viewFinderRectangle.left.toFloat()
+ viewFinderRight = viewFinderRectangle.right.toFloat()
+ viewFinderBottom = viewFinderRectangle.bottom.toFloat()
+ normalizedRadius = min(viewFinderCornersRadius, (viewFinderCornersSize - 1).coerceAtLeast(0f))
+
+ showMessage(scanMessageStringRes)
+ }
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ computeViewFinderRect(width, height)
+ }
+
+ // useful when you have a disappearing keyboard
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ redraw()
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration?) {
+ super.onConfigurationChanged(newConfig)
+ redraw()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ drawOverlay(canvas)
+ drawViewFinder(canvas)
+ drawMessage(canvas)
+ }
+
+ private fun redraw() {
+ overlayPathSaved = false
+ viewFinderPathSaved = false
+ computeViewFinderRect(width, height)
+ invalidate()
+ }
+
+ /** Draws the Overlay */
+ private fun drawOverlay(canvas: Canvas) {
+ if (!overlayPathSaved) {
+ overlayPath.apply {
+ reset()
+ moveTo(viewFinderLeft, viewFinderTop + normalizedRadius)
+ quadTo(viewFinderLeft, viewFinderTop, viewFinderLeft + normalizedRadius, viewFinderTop)
+ lineTo(viewFinderRight - normalizedRadius, viewFinderTop)
+ quadTo(viewFinderRight, viewFinderTop, viewFinderRight, viewFinderTop + normalizedRadius)
+ lineTo(viewFinderRight, viewFinderBottom - normalizedRadius)
+ quadTo(viewFinderRight, viewFinderBottom, viewFinderRight - normalizedRadius, viewFinderBottom)
+ lineTo(viewFinderLeft + normalizedRadius, viewFinderBottom)
+ quadTo(viewFinderLeft, viewFinderBottom, viewFinderLeft, viewFinderBottom - normalizedRadius)
+ lineTo(viewFinderLeft, viewFinderTop + normalizedRadius)
+ moveTo(0f, 0f)
+ lineTo(width.toFloat(), 0f)
+ lineTo(width.toFloat(), height.toFloat())
+ lineTo(0f, height.toFloat())
+ lineTo(0f, 0f)
+ }
+ overlayPathSaved = true
+ }
+ canvas.drawPath(overlayPath, overlayPaint)
+ }
+
+ /** Draws the ViewFinder */
+ private fun drawViewFinder(canvas: Canvas) {
+ if (!viewFinderPathSaved) {
+ viewFinderPath.apply {
+ reset()
+ moveTo(viewFinderLeft, viewFinderTop + viewFinderCornersSize)
+ lineTo(viewFinderLeft, viewFinderTop + normalizedRadius)
+ quadTo(viewFinderLeft, viewFinderTop, viewFinderLeft + normalizedRadius, viewFinderTop)
+ lineTo(viewFinderLeft + viewFinderCornersSize, viewFinderTop)
+ moveTo(viewFinderRight - viewFinderCornersSize, viewFinderTop)
+ lineTo(viewFinderRight - normalizedRadius, viewFinderTop)
+ quadTo(viewFinderRight, viewFinderTop, viewFinderRight, viewFinderTop + normalizedRadius)
+ lineTo(viewFinderRight, viewFinderTop + viewFinderCornersSize)
+ moveTo(viewFinderRight, viewFinderBottom - viewFinderCornersSize)
+ lineTo(viewFinderRight, viewFinderBottom - normalizedRadius)
+ quadTo(viewFinderRight, viewFinderBottom, viewFinderRight - normalizedRadius, viewFinderBottom)
+ lineTo(viewFinderRight - viewFinderCornersSize, viewFinderBottom)
+ moveTo(viewFinderLeft + viewFinderCornersSize, viewFinderBottom)
+ lineTo(viewFinderLeft + normalizedRadius, viewFinderBottom)
+ quadTo(viewFinderLeft, viewFinderBottom, viewFinderLeft, viewFinderBottom - normalizedRadius)
+ lineTo(viewFinderLeft, viewFinderBottom - viewFinderCornersSize)
+ }
+ viewFinderPathSaved = true
+ }
+ canvas.drawPath(viewFinderPath, this.viewFinderPaint)
+ }
+
+ /**
+ * Creates a Static Layout used to show a message below the viewfinder
+ */
+ @Suppress("Deprecation")
+ private fun showMessage(@StringRes scanMessageId: Int?) {
+ val scanMessage = if (scanMessageId != null) {
+ HtmlCompat.fromHtml(
+ context.getString(scanMessageId),
+ HtmlCompat.FROM_HTML_MODE_LEGACY,
+ )
+ } else {
+ ""
+ }
+ messageTextPaint = TextPaint().apply {
+ isAntiAlias = true
+ color = ContextCompat.getColor(context, android.R.color.white)
+ textSize = SCAN_MESSAGE_TEXT_SIZE_SP.spToPx(resources.displayMetrics)
+ }
+
+ scanMessageLayout =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ StaticLayout.Builder.obtain(
+ scanMessage,
+ 0,
+ scanMessage.length,
+ messageTextPaint,
+ viewFinderRectangle.width(),
+ ).setAlignment(Layout.Alignment.ALIGN_CENTER).build()
+ } else {
+ StaticLayout(
+ scanMessage,
+ messageTextPaint,
+ viewFinderRectangle.width(),
+ Layout.Alignment.ALIGN_CENTER,
+ 1.0f,
+ 0.0f,
+ true,
+ )
+ }
+
+ messageResource = scanMessageId
+ }
+
+ /** Draws text below the ViewFinder. */
+ private fun drawMessage(canvas: Canvas) {
+ canvas.save()
+ canvas.translate(
+ viewFinderRectangle.left.toFloat(),
+ viewFinderRectangle.bottom.toFloat() +
+ SCAN_MESSAGE_TOP_PADDING_DP.dpToPx(resources.displayMetrics),
+ )
+ scanMessageLayout?.draw(canvas)
+ canvas.restore()
+ }
+
+ /** Sets the color for the Overlay. */
+ private fun setOverlayColor(@ColorInt color: Int) {
+ overlayPaint.color = color
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the stroke color for the ViewFinder. */
+ fun setViewFinderColor(@ColorInt color: Int) {
+ viewFinderPaint.color = color
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the stroke width for the ViewFinder. */
+ private fun setViewFinderStroke(@Px stroke: Float) {
+ viewFinderPaint.strokeWidth = stroke
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the corner size for the ViewFinder. */
+ private fun setViewFinderCornerSize(@Px size: Float) {
+ viewFinderCornersSize = size
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ /** Sets the corner radius for the ViewFinder. */
+ private fun setViewFinderCornerRadius(@Px radius: Float) {
+ viewFinderCornersRadius = radius
+ if (isLaidOut) {
+ invalidate()
+ }
+ }
+
+ companion object {
+ internal const val DEFAULT_VIEWFINDER_WIDTH_RATIO = 0.5f
+ private const val DEFAULT_OVERLAY_COLOR = 0x77000000
+ private const val DEFAULT_VIEWFINDER_COLOR = Color.WHITE
+ private const val DEFAULT_VIEWFINDER_THICKNESS_DP = 4f
+ private const val DEFAULT_VIEWFINDER_CORNER_SIZE_RATIO = 0.25f
+ private const val DEFAULT_VIEWFINDER_CORNERS_RADIUS_DP = 8f
+ private const val SCAN_MESSAGE_TOP_PADDING_DP = 10f
+ internal const val SCAN_MESSAGE_TEXT_SIZE_SP = 12f
+ internal var scanMessageStringRes: Int? = null
+
+ /** Sets the message to be displayed below ViewFinder. */
+ fun setMessage(scanMessageStringRes: Int?) {
+ this.scanMessageStringRes = scanMessageStringRes
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..9c9f4585e0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-hdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..64f1ff0285
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-ldpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..0f06992180
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-mdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..eb0340a6be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xhdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..d26d0b8a34
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxhdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webp b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webp
new file mode 100644
index 0000000000..5c28c6b470
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/drawable-xxxhdpi/qr_cam_focus.webp
Binary files differ
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml b/mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml
new file mode 100644
index 0000000000..0b930d33e5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/layout/fragment_layout.xml
@@ -0,0 +1,35 @@
+<?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/. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/background_dark"
+ android:clickable="true"
+ android:focusable="true"
+ tools:ignore="Overdraw">
+
+ <TextView
+ android:id="@+id/camera_error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:text="@string/mozac_feature_qr_scanner_no_camera"
+ android:textColor="@android:color/white"
+ android:visibility="gone" />
+
+ <mozilla.components.feature.qr.views.AutoFitTextureView
+ android:id="@+id/texture"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="true" />
+
+ <mozilla.components.feature.qr.views.CustomViewFinder
+ android:id="@+id/view_finder"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="true" />
+
+</RelativeLayout>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml
new file mode 100644
index 0000000000..5c33f13247
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-am/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">የQR ስካነር</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">በመሳሪያው ላይ ምንም ካሜራ የለም</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml
new file mode 100644
index 0000000000..6b2518105f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-an/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escaner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Lo dispositivo no tiene camara disponible</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..a4563264a5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ar/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ماسح رمز QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ما من كمرة متاحة في الجهاز</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml
new file mode 100644
index 0000000000..c1ae3c897f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ast/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de códigos QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nun hai nenguna cámara disponible nel preséu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml
new file mode 100644
index 0000000000..db835de8d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-az/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Cihazda kamera yoxdur</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml
new file mode 100644
index 0000000000..e101130566
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-azb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR اسکنچی‌سی</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">جهازدا کامئرا یوخ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml
new file mode 100644
index 0000000000..06d0258b86
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ban/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ten wénten kaméra ring perangkat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml
new file mode 100644
index 0000000000..b69588cfef
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-be/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-сканэр</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Няма даступнай камеры на прыладзе</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml
new file mode 100644
index 0000000000..2e354a11f7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-bg/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Скенер за QR код</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Устройството не разполага с камера</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml
new file mode 100644
index 0000000000..ccafaf7766
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-bn/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR স্ক্যানার</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ডিভাইসে কোনো ক্যামেরা নেই</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml
new file mode 100644
index 0000000000..f076513b3e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-br/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">C’hwilerver QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">N’eus bet kavet kamera ebet</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml
new file mode 100644
index 0000000000..ea89910221
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-bs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skener</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nema dostupne kamere na uređaju</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml
new file mode 100644
index 0000000000..f6c743ec4c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ca/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escàner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hi ha cap càmera disponible en el dispositiu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml
new file mode 100644
index 0000000000..ede6264fbc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-cak/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR tz\'ajwachib\'äl</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Majun elesäy wachib\'äl pa ri okisab\'äl</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml
new file mode 100644
index 0000000000..36247920f0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ceb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Wala\'y camera nga magamit sa device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml
new file mode 100644
index 0000000000..8928c305dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ckb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">پشکنەری QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">کامێرا بوونی نیە لەسەر ئەم ئامێرە</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml
new file mode 100644
index 0000000000..1c38f1ea2f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-co/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Analiza di codice QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Alcunu apparechju-fotò hè dispunibule nant’à l’apparechju</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml
new file mode 100644
index 0000000000..394f65f77a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-cs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skener QR kódů</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Zařízení nemá fotoaparát</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml
new file mode 100644
index 0000000000..ca4c7127b8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-cy/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Sganiwr QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nid oes camera ar gael ar y ddyfais</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml
new file mode 100644
index 0000000000..af86f6552d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-da/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Intet kamera tilgængelig på enheden</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000000..79aa856fa0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-de/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-Scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Keine Kamera auf dem Gerät verfügbar</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml
new file mode 100644
index 0000000000..1d0c62d9a4
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-dsb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Žedna kamera njejo k dispoziciji na rěźe</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml
new file mode 100644
index 0000000000..4fa0a90e5b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-el/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Σάρωση QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Η συσκευή σας δεν διαθέτει κάμερα</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000000..5fae94c403
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No camera available on device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000000..5fae94c403
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-en-rGB/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No camera available on device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml
new file mode 100644
index 0000000000..2be7a096dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-eo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skanilo QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Neniu fimilo disponebla en la aparato</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rAR/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rCL/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es-rMX/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000000..b0b62591ce
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-es/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No hay cámara disponible en el dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml
new file mode 100644
index 0000000000..9e31766b3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-et/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Seadme kaamera pole saadaval</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml
new file mode 100644
index 0000000000..ecd1d47fa6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-eu/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR eskanerra</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Kamera ez dago erabilgarri gailuan</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..66d7e8892f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fa/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">پویندهٔ رمزینهٔ پاس</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">دوربین در این افزاره در دسترس نیست</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml
new file mode 100644
index 0000000000..ca95dd65f3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fi/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanneri</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Kameraa ei ole käytettävissä tällä laitteella</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000000..9e580ec1d2
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Aucune caméra disponible sur l’appareil</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml
new file mode 100644
index 0000000000..e9ff7a2bd9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fur/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scansionadôr QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nissune fotocjamare disponibile sul dispositîf</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml
new file mode 100644
index 0000000000..866102f55b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-fy-rNL/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Gjin kamera beskikber op apparaat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml
new file mode 100644
index 0000000000..e326071aff
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gd/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Sganair QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Chan eil camara ri làimh air an uidheam</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml
new file mode 100644
index 0000000000..95eea190f8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Escáner de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Non hai cámara dispoñíbel no dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml
new file mode 100644
index 0000000000..29987db474
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gn/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR moha’ãngaha</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ndaipóri ta’ãngamýi mba’e’okápe</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000000..18860156b9
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-gu-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR સ્કેનર</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ઉપકરણ પર કોઈ કેમેરો ઉપલબ્ધ નથી</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000000..1bc5719127
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hi-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR स्कैनर</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">डिवाइस पर कोई कैमरा उपलब्ध नहीं है</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml
new file mode 100644
index 0000000000..5e64717bca
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hil/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml
new file mode 100644
index 0000000000..e3829462a1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skener</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Na uređaju nema kamere</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml
new file mode 100644
index 0000000000..357f90e1b6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hsb/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skener</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Na graće žana kamera k dispoziciji njeje</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml
new file mode 100644
index 0000000000..1818bcbaea
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hu/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-leolvasó</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nincs elérhető kamera az eszközön</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000000..5deb3d814c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-hy-rAM/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR սկաներ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Սարքում տեսախցիկը հասնելի չէ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000000..dfcbe5740d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ia/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanditor de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nulle camera disponibile sur le apparato</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000000..358ce12175
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-in/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Pemindai QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Tidak ada kamera yang tersedia di perangkat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml
new file mode 100644
index 0000000000..6927a627a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-is/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanni</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Engin myndavél tiltæk á tækinu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000000..7c936c965b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-it/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nessuna fotocamera disponibile sul dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml
new file mode 100644
index 0000000000..82896d047c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-iw/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">סורק QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">אין מצלמה זמינה במכשיר</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000000..a4eb8620c6
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ja/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR コードスキャナー</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">使用可能なカメラが端末にありません</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml
new file mode 100644
index 0000000000..6ed347acb1
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ka/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-წამკითხველი</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">მოწყობილობაზე კამერა არაა</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml
new file mode 100644
index 0000000000..c87ff57355
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kaa/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Qurılmada kamera joq</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml
new file mode 100644
index 0000000000..a95b95687a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kab/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">alnafraḍ QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ulac takamiṛat deg ibenk</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml
new file mode 100644
index 0000000000..dd74fd220b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR сканері</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Құрылғыда камера жоқ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml
new file mode 100644
index 0000000000..808536e55d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Xwînera QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Di cîhazê de kamera tune</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml
new file mode 100644
index 0000000000..d059ae06be
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-kn/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ಕ್ಯೂಆರ್ ಸ್ಕ್ಯಾನರ್</string>
+
+ </resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml
new file mode 100644
index 0000000000..b57f4803f5
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ko/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR 스캐너</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">기기에 사용 가능한 카메라 없음</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml
new file mode 100644
index 0000000000..aff4d28c6c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-lo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ຕົວສະແກນ QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ອຸປະກອນນີ້ບໍ່ມີກ້ອງ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml
new file mode 100644
index 0000000000..dde9b8fc28
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-lt/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaitytuvas</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Įrenginyje nėra kameros</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml
new file mode 100644
index 0000000000..e4e5924041
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-mix/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Ndatava QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Koo ña ndatava nu kaa ndusu ku</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml
new file mode 100644
index 0000000000..ce55ae4e3c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-mr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR स्कॅनर</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">उपकरणावर कोणताही कॅमेरा उपलब्ध नाही</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml
new file mode 100644
index 0000000000..5e66f1b09c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-my/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR ဖတ်ရန်</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">စက်တွင်ကင်မရာမပါပါ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..7e1f547927
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ingen kamera tilgjengelige på enheten</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000000..1b22f9fc30
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ne-rNP/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR स्क्यानर</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">यन्त्रमा कुनै क्यामरा उपलब्ध छैन</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000000..bbd95f9d57
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-nl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Geen camera beschikbaar op apparaat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml
new file mode 100644
index 0000000000..24e3198e26
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-nn-rNO/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skannar</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ingen kamera tilgjengelege på eininga</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml
new file mode 100644
index 0000000000..1828bb344f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-oc/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Numerizador QR còdi</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Cap de camèra pas disponibla sul periferic</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000000..097b5d7efb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rIN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR ਸਕੈਨਰ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ਡਿਵਾਈਸ ਉੱਤੇ ਕੈਮਰਾ ਉਪਲਬਧ ਨਹੀਂ ਹੈ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml
new file mode 100644
index 0000000000..425257dbbf
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pa-rPK/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">کیوآر کوڈ سکین والا</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">کوئی کیمرہ نہیں لبھیا</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml
new file mode 100644
index 0000000000..4f6016e055
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skaner kodów QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Urządzenie nie ma aparatu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..eb2379dd0f
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nenhuma câmera disponível no dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..cf4d062a1b
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Digitalizador QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nenhuma câmara disponível no dispositivo</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml
new file mode 100644
index 0000000000..46cc46ca31
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-rm/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nagina camera disponibla en l\'apparat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml
new file mode 100644
index 0000000000..534096197c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ro/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Scanner QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nu există camere disponibile pe dispozitiv</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml
new file mode 100644
index 0000000000..8c14a264d3
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ru/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Считыватель штрих-кодов</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">На этом устройстве камера недоступна</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml
new file mode 100644
index 0000000000..74a96b7d83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sat/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR ᱥᱠᱟᱱᱚᱨ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ᱥᱟᱫᱷᱚᱱ ᱨᱮ ᱠᱮᱢᱨᱟ ᱵᱟᱹᱱᱩᱜ-ᱟ</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml
new file mode 100644
index 0000000000..ee4c7fd470
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sc/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Iscansionadore de QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nissuna càmera a disponimentu in su dispositivu</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml
new file mode 100644
index 0000000000..d841d8365a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-si/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR සුපිරික්සකය</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">උපාංගයෙහි රූගතයක් නැත</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml
new file mode 100644
index 0000000000..1f60626aa0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skener QR kódov</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Zariadenie nemá fotoaparát</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml
new file mode 100644
index 0000000000..e44642f62c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-skr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR سکینر</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ڈیوائس تے کوئی کیمرہ دستیاب کائنی</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml
new file mode 100644
index 0000000000..efffc8172e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Bralnik QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Naprava nima razpoložljive kamere</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml
new file mode 100644
index 0000000000..8c8796d692
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sq/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Skanues QR-esh</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">S’ka kamera të përdorshme në pajisje</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml
new file mode 100644
index 0000000000..91503317dd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR скенер</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">На уређају није доступна камера</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml
new file mode 100644
index 0000000000..9091036029
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-su/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Paminday QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Henteu aya kaméra dina alat</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..cc0601fb83
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR-skanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ingen kamera tillgänglig på enheten</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml
new file mode 100644
index 0000000000..18cb652d7e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ta/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR வருடி</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">சாதனத்தில் படக்கருவி இல்லை</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml
new file mode 100644
index 0000000000..8ab2d2d6da
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-te/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR స్కానర్</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">పరికరంలో కెమెరా లేదు</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml
new file mode 100644
index 0000000000..f614203639
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tg/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Аксбардории QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Ягон камера дар дастгоҳ дастрас нест</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml
new file mode 100644
index 0000000000..03d2a7c3a8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-th/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ตัวสแกน QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ไม่มีกล้องที่พร้อมใช้งานบนอุปกรณ์</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml
new file mode 100644
index 0000000000..f61d1973fc
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tl/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Walang magagamit na camera sa aparato</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml
new file mode 100644
index 0000000000..43d57c4a0e
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tok/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">ilo lukin pi leko sona</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">ilo sina li jo ala e ilo lukin</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000000..b64a1decc7
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tr/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR tarayıcı</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Cihazda kamera yok</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml
new file mode 100644
index 0000000000..0f85b43439
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-trs/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Sa natsij nej QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Nitāj aga\’ nari ñadu\’ua nikāj aga\’ nan</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml
new file mode 100644
index 0000000000..2828e3f906
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-tt/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR сканеры</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Җиһазда камера юк</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml
new file mode 100644
index 0000000000..67ff29d85d
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ug/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR سايىلىغۇچ</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">كامېرا يوق ئىكەن</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml
new file mode 100644
index 0000000000..4154a29991
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-uk/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Сканер QR-кодів</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Немає доступу до камери пристрою</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml
new file mode 100644
index 0000000000..dc2d085e48
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-ur/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR سکینر</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">آلہ پر کوئی کمیرہ دستیاب نہیں ہے</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml
new file mode 100644
index 0000000000..ba70db4633
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-uz/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR skaner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Qurilmada kamera mavjud emas</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml
new file mode 100644
index 0000000000..78039314fd
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-vi/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Quét mã QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Hiện không tìm thấy máy ảnh trên thiết bị</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml
new file mode 100644
index 0000000000..a17b5f6d7a
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-yo/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">Síkánà QR</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">Kò sí ẹ̀rọ-ayàwòrán kankan lórí ẹ̀rọ yìí</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..cdf50696d8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">扫码器</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">设备上没有可用的相机</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..5023389874
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR Code 掃描器</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">裝置上無攝影機可用</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..004d4bbd59
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+<?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>
+ <color name="mozac_feature_qr_scan_success_color">#9059FF</color>
+</resources> \ No newline at end of file
diff --git a/mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml b/mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..dcf8ae522c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?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>
+
+ <!-- Content description (not visible, for screen readers etc.): Description of an image view. -->
+ <string name="mozac_feature_qr_scanner">QR scanner</string>
+
+ <!-- Text shown if no camera is available on the device and the QR scanner cannot be displayed. -->
+ <string name="mozac_feature_qr_scanner_no_camera">No camera available on device</string>
+
+</resources>
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt
new file mode 100644
index 0000000000..1c8ece369c
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFeatureTest.kt
@@ -0,0 +1,269 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.qr
+
+import android.Manifest.permission.CAMERA
+import android.content.pm.PackageManager
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.qr.QrFeature.Companion.QR_FRAGMENT_TAG
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.grantPermission
+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.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.openMocks
+
+@RunWith(AndroidJUnit4::class)
+class QrFeatureTest {
+
+ @Mock
+ lateinit var fragmentManager: FragmentManager
+
+ @Before
+ fun setUp() {
+ openMocks(this)
+
+ mock<FragmentTransaction>().let { transaction ->
+ whenever(fragmentManager.beginTransaction())
+ .thenReturn(transaction)
+ whenever(transaction.add(anyInt(), any(), anyString()))
+ .thenReturn(transaction)
+ whenever(transaction.remove(any()))
+ .thenReturn(transaction)
+ }
+ }
+
+ fun `scanning is in progress if the scanning fragment is shown`() {
+ val feature = QrFeature(testContext, fragmentManager)
+
+ assertFalse(feature.isScanInProgress)
+
+ doReturn(mock<QrFragment>()).`when`(fragmentManager).findFragmentByTag(QR_FRAGMENT_TAG)
+ assertTrue(feature.isScanInProgress)
+ }
+
+ @Test
+ fun `feature requests camera permission if required`() {
+ // Given
+ var callbackInvoked = false
+ val permissionsCallback: (permissions: Array<String>) -> Unit = {
+ callbackInvoked = true
+ }
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ onNeedToRequestPermissions = permissionsCallback,
+ )
+
+ // When
+ val scanResult = feature.scan()
+
+ // Then
+ assertFalse(scanResult)
+ assertTrue(callbackInvoked)
+ }
+
+ @Test
+ fun `scan starts qr fragment if permissions granted`() {
+ // Given
+ grantPermission(CAMERA)
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ )
+
+ // When
+ val scanResult = feature.scan()
+
+ // Then
+ assertTrue(scanResult)
+ verify(fragmentManager).beginTransaction()
+ }
+
+ @Test
+ fun `scan resumes qr fragment if permissions granted and scanning was already started`() {
+ grantPermission(CAMERA)
+ val feature = QrFeature(testContext, fragmentManager)
+ val qrFragment: QrFragment = mock()
+ doReturn(qrFragment).`when`(fragmentManager).findFragmentByTag(QR_FRAGMENT_TAG)
+
+ val scanResult = feature.scan()
+
+ assertTrue(scanResult)
+ verify(qrFragment).startScanning()
+ }
+
+ @Test
+ fun `onPermissionsResult displays scanner only if permission granted`() {
+ // Given
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+
+ // When
+ resolvePermissionRequestFrom(feature) { PermissionResolution.DENIED }
+
+ // Then
+ verify(feature, never()).scan(anyInt())
+ verify(feature).removeQrFragment()
+
+ // When
+ grantPermission(CAMERA)
+ resolvePermissionRequestFrom(feature) { PermissionResolution.GRANTED }
+
+ // Then
+ verify(feature, times(1)).scan(anyInt())
+ verify(feature, times(1)).removeQrFragment()
+ }
+
+ @Test
+ fun `scan result is forwarded to caller`() {
+ // Given
+ var scanResult: String? = null
+ val scanResultCallback: OnScanResult = { result ->
+ scanResult = result
+ }
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ onScanResult = scanResultCallback,
+ )
+
+ // When
+ feature.scanCompleteListener.onScanComplete("result")
+
+ // Then
+ assertEquals("result", scanResult)
+ }
+
+ @Test
+ fun `qr fragment is removed on back pressed`() {
+ // Given
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(mock())
+
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+
+ // When
+ feature.onBackPressed()
+
+ // Then
+ verify(feature).removeQrFragment()
+ }
+
+ @Test
+ fun `start attaches scan complete listener`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+ val listener = feature.scanCompleteListener
+
+ // When
+ feature.start()
+
+ // Then
+ verify(feature).setScanCompleteListener(listener)
+ }
+
+ @Test
+ fun `stop attaches a null listener`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+ val feature = spy(
+ QrFeature(
+ testContext,
+ fragmentManager,
+ ),
+ )
+
+ // When
+ feature.stop()
+
+ // Then
+ verify(feature).setScanCompleteListener(null)
+ }
+
+ @Test
+ fun `setScanCompleteListener allows setting a null callback in QrFragment`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ )
+ fragment.scanCompleteListener = feature.scanCompleteListener
+
+ // When
+ feature.setScanCompleteListener(null)
+ // Then
+ verify(fragment).scanCompleteListener = null
+ }
+
+ @Test
+ fun `setScanCompleteListener allows setting a valid callback in QrFragment`() {
+ // Given
+ val fragment = mock<QrFragment>()
+ whenever(fragmentManager.findFragmentByTag(QR_FRAGMENT_TAG))
+ .thenReturn(fragment)
+ val feature = QrFeature(
+ testContext,
+ fragmentManager,
+ )
+ fragment.scanCompleteListener = null
+
+ // When
+ feature.setScanCompleteListener(feature.scanCompleteListener)
+ // Then
+ verify(fragment).scanCompleteListener = feature.scanCompleteListener
+ }
+}
+
+private enum class PermissionResolution(val value: Int) {
+ GRANTED(PackageManager.PERMISSION_GRANTED),
+ DENIED(PackageManager.PERMISSION_DENIED),
+}
+
+private fun resolvePermissionRequestFrom(
+ feature: QrFeature,
+ resolution: () -> PermissionResolution,
+) {
+ feature.onPermissionsResult(emptyArray(), IntArray(1) { resolution().value })
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt
new file mode 100644
index 0000000000..98c58deec0
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/QrFragmentTest.kt
@@ -0,0 +1,786 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.qr
+
+import android.Manifest.permission
+import android.content.Context
+import android.content.pm.PackageManager
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.params.SessionConfiguration
+import android.media.Image
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper.getMainLooper
+import android.util.Size
+import android.view.Display
+import android.view.Surface
+import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
+import androidx.fragment.app.FragmentActivity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.BinaryBitmap
+import com.google.zxing.LuminanceSource
+import com.google.zxing.MultiFormatReader
+import com.google.zxing.NotFoundException
+import com.google.zxing.PlanarYUVLuminanceSource
+import mozilla.components.feature.qr.QrFragment.Companion.chooseOptimalSize
+import mozilla.components.feature.qr.views.AutoFitTextureView
+import mozilla.components.feature.qr.views.CustomViewFinder
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+import java.nio.ByteBuffer
+import java.util.concurrent.ExecutorService
+
+@RunWith(AndroidJUnit4::class)
+class QrFragmentTest {
+
+ @Test
+ fun `initialize QR fragment`() {
+ val scanCompleteListener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(scanCompleteListener))
+
+ qrFragment.scanCompleteListener?.onScanComplete("result")
+ shadowOf(getMainLooper()).idle()
+ verify(scanCompleteListener).onScanComplete("result")
+ }
+
+ @Test
+ fun `onPause closes camera, stops background thread, and shuts down executor service`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ qrFragment.onPause()
+
+ verify(qrFragment).stopBackgroundThread()
+ verify(qrFragment).stopExecutorService()
+ verify(qrFragment).closeCamera()
+ }
+
+ @Test
+ fun `onResume opens camera, starts background thread and starts executor service`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(context).checkPermission(eq(permission.CAMERA), anyInt(), anyInt())
+ doReturn(context).`when`(qrFragment).context
+ doNothing().`when`(qrFragment).startScanning()
+
+ qrFragment.onResume()
+
+ verify(qrFragment).startScanning()
+ }
+
+ @Test
+ fun `onResume avoids starting scanning if the camera permission is missing`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .`when`(context).checkPermission(eq(permission.CAMERA), anyInt(), anyInt())
+ doReturn(context).`when`(qrFragment).context
+ doNothing().`when`(qrFragment).startScanning()
+
+ qrFragment.onResume()
+
+ verify(qrFragment, never()).startScanning()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `WHEN running a device lower than P THEN startExecutorService should not be executed`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+ doNothing().`when`(qrFragment).maybeStartBackgroundThread()
+ doNothing().`when`(qrFragment).tryOpenCamera(anyInt(), anyInt(), anyBoolean())
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_GRANTED).`when`(context).checkSelfPermission(permission.CAMERA)
+ doReturn(context).`when`(qrFragment).context
+
+ qrFragment.onResume()
+
+ verify(qrFragment, never()).maybeStartExecutorService()
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `WHEN calling createCaptureSessionCompat on a device lower than P THEN use older API`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val camera = mock<CameraDevice>()
+ val imageSurface = mock<Surface>()
+ val surface = mock<Surface>()
+ val stateCallback = mock<CameraCaptureSession.StateCallback>()
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+
+ qrFragment.createCaptureSessionCompat(camera, imageSurface, surface, stateCallback)
+
+ @Suppress("DEPRECATION")
+ verify(camera).createCaptureSession(listOf(imageSurface, surface), stateCallback, null)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.P])
+ fun `WHEN calling createCaptureSessionCompat on a device higher than P THEN use newer api`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val camera = mock<CameraDevice>()
+ val imageSurface = mock<Surface>()
+ val surface = mock<Surface>()
+ val stateCallback = mock<CameraCaptureSession.StateCallback>()
+
+ doNothing().`when`(qrFragment).maybeStartExecutorService()
+ whenever(qrFragment.shouldStartExecutorService()).thenReturn(true)
+
+ qrFragment.backgroundExecutor = mock()
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+
+ qrFragment.createCaptureSessionCompat(camera, imageSurface, surface, stateCallback)
+
+ verify(camera).createCaptureSession(any<SessionConfiguration>())
+ }
+
+ @Test
+ fun `onStop resets state`() {
+ val qrFragment = QrFragment.newInstance(mock())
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ qrFragment.onStop()
+
+ assertEquals(QrFragment.STATE_FIND_QRCODE, QrFragment.qrState)
+ }
+
+ @Test
+ fun `onViewCreated sets initial state`() {
+ val qrFragment = QrFragment.newInstance(mock())
+ val view: View = mock()
+ val textureView: AutoFitTextureView = mock()
+ val viewFinder: CustomViewFinder = mock()
+ val cameraErrorView: TextView = mock()
+
+ whenever(view.findViewById<AutoFitTextureView>(R.id.texture)).thenReturn(textureView)
+ whenever(view.findViewById<CustomViewFinder>(R.id.view_finder)).thenReturn(viewFinder)
+ whenever(view.findViewById<TextView>(R.id.camera_error)).thenReturn(cameraErrorView)
+
+ qrFragment.onViewCreated(view, mock())
+ assertEquals(QrFragment.STATE_FIND_QRCODE, QrFragment.qrState)
+ }
+
+ @Test
+ fun `qr fragment has the correct scan message resource`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+
+ val qrFragmentWithMessage = QrFragment.newInstance(listener, R.string.mozac_feature_qr_scanner)
+ val qrFragmentNoMessage = QrFragment.newInstance(listener, null)
+
+ assertEquals(null, qrFragmentNoMessage.scanMessage)
+ assertEquals(R.string.mozac_feature_qr_scanner, qrFragmentWithMessage.scanMessage)
+ }
+
+ @Test
+ fun `listener is invoked on successful qr scan`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val source = mock<PlanarYUVLuminanceSource>()
+ val result = com.google.zxing.Result("qrcode-result", ByteArray(0), emptyArray(), BarcodeFormat.ITF)
+ whenever(reader.decodeWithState(any())).thenReturn(result)
+ qrFragment.multiFormatReader = reader
+ qrFragment.scanCompleteListener = listener
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ qrFragment.tryScanningSource(source)
+ shadowOf(getMainLooper()).idle()
+
+ verify(listener).onScanComplete(eq("qrcode-result"))
+ assertEquals(QrFragment.STATE_QRCODE_EXIST, QrFragment.qrState)
+ }
+
+ @Test
+ fun `resets state after each decoding attempt`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+
+ val source = mock<PlanarYUVLuminanceSource>()
+ val invertedSource = mock<PlanarYUVLuminanceSource>()
+
+ val bitmap = mock<BinaryBitmap>()
+ val invertedBitmap = mock<BinaryBitmap>()
+
+ whenever(source.invert()).thenReturn(invertedSource)
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ whenever(createBinaryBitmap(invertedSource)).thenReturn(invertedBitmap)
+ }
+
+ qrFragment.multiFormatReader = reader
+
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ qrFragment.tryScanningSource(source)
+
+ assertEquals(QrFragment.STATE_FIND_QRCODE, QrFragment.qrState)
+ verify(reader, times(2)).reset()
+ }
+
+ @Test
+ fun `don't consider scanning complete if decoding not in progress`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val source = mock<PlanarYUVLuminanceSource>()
+ qrFragment.scanCompleteListener = listener
+ qrFragment.multiFormatReader = reader
+ whenever(reader.decodeWithState(any())).thenThrow(NotFoundException::class.java)
+ QrFragment.qrState = QrFragment.STATE_FIND_QRCODE
+
+ qrFragment.tryScanningSource(source)
+
+ verify(reader, never()).decodeWithState(any())
+ verify(listener, never()).onScanComplete(any())
+ }
+
+ @Test
+ fun `early return null for decoding attempt if decoding not in progress`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val source = mock<PlanarYUVLuminanceSource>()
+ qrFragment.multiFormatReader = reader
+ whenever(reader.decodeWithState(any())).thenThrow(NotFoundException::class.java)
+ QrFragment.qrState = QrFragment.STATE_FIND_QRCODE
+ qrFragment.tryScanningSource(source)
+
+ verify(qrFragment, never()).decodeSource(any())
+ verify(reader, never()).decodeWithState(any())
+ verify(listener, never()).onScanComplete(any())
+ }
+
+ @Test
+ fun `async scanning decodes original unmodified source`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val imageCaptor = argumentCaptor<BinaryBitmap>()
+ val source = mock<LuminanceSource>()
+ val bitmap = mock<BinaryBitmap>()
+ val result = mock<com.google.zxing.Result>()
+ qrFragment.multiFormatReader = reader
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ }
+
+ whenever(reader.decodeWithState(bitmap)).thenReturn(result)
+
+ qrFragment.tryScanningSource(source)
+ verify(reader).decodeWithState(imageCaptor.capture())
+ assertSame(bitmap, imageCaptor.value)
+ }
+
+ @Test
+ fun `camera is closed on disconnect and error`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ var camera: CameraDevice = mock()
+ qrFragment.stateCallback.onDisconnected(camera)
+ verify(camera).close()
+
+ camera = mock()
+ qrFragment.stateCallback.onError(camera, 0)
+ verify(camera).close()
+ }
+
+ @Test
+ fun `catches and handles CameraAccessException when creating preview session`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ val camera: CameraDevice = mock()
+ whenever(camera.createCaptureRequest(anyInt())).thenThrow(CameraAccessException(123))
+ qrFragment.cameraDevice = camera
+
+ val textureView: AutoFitTextureView = mock()
+ whenever(textureView.surfaceTexture).thenReturn(mock())
+ qrFragment.textureView = textureView
+
+ qrFragment.previewSize = mock()
+
+ try {
+ qrFragment.createCameraPreviewSession()
+ } catch (e: CameraAccessException) {
+ fail("CameraAccessException should have been caught and logged, not re-thrown.")
+ }
+ }
+
+ @Test
+ fun `catches and handles IllegalStateException when creating preview session`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ val camera: CameraDevice = mock()
+ whenever(camera.createCaptureRequest(anyInt())).thenThrow(IllegalStateException("CameraDevice was already closed"))
+ qrFragment.cameraDevice = camera
+
+ val textureView: AutoFitTextureView = mock()
+ whenever(textureView.surfaceTexture).thenReturn(mock())
+ qrFragment.textureView = textureView
+
+ qrFragment.previewSize = mock()
+
+ try {
+ qrFragment.createCameraPreviewSession()
+ } catch (e: IllegalStateException) {
+ fail("IllegalStateException should have been caught and logged, not re-thrown.")
+ }
+ }
+
+ @Test
+ fun `catches and handles CameraAccessException when opening camera`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+
+ val cameraManager: CameraManager = mock()
+ whenever(cameraManager.openCamera(anyString(), any<CameraDevice.StateCallback>(), any()))
+ .thenThrow(CameraAccessException(123))
+
+ val activity: FragmentActivity = mock()
+ whenever(activity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+ whenever(qrFragment.activity).thenReturn(activity)
+ qrFragment.cameraId = "mockCamera"
+
+ try {
+ qrFragment.openCamera(1920, 1080)
+ } catch (e: CameraAccessException) {
+ fail("CameraAccessException should have been caught and logged, not re-thrown.")
+ }
+ }
+
+ @Test
+ fun `throws exception on device without camera`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+
+ val cameraManager: CameraManager = mock()
+ val activity: FragmentActivity = mock()
+ whenever(activity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+ whenever(qrFragment.activity).thenReturn(activity)
+
+ qrFragment.cameraId = null
+ try {
+ qrFragment.openCamera(1920, 1080)
+ fail("Expected IllegalStateException")
+ } catch (e: IllegalStateException) {
+ assertEquals("No camera found on device", e.message)
+ }
+ }
+
+ @Test
+ fun `choose optimal size`() {
+ var size = chooseOptimalSize(
+ arrayOf(Size(640, 480), Size(1024, 768)),
+ 640,
+ 480,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480)),
+ 1024,
+ 768,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(4, 3),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480), Size(320, 240)),
+ 2048,
+ 768,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(4, 3),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480), Size(320, 240)),
+ 1024,
+ 1024,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(4, 3),
+ )
+
+ assertEquals(640, size.width)
+ assertEquals(480, size.height)
+
+ size = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(786, 480), Size(320, 240)),
+ 2048,
+ 1024,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+
+ assertEquals(1024, size.width)
+ assertEquals(768, size.height)
+ }
+
+ @Test
+ fun `read image source adjusts for rowstride`() {
+ val image: Image = mock()
+ val plane: Image.Plane = mock()
+
+ `when`(image.height).thenReturn(1080)
+ `when`(image.width).thenReturn(1920)
+ `when`(plane.pixelStride).thenReturn(1)
+ `when`(image.planes).thenReturn(arrayOf(plane))
+
+ // Create an image source where rowstride is equal to the width
+ val bytesWithEqualRowStride: ByteBuffer = ByteBuffer.allocate(1080 * 1920)
+ `when`(plane.buffer).thenReturn(bytesWithEqualRowStride)
+ `when`(plane.rowStride).thenReturn(1920)
+ assertArrayEquals(bytesWithEqualRowStride.array(), QrFragment.readImageSource(image).matrix)
+
+ // Create an image source where rowstride is greater than the width
+ val bytesWithNotEqualRowStride: ByteBuffer = ByteBuffer.allocate(1080 * (1920 + 128))
+ `when`(plane.buffer).thenReturn(bytesWithNotEqualRowStride)
+ `when`(plane.rowStride).thenReturn(2048)
+
+ // The rowstride / offset should have been taken into account resulting
+ // in the same 1080 * 1920 image source as if the rowstride was equal to the width
+ assertArrayEquals(bytesWithEqualRowStride.array(), QrFragment.readImageSource(image).matrix)
+ }
+
+ @Test
+ fun `uses square preview of optimal size`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ val textureView: AutoFitTextureView = mock()
+ qrFragment.textureView = textureView
+
+ var optimalSize = chooseOptimalSize(
+ arrayOf(Size(640, 480), Size(1024, 768)),
+ 640,
+ 480,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+ qrFragment.adjustPreviewSize(optimalSize)
+ verify(textureView).setAspectRatio(480, 480)
+ assertEquals(480, qrFragment.previewSize?.width)
+ assertEquals(480, qrFragment.previewSize?.height)
+
+ optimalSize = chooseOptimalSize(
+ arrayOf(Size(1024, 768), Size(640, 480), Size(320, 240)),
+ 2048,
+ 1024,
+ QrFragment.MAX_PREVIEW_WIDTH,
+ QrFragment.MAX_PREVIEW_HEIGHT,
+ Size(16, 9),
+ )
+ qrFragment.adjustPreviewSize(optimalSize)
+ verify(textureView).setAspectRatio(768, 768)
+ assertEquals(768, qrFragment.previewSize?.width)
+ assertEquals(768, qrFragment.previewSize?.height)
+ }
+
+ @Test
+ fun `tryOpenCamera displays error message if no camera is available`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+
+ qrFragment.tryOpenCamera(0, 0)
+ verify(qrFragment.cameraErrorView).visibility = View.VISIBLE
+ verify(qrFragment.customViewFinder).visibility = View.GONE
+ }
+
+ @Test
+ fun `tryOpenCamera opens camera if available and hides the error message is shown`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ doNothing().`when`(qrFragment).openCamera(anyInt(), anyInt())
+
+ qrFragment.tryOpenCamera(0, 0, skipCheck = true)
+
+ verify(qrFragment).openCamera(0, 0)
+ verify(qrFragment.cameraErrorView).visibility = View.GONE
+ verify(qrFragment.customViewFinder).visibility = View.VISIBLE
+ }
+
+ @Test
+ fun `tryOpenCamera displays error message if camera throws exception`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+
+ val cameraManager: CameraManager = mock()
+ whenever(cameraManager.openCamera(anyString(), any<CameraDevice.StateCallback>(), any()))
+ .thenThrow(IllegalStateException("no camera"))
+
+ val activity: FragmentActivity = mock()
+ whenever(activity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(cameraManager)
+ whenever(qrFragment.activity).thenReturn(activity)
+ qrFragment.cameraId = "mockCamera"
+
+ qrFragment.tryOpenCamera(0, 0, skipCheck = true)
+ verify(qrFragment.cameraErrorView).visibility = View.VISIBLE
+ verify(qrFragment.customViewFinder).visibility = View.GONE
+ }
+
+ @Test
+ fun `tries to decode inverted source on original source decode exception`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val imageCaptor = argumentCaptor<BinaryBitmap>()
+
+ val source = mock<LuminanceSource>()
+ val invertedSource = mock<LuminanceSource>()
+ whenever(source.invert()).thenReturn(invertedSource)
+
+ val bitmap = mock<BinaryBitmap>()
+ val invertedBitmap = mock<BinaryBitmap>()
+
+ qrFragment.multiFormatReader = reader
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ whenever(createBinaryBitmap(invertedSource)).thenReturn(invertedBitmap)
+ }
+
+ whenever(reader.decodeWithState(bitmap)).thenThrow(NotFoundException::class.java)
+
+ qrFragment.tryScanningSource(source)
+
+ verify(reader, times(2)).decodeWithState(imageCaptor.capture())
+ assertSame(bitmap, imageCaptor.allValues[0])
+ assertSame(invertedBitmap, imageCaptor.allValues[1])
+ }
+
+ @Test
+ fun `tries to decode inverted source when original source returns null`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val reader = mock<MultiFormatReader>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val imageCaptor = argumentCaptor<BinaryBitmap>()
+
+ val source = mock<LuminanceSource>()
+ val invertedSource = mock<LuminanceSource>()
+ whenever(source.invert()).thenReturn(invertedSource)
+
+ val bitmap = mock<BinaryBitmap>()
+ val invertedBitmap = mock<BinaryBitmap>()
+
+ qrFragment.multiFormatReader = reader
+ QrFragment.qrState = QrFragment.STATE_DECODE_PROGRESS
+
+ with(qrFragment) {
+ whenever(createBinaryBitmap(source)).thenReturn(bitmap)
+ whenever(createBinaryBitmap(invertedSource)).thenReturn(invertedBitmap)
+ }
+
+ whenever(reader.decodeWithState(bitmap)).thenReturn(null)
+
+ qrFragment.tryScanningSource(source)
+
+ verify(reader, times(2)).decodeWithState(imageCaptor.capture())
+ assertSame(bitmap, imageCaptor.allValues[0])
+ assertSame(invertedBitmap, imageCaptor.allValues[1])
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `GIVEN a device rotation of 90 deg WHEN getting the device rotation on a device below SDK 30 THEN the rotation should be 90 deg`() {
+ val mockActivity: FragmentActivity = mock()
+ val mockManager: WindowManager = mock()
+ val mockDisplay: Display = mock()
+
+ val testRotation = Surface.ROTATION_90
+
+ whenever(mockActivity.windowManager).thenReturn(mockManager)
+ whenever(mockManager.defaultDisplay).thenReturn(mockDisplay)
+ whenever(mockDisplay.rotation).thenReturn(testRotation)
+
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ whenever(qrFragment.activity).thenReturn(mockActivity)
+
+ val rotation = qrFragment.getScreenRotation()
+
+ assertEquals(testRotation, rotation)
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `configureTransform uses getScreenRotation method to get rotation`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+ val textureView: AutoFitTextureView = mock()
+
+ qrFragment.previewSize = Size(4, 4)
+ qrFragment.textureView = textureView
+
+ qrFragment.configureTransform(4, 4)
+
+ verify(qrFragment, times(1)).getScreenRotation()
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `setUpCameraOutputs uses getScreenRotation method to get rotation`() {
+ val listener = mock<QrFragment.OnScanCompleteListener>()
+ val qrFragment = spy(QrFragment.newInstance(listener))
+
+ qrFragment.setUpCameraOutputs(4, 4)
+
+ verify(qrFragment, times(1)).getScreenRotation()
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `getDisplaySize calls defaultDisplay getSize for SDK below 30`() {
+ val mockActivity: FragmentActivity = mock()
+ val mockManager: WindowManager = mock()
+ val mockDisplay: Display = mock()
+
+ whenever(mockActivity.windowManager).thenReturn(mockManager)
+ whenever(mockManager.defaultDisplay).thenReturn(mockDisplay)
+ whenever(mockDisplay.getSize(any())).then { }
+
+ mockManager.getDisplaySize()
+
+ verify(mockDisplay, times(1)).getSize(any())
+ }
+
+ @Test
+ fun `maybeStartBackgroundThread does nothing if the thread already exists`() {
+ val qrFragment = QrFragment()
+ val existingBackgroundThread = HandlerThread("test").apply {
+ start() // need the thread to be "alive"
+ }
+ val existingBackgroundHandler: Handler = mock()
+ qrFragment.backgroundThread = existingBackgroundThread
+ qrFragment.backgroundHandler = existingBackgroundHandler
+
+ qrFragment.maybeStartBackgroundThread()
+
+ assertSame(existingBackgroundThread, qrFragment.backgroundThread)
+ assertSame(existingBackgroundHandler, qrFragment.backgroundHandler)
+ }
+
+ @Test
+ fun `maybeStartBackgroundThread creates and starts a new background thread and handler if doesn't already exist`() {
+ val qrFragment = QrFragment()
+ qrFragment.backgroundThread = null
+ qrFragment.backgroundHandler = null
+
+ qrFragment.maybeStartBackgroundThread()
+
+ assertNotNull(qrFragment.backgroundThread)
+ assertTrue(qrFragment.backgroundThread!!.isAlive)
+ assertNotNull(qrFragment.backgroundHandler)
+ }
+
+ @Test
+ fun `maybeStartExecutorService does nothing if the executor already exists`() {
+ val qrFragment = QrFragment()
+ val existingExecutorService: ExecutorService = mock()
+ qrFragment.backgroundExecutor = existingExecutorService
+
+ qrFragment.maybeStartExecutorService()
+
+ assertSame(existingExecutorService, qrFragment.backgroundExecutor)
+ }
+
+ @Test
+ fun `maybeStartExecutorService creates a new executor service if doesn't exist already`() {
+ val qrFragment = QrFragment()
+ qrFragment.backgroundExecutor = null
+
+ qrFragment.maybeStartExecutorService()
+
+ assertNotNull(null, qrFragment.backgroundExecutor)
+ }
+
+ @Test
+ fun `startScanning opens camera, starts background thread and starts executor service`() {
+ val qrFragment = spy(QrFragment.newInstance(mock()))
+ whenever(qrFragment.setUpCameraOutputs(anyInt(), anyInt())).then { }
+ val context: Context = mock()
+ doReturn(PackageManager.PERMISSION_GRANTED)
+ .`when`(context).checkPermission(eq(permission.CAMERA), anyInt(), anyInt())
+ doReturn(context).`when`(qrFragment).context
+
+ qrFragment.textureView = mock()
+ qrFragment.cameraErrorView = mock()
+ qrFragment.customViewFinder = mock()
+ qrFragment.startScanning()
+ verify(qrFragment, never()).tryOpenCamera(anyInt(), anyInt(), anyBoolean())
+
+ whenever(qrFragment.textureView.isAvailable).thenReturn(true)
+ qrFragment.cameraId = "mockCamera"
+ qrFragment.startScanning()
+ verify(qrFragment, times(2)).maybeStartBackgroundThread()
+ verify(qrFragment, times(2)).maybeStartExecutorService()
+ verify(qrFragment).tryOpenCamera(anyInt(), anyInt(), anyBoolean())
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt
new file mode 100644
index 0000000000..32ab0ab398
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/AutoFitTextureViewTest.kt
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.feature.qr.views
+
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AutoFitTextureViewTest {
+
+ @Test
+ fun `set aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(16, 9)
+
+ assertEquals(16, view.mRatioWidth)
+ assertEquals(9, view.mRatioHeight)
+ verify(view).requestLayout()
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `width must not be negative when setting aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(-1, 0)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `height must not be negative when setting aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(0, -1)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `width and height must not be negative when setting aspect ratio`() {
+ val view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(-1, -1)
+ }
+
+ @Test
+ fun `measure`() {
+ val width = View.MeasureSpec.getSize(640)
+ val height = View.MeasureSpec.getSize(480)
+
+ var view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(0, 0)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(640, 0)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(0, 480)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(16, 9)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(360, view.measuredHeight)
+
+ view = spy(AutoFitTextureView(ApplicationProvider.getApplicationContext()))
+ view.setAspectRatio(4, 3)
+ view.measure(width, height)
+ assertEquals(width, view.measuredWidth)
+ assertEquals(height, view.measuredHeight)
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.kt b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.kt
new file mode 100644
index 0000000000..a718d13ffb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/java/mozilla/components/feature/qr/views/CustomViewFinderTest.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.feature.qr.views
+
+import android.graphics.Rect
+import androidx.core.content.ContextCompat
+import androidx.core.text.HtmlCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.feature.qr.R
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+
+@RunWith(AndroidJUnit4::class)
+class CustomViewFinderTest {
+
+ @Test
+ fun `Static Layout is null on view init`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ assertNull(customViewFinder.scanMessageLayout)
+ }
+
+ @Test
+ fun `calling setupMessage initializes the StaticLayout`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+
+ CustomViewFinder.setMessage(R.string.mozac_feature_qr_scanner)
+ assertNotNull(CustomViewFinder.scanMessageStringRes)
+ }
+
+ @Test
+ fun `calling setupMessage with null value clears scan message `() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+
+ CustomViewFinder.setMessage(R.string.mozac_feature_qr_scanner)
+ assertNotNull(CustomViewFinder.scanMessageStringRes)
+
+ CustomViewFinder.setMessage(null)
+ assertNull(CustomViewFinder.scanMessageStringRes)
+ }
+
+ @Test
+ fun `message has the correct attributes`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+ val mockWidth = 200
+ val mockHeight = 300
+ val testScanMessage = HtmlCompat.fromHtml(
+ testContext.getString(R.string.mozac_feature_qr_scanner),
+ HtmlCompat.FROM_HTML_MODE_LEGACY,
+ )
+
+ CustomViewFinder.setMessage(R.string.mozac_feature_qr_scanner)
+ customViewFinder.computeViewFinderRect(mockWidth, mockHeight)
+
+ assertEquals(
+ ContextCompat.getColor(testContext, android.R.color.white),
+ customViewFinder.scanMessageLayout?.paint?.color,
+ )
+
+ assertEquals(
+ mockWidth * CustomViewFinder.DEFAULT_VIEWFINDER_WIDTH_RATIO,
+ customViewFinder.scanMessageLayout?.width?.toFloat(),
+ )
+
+ assertEquals(
+ testScanMessage,
+ customViewFinder.scanMessageLayout?.text,
+ )
+
+ assertEquals(
+ CustomViewFinder.SCAN_MESSAGE_TEXT_SIZE_SP,
+ customViewFinder.scanMessageLayout?.paint?.textSize,
+ )
+ }
+
+ @Test
+ fun `setViewFinderColor sets the proper color to viewfinder`() {
+ val customViewFinder = spy(CustomViewFinder(testContext))
+ val rect = mock(Rect::class.java)
+ customViewFinder.viewFinderRectangle = rect
+
+ customViewFinder.setViewFinderColor(android.R.color.holo_red_dark)
+
+ assertEquals(
+ android.R.color.holo_red_dark,
+ customViewFinder.viewFinderPaint.color,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/feature/qr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/feature/qr/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/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/feature/qr/src/test/resources/robolectric.properties b/mobile/android/android-components/components/feature/qr/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/feature/qr/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28