diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/feature/qr | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-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')
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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 Binary files differnew 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 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 |