Source added

This commit is contained in:
Fr4nz D13trich 2025-11-20 09:26:33 +01:00
parent b2864b500e
commit ba28ca859e
8352 changed files with 1487182 additions and 1 deletions

19
qr/app/build.gradle.kts Normal file
View file

@ -0,0 +1,19 @@
plugins {
id("signal-sample-app")
alias(libs.plugins.compose.compiler)
}
android {
namespace = "org.signal.qrtest"
defaultConfig {
applicationId = "org.signal.qrtest"
}
}
dependencies {
implementation(project(":qr"))
implementation(libs.google.zxing.android.integration)
implementation(libs.google.zxing.core)
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.App">
<activity
android:name=".QrMainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,112 @@
package org.signal.qrtest
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import com.google.zxing.PlanarYUVLuminanceSource
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.AndroidLogger
import org.signal.core.util.logging.Log
import org.signal.qr.ImageProxyLuminanceSource
import org.signal.qr.QrProcessor
import org.signal.qr.QrScannerView
class QrMainActivity : AppCompatActivity() {
private lateinit var text: EditText
@SuppressLint("NewApi", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
Log.initialize(
AndroidLogger,
object : Log.Logger() {
override fun v(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) {
printlnFormatted('v', tag, message, t)
}
override fun d(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) {
printlnFormatted('d', tag, message, t)
}
override fun i(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) {
printlnFormatted('i', tag, message, t)
}
override fun w(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) {
printlnFormatted('w', tag, message, t)
}
override fun e(tag: String, message: String?, t: Throwable?, keepLonger: Boolean) {
printlnFormatted('e', tag, message, t)
}
override fun flush() {}
private fun printlnFormatted(level: Char, tag: String, message: String?, t: Throwable?) {
ThreadUtil.runOnMain {
val allText = text.text.toString() + "\n" + format(level, tag, message, t)
text.setText(allText)
}
}
private fun format(level: Char, tag: String, message: String?, t: Throwable?): String {
return if (t != null) {
String.format("%c[%s] %s %s:%s", level, tag, message, t.javaClass.simpleName, t.message)
} else {
String.format("%c[%s] %s", level, tag, message)
}
}
}
)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
text = findViewById(R.id.log)
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 1)
val scanner = findViewById<QrScannerView>(R.id.scanner)
scanner.start(this)
val qrText = findViewById<TextView>(R.id.text_qr_data)
scanner.qrData
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
qrText.text = it
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
}
val sourceView = findViewById<AppCompatImageView>(R.id.scanner_source)
val qrSize = findViewById<TextView>(R.id.text_size)
QrProcessor.listener = { source ->
val bitmap = when (source) {
is ImageProxyLuminanceSource -> Bitmap.createBitmap(source.render(), 0, source.width, source.width, source.height, Bitmap.Config.ARGB_8888)
is PlanarYUVLuminanceSource -> Bitmap.createBitmap(source.renderThumbnail(), 0, source.thumbnailWidth, source.thumbnailWidth, source.thumbnailHeight, Bitmap.Config.ARGB_8888)
else -> null
}
if (bitmap != null) {
ThreadUtil.runOnMain {
qrSize.text = "${bitmap.width} x ${bitmap.height}"
sourceView.setImageBitmap(bitmap)
}
}
}
findViewById<View>(R.id.camera_switch).setOnClickListener {
scanner.toggleCamera()
}
}
}

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#000000">
<group android:scaleX="2.61"
android:scaleY="2.61"
android:translateX="22.68"
android:translateY="22.68">
<path
android:fillColor="@android:color/white"
android:pathData="M5,16c0,3.87 3.13,7 7,7s7,-3.13 7,-7v-4L5,12v4zM16.12,4.37l2.1,-2.1 -0.82,-0.83 -2.3,2.31C14.16,3.28 13.12,3 12,3s-2.16,0.28 -3.09,0.75L6.6,1.44l-0.82,0.83 2.1,2.1C6.14,5.64 5,7.68 5,10v1h14v-1c0,-2.32 -1.14,-4.36 -2.88,-5.63zM9,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM15,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</group>
</vector>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical">
<Button
android:id="@+id/camera_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change Camera" />
<TextView
android:id="@+id/text_qr_data"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text_size"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/teal_200">
<org.signal.qr.QrScannerView
android:id="@+id/scanner"
android:layout_width="240dp"
android:layout_height="240dp" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/teal_700">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/scanner_source"
android:layout_width="240dp"
android:layout_height="240dp"
android:scaleType="fitCenter"
android:rotation="90"
tools:src="@tools:sample/backgrounds/scenic" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/log"
android:layout_width="match_parent"
android:layout_height="100dp" />
</LinearLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ContactsTest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">QR</string>
</resources>

View file

@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">#2c6bed</item>
<item name="colorPrimaryVariant">#1851b4</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

22
qr/lib/build.gradle.kts Normal file
View file

@ -0,0 +1,22 @@
plugins {
id("signal-library")
}
android {
namespace = "org.signal.qr"
}
dependencies {
implementation(project(":core-util"))
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.livedata.core)
implementation(libs.google.guava.android)
implementation(libs.google.zxing.android.integration)
implementation(libs.google.zxing.core)
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle,androidx.camera.view" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
</manifest>

View file

@ -0,0 +1,68 @@
package org.signal.qr
import android.graphics.ImageFormat
import androidx.camera.core.ImageProxy
import com.google.zxing.LuminanceSource
import java.nio.ByteBuffer
/**
* Luminance source that gets data via an [ImageProxy]. The main reason for this is because
* the Y-Plane provided by the camera framework can have a row stride (number of bytes that make up a row)
* that is different than the image width.
*
* An image width can be reported as 1080 but the row stride may be 1088. Thus when representing a row-major
* 2D array as a 1D array, the math can go sideways if width is used instead of row stride.
*/
class ImageProxyLuminanceSource(image: ImageProxy) : LuminanceSource(image.width, image.height) {
val yData: ByteArray
init {
require(image.format == ImageFormat.YUV_420_888) { "Invalid image format" }
yData = ByteArray(image.width * image.height)
val yBuffer: ByteBuffer = image.planes[0].buffer
yBuffer.position(0)
val yRowStride: Int = image.planes[0].rowStride
for (y in 0 until image.height) {
val yIndex: Int = y * yRowStride
yBuffer.position(yIndex)
yBuffer.get(yData, y * image.width, image.width)
}
}
override fun getRow(y: Int, row: ByteArray?): ByteArray {
require(y in 0 until height) { "Requested row is outside the image: $y" }
val toReturn: ByteArray = if (row == null || row.size < width) {
ByteArray(width)
} else {
row
}
val yIndex: Int = y * width
yData.copyInto(toReturn, 0, yIndex, yIndex + width)
return toReturn
}
override fun getMatrix(): ByteArray {
return yData
}
fun render(): IntArray {
val argbArray = IntArray(width * height)
var yValue: Int
yData.forEachIndexed { i, byte ->
yValue = (byte.toInt() and 0xff).coerceIn(0..255)
argbArray[i] = 255 shl 24 or (yValue and 255 shl 16) or (yValue and 255 shl 8) or (yValue and 255)
}
return argbArray
}
}

View file

@ -0,0 +1,85 @@
package org.signal.qr
import android.graphics.Bitmap
import androidx.camera.core.ImageProxy
import com.google.zxing.BinaryBitmap
import com.google.zxing.ChecksumException
import com.google.zxing.DecodeHintType
import com.google.zxing.FormatException
import com.google.zxing.LuminanceSource
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.Result
import com.google.zxing.common.HybridBinarizer
import com.google.zxing.qrcode.QRCodeReader
import org.signal.core.util.logging.Log
import java.nio.IntBuffer
/**
* Wraps [QRCodeReader] for use from API19 or API21+.
*/
class QrProcessor {
private val reader = QRCodeReader()
private var previousHeight = 0
private var previousWidth = 0
fun getScannedData(proxy: ImageProxy): String? {
return getScannedData(ImageProxyLuminanceSource(proxy))
}
fun getScannedData(
data: ByteArray,
width: Int,
height: Int
): String? {
return getScannedData(PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false))
}
fun getScannedData(bitmap: Bitmap?): String? {
if (bitmap == null) {
return null
}
val buffer = IntBuffer.allocate((bitmap.byteCount / 4) + 1)
bitmap.copyPixelsToBuffer(buffer)
return getScannedData(RGBLuminanceSource(bitmap.width, bitmap.height, buffer.array()))
}
private fun getScannedData(source: LuminanceSource): String? {
try {
if (source.width != previousWidth || source.height != previousHeight) {
Log.i(TAG, "Processing ${source.width} x ${source.height} image")
previousWidth = source.width
previousHeight = source.height
}
listener?.invoke(source)
val bitmap = BinaryBitmap(HybridBinarizer(source))
val result: Result? = reader.decode(bitmap, mapOf(DecodeHintType.TRY_HARDER to true, DecodeHintType.CHARACTER_SET to "ISO-8859-1"))
if (result != null) {
return result.text
}
} catch (e: NullPointerException) {
Log.w(TAG, "Random null", e)
} catch (e: ChecksumException) {
Log.w(TAG, "QR code read and decoded, but checksum failed", e)
} catch (e: FormatException) {
Log.w(TAG, "Thrown when a barcode was successfully detected, but some aspect of the content did not conform to the barcodes format rules.", e)
} catch (e: NotFoundException) {
// Thanks ZXing...
}
return null
}
companion object {
private val TAG = Log.tag(QrProcessor::class.java)
/** For debugging only */
var listener: ((LuminanceSource) -> Unit)? = null
}
}

View file

@ -0,0 +1,67 @@
package org.signal.qr
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
/**
* View for starting up a camera and scanning a QR-Code. Safe to use on an API version and
* will delegate to legacy camera APIs or CameraX APIs when appropriate.
*
* QR-code data is emitted via [qrData] observable.
*/
class QrScannerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var scannerView: ScannerView? = null
private val qrDataPublish: PublishSubject<String> = PublishSubject.create()
val qrData: Observable<String> = qrDataPublish
private fun initScannerView(forceLegacy: Boolean) {
val scannerView: FrameLayout = if (!forceLegacy) {
ScannerView21(context) { qrDataPublish.onNext(it) }
} else {
ScannerView19(context) { qrDataPublish.onNext(it) }
}
scannerView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(scannerView)
this.scannerView = (scannerView as ScannerView)
}
@JvmOverloads
fun start(lifecycleOwner: LifecycleOwner, forceLegacy: Boolean = false) {
if (scannerView != null) {
Log.w(TAG, "Attempt to start scanning that has already started")
return
}
initScannerView(forceLegacy)
scannerView?.start(lifecycleOwner)
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
qrDataPublish.onComplete()
}
})
}
fun toggleCamera() {
Log.d(TAG, "Toggling camera")
scannerView?.toggleCamera()
}
companion object {
private val TAG = Log.tag(QrScannerView::class.java)
}
}

View file

@ -0,0 +1,11 @@
package org.signal.qr
import androidx.lifecycle.LifecycleOwner
/**
* Common interface for interacting with QR scanning views.
*/
interface ScannerView {
fun start(lifecycleOwner: LifecycleOwner)
fun toggleCamera()
}

View file

@ -0,0 +1,62 @@
package org.signal.qr
import android.annotation.SuppressLint
import android.content.Context
import android.widget.FrameLayout
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.signal.qr.kitkat.QrCameraView
import org.signal.qr.kitkat.ScanListener
import org.signal.qr.kitkat.ScanningThread
/**
* API19 version of QR scanning. Uses deprecated camera APIs.
*/
@SuppressLint("ViewConstructor")
internal class ScannerView19 constructor(
context: Context,
private val scanListener: ScanListener
) : FrameLayout(context), ScannerView {
private var lifecycleOwner: LifecycleOwner? = null
private var scanningThread: ScanningThread? = null
private lateinit var cameraView: QrCameraView
private val lifecycleObserver = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
val scanningThread = ScanningThread()
scanningThread.setScanListener(scanListener)
cameraView.onResume()
cameraView.setPreviewCallback(scanningThread)
scanningThread.start()
this@ScannerView19.scanningThread = scanningThread
}
override fun onPause(owner: LifecycleOwner) {
cameraView.onPause()
scanningThread?.stopScanning()
scanningThread = null
}
}
init {
cameraView = QrCameraView(context)
cameraView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(cameraView)
}
override fun start(lifecycleOwner: LifecycleOwner) {
this.lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver)
this.lifecycleOwner = lifecycleOwner
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
}
override fun toggleCamera() {
cameraView.toggleCamera()
lifecycleOwner?.let {
lifecycleObserver.onPause(it)
lifecycleObserver.onResume(it)
}
}
}

View file

@ -0,0 +1,123 @@
package org.signal.qr
import android.annotation.SuppressLint
import android.content.Context
import android.util.Size
import android.widget.FrameLayout
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import org.signal.core.util.logging.Log
import org.signal.qr.kitkat.ScanListener
import java.util.concurrent.Executors
/**
* API21+ version of QR scanning view. Uses camerax APIs.
*/
@SuppressLint("ViewConstructor")
internal class ScannerView21 constructor(
context: Context,
private val listener: ScanListener
) : FrameLayout(context), ScannerView {
private var lifecyleOwner: LifecycleOwner? = null
private val analyzerExecutor = Executors.newSingleThreadExecutor()
private var cameraProvider: ProcessCameraProvider? = null
private var cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var camera: Camera? = null
private var previewView: PreviewView
private val qrProcessor = QrProcessor()
private val lifecycleObserver: DefaultLifecycleObserver = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
cameraProvider = null
camera = null
analyzerExecutor.shutdown()
}
}
init {
previewView = PreviewView(context)
previewView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(previewView)
}
override fun toggleCamera() {
cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
lifecyleOwner?.let { start(it) }
}
override fun start(lifecycleOwner: LifecycleOwner) {
this.lifecyleOwner?.lifecycle?.removeObserver(lifecycleObserver)
this.lifecyleOwner = lifecycleOwner
previewView.post {
Log.i(TAG, "Starting")
ProcessCameraProvider.getInstance(context).apply {
addListener({
try {
onCameraProvider(lifecycleOwner, get())
} catch (e: Exception) {
Log.w(TAG, e)
}
}, ContextCompat.getMainExecutor(context))
}
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
}
private fun onCameraProvider(lifecycle: LifecycleOwner, cameraProvider: ProcessCameraProvider?) {
if (cameraProvider == null) {
Log.w(TAG, "Camera provider is null")
return
}
Log.i(TAG, "Initializing use cases")
val resolution = Size(480, 640)
val preview = Preview.Builder()
.setTargetResolution(resolution)
.build()
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(resolution)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(analyzerExecutor) { proxy ->
proxy.use {
val data: String? = qrProcessor.getScannedData(it)
if (data != null) {
listener.onQrDataFound(data)
}
}
}
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(lifecycle, cameraSelector, preview, imageAnalysis)
preview.setSurfaceProvider(previewView.surfaceProvider)
Log.d(TAG, "Preview: ${preview.resolutionInfo}")
Log.d(TAG, "Analysis: ${imageAnalysis.resolutionInfo}")
this.cameraProvider = cameraProvider
}
companion object {
private val TAG = Log.tag(ScannerView21::class.java)
}
}

View file

@ -0,0 +1,33 @@
package org.signal.qr.kitkat;
import android.content.Context;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private boolean ready;
@SuppressWarnings("deprecation")
public CameraSurfaceView(Context context) {
super(context);
getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
getHolder().addCallback(this);
}
public boolean isReady() {
return ready;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
ready = true;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
ready = false;
}
}

View file

@ -0,0 +1,109 @@
package org.signal.qr.kitkat;
import android.app.Activity;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.util.DisplayMetrics;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@SuppressWarnings("deprecation")
public class CameraUtils {
private static final String TAG = Log.tag(CameraUtils.class);
/*
* modified from: https://github.com/commonsguy/cwac-camera/blob/master/camera/src/com/commonsware/cwac/camera/CameraUtils.java
*/
public static @Nullable Size getPreferredPreviewSize(int displayOrientation,
int width,
int height,
@NonNull Parameters parameters) {
final int targetWidth = displayOrientation % 180 == 90 ? height : width;
final int targetHeight = displayOrientation % 180 == 90 ? width : height;
final double targetRatio = (double) targetWidth / targetHeight;
Log.d(TAG, String.format(Locale.US,
"getPreferredPreviewSize(%d, %d, %d) -> target %dx%d, AR %.02f",
displayOrientation, width, height,
targetWidth, targetHeight, targetRatio));
List<Size> sizes = parameters.getSupportedPreviewSizes();
List<Size> ideals = new LinkedList<>();
List<Size> bigEnough = new LinkedList<>();
for (Size size : sizes) {
Log.d(TAG, String.format(Locale.US, " %dx%d (%.02f)", size.width, size.height, (float)size.width / size.height));
if (size.height == size.width * targetRatio && size.height >= targetHeight && size.width >= targetWidth) {
ideals.add(size);
Log.d(TAG, " (ideal ratio)");
} else if (size.width >= targetWidth && size.height >= targetHeight) {
bigEnough.add(size);
Log.d(TAG, " (good size, suboptimal ratio)");
}
}
if (!ideals.isEmpty()) return Collections.min(ideals, new AreaComparator());
else if (!bigEnough.isEmpty()) return Collections.min(bigEnough, new AspectRatioComparator(targetRatio));
else return Collections.max(sizes, new AreaComparator());
}
// based on
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
// and http://stackoverflow.com/a/10383164/115145
public static int getCameraDisplayOrientation(@NonNull Activity activity,
@NonNull CameraInfo info)
{
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
return (360 - ((info.orientation + degrees) % 360)) % 360;
} else {
return (info.orientation - degrees + 360) % 360;
}
}
private static class AreaComparator implements Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum(lhs.width * lhs.height - rhs.width * rhs.height);
}
}
private static class AspectRatioComparator extends AreaComparator {
private final double target;
public AspectRatioComparator(double target) {
this.target = target;
}
@Override
public int compare(Size lhs, Size rhs) {
final double lhsDiff = Math.abs(target - (double) lhs.width / lhs.height);
final double rhsDiff = Math.abs(target - (double) rhs.width / rhs.height);
if (lhsDiff < rhsDiff) return -1;
else if (lhsDiff > rhsDiff) return 1;
else return super.compare(lhs, rhs);
}
}
}

View file

@ -0,0 +1,480 @@
/*
Copyright (c) 2013-2014 CommonsWare, LLC
Portions Copyright (C) 2007 The Android Open Source Project
<p>
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 org.signal.qr.kitkat;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.OrientationEventListener;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("deprecation")
public class QrCameraView extends ViewGroup {
private static final String TAG = Log.tag(QrCameraView.class);
private final CameraSurfaceView surface;
private final OnOrientationChange onOrientationChange;
private volatile Optional<Camera> camera = Optional.empty();
private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK;
private volatile int displayOrientation = -1;
private @NonNull State state = State.PAUSED;
private @Nullable Size previewSize;
private final List<CameraViewListener> listeners = Collections.synchronizedList(new LinkedList<>());
private int outputOrientation = -1;
public QrCameraView(Context context) {
this(context, null);
}
public QrCameraView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QrCameraView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setBackgroundColor(Color.BLACK);
surface = new CameraSurfaceView(getContext());
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
addView(surface);
}
public void onResume() {
if (state != State.PAUSED) return;
state = State.RESUMED;
Log.i(TAG, "onResume() queued");
enqueueTask(new SerialAsyncTask<Void>() {
@Override
protected
@Nullable
Void onRunBackground() {
try {
long openStartMillis = System.currentTimeMillis();
camera = Optional.ofNullable(Camera.open(cameraId));
Log.i(TAG, "camera.open() -> " + (System.currentTimeMillis() - openStartMillis) + "ms");
synchronized (QrCameraView.this) {
QrCameraView.this.notifyAll();
}
camera.ifPresent(value -> onCameraReady(value));
} catch (Exception e) {
Log.w(TAG, e);
}
return null;
}
@Override
protected void onPostMain(Void avoid) {
if (!camera.isPresent()) {
Log.w(TAG, "tried to open camera but got null");
for (CameraViewListener listener : listeners) {
listener.onCameraFail();
}
return;
}
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
onOrientationChange.enable();
}
Log.i(TAG, "onResume() completed");
}
});
}
public void onPause() {
if (state == State.PAUSED) return;
state = State.PAUSED;
Log.i(TAG, "onPause() queued");
enqueueTask(new SerialAsyncTask<Void>() {
private Optional<Camera> cameraToDestroy;
@Override
protected void onPreMain() {
cameraToDestroy = camera;
camera = Optional.empty();
}
@Override
protected Void onRunBackground() {
if (cameraToDestroy.isPresent()) {
try {
stopPreview();
cameraToDestroy.get().setPreviewCallback(null);
cameraToDestroy.get().release();
Log.w(TAG, "released old camera instance");
} catch (Exception e) {
Log.w(TAG, e);
}
}
return null;
}
@Override protected void onPostMain(Void avoid) {
onOrientationChange.disable();
displayOrientation = -1;
outputOrientation = -1;
removeView(surface);
addView(surface);
Log.i(TAG, "onPause() completed");
}
});
for (CameraViewListener listener : listeners) {
listener.onCameraStop();
}
}
public boolean isStarted() {
return state != State.PAUSED;
}
public void toggleCamera() {
if (cameraId == CameraInfo.CAMERA_FACING_BACK) {
cameraId = CameraInfo.CAMERA_FACING_FRONT;
} else {
cameraId = CameraInfo.CAMERA_FACING_BACK;
}
}
@SuppressWarnings("SuspiciousNameCombination")
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
final int height = b - t;
final int previewWidth;
final int previewHeight;
if (camera.isPresent() && previewSize != null) {
if (displayOrientation == 90 || displayOrientation == 270) {
previewWidth = previewSize.height;
previewHeight = previewSize.width;
} else {
previewWidth = previewSize.width;
previewHeight = previewSize.height;
}
} else {
previewWidth = width;
previewHeight = height;
}
if (previewHeight == 0 || previewWidth == 0) {
Log.w(TAG, "skipping layout due to zero-width/height preview size");
return;
}
if (width * previewHeight > height * previewWidth) {
final int scaledChildHeight = previewHeight * width / previewWidth;
surface.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2);
} else {
final int scaledChildWidth = previewWidth * height / previewHeight;
surface.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.i(TAG, "onSizeChanged(" + oldw + "x" + oldh + " -> " + w + "x" + h + ")");
super.onSizeChanged(w, h, oldw, oldh);
camera.ifPresent(value -> startPreview(value.getParameters()));
}
public void addListener(@NonNull CameraViewListener listener) {
listeners.add(listener);
}
public void setPreviewCallback(final @NonNull PreviewCallback previewCallback) {
enqueueTask(new PostInitializationTask<Void>() {
@Override
protected void onPostMain(Void avoid) {
camera.ifPresent(value -> value.setPreviewCallback((data, camera) -> {
if (!QrCameraView.this.camera.isPresent()) {
return;
}
final int rotation = getCameraPictureOrientation();
final Size previewSize = camera.getParameters().getPreviewSize();
if (data != null) {
previewCallback.onPreviewFrame(new PreviewFrame(data, previewSize.width, previewSize.height, rotation));
}
}));
}
});
}
private void onCameraReady(final @NonNull Camera camera) {
final Parameters parameters = camera.getParameters();
parameters.setRecordingHint(true);
final List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
displayOrientation = CameraUtils.getCameraDisplayOrientation(getActivity(), getCameraInfo());
camera.setDisplayOrientation(displayOrientation);
camera.setParameters(parameters);
enqueueTask(new PostInitializationTask<Void>() {
@Override
protected Void onRunBackground() {
try {
camera.setPreviewDisplay(surface.getHolder());
startPreview(parameters);
} catch (Exception e) {
Log.w(TAG, "couldn't set preview display", e);
}
return null;
}
});
}
private void startPreview(final @NonNull Parameters parameters) {
camera.ifPresent(camera -> {
try {
final Size preferredPreviewSize = getPreferredPreviewSize(parameters);
if (preferredPreviewSize != null && !parameters.getPreviewSize().equals(preferredPreviewSize)) {
Log.i(TAG, "starting preview with size " + preferredPreviewSize.width + "x" + preferredPreviewSize.height);
if (state == State.ACTIVE) stopPreview();
previewSize = preferredPreviewSize;
parameters.setPreviewSize(preferredPreviewSize.width, preferredPreviewSize.height);
camera.setParameters(parameters);
} else {
previewSize = parameters.getPreviewSize();
}
long previewStartMillis = System.currentTimeMillis();
camera.startPreview();
Log.i(TAG, "camera.startPreview() -> " + (System.currentTimeMillis() - previewStartMillis) + "ms");
state = State.ACTIVE;
ThreadUtil.runOnMain(() -> {
requestLayout();
for (CameraViewListener listener : listeners) {
listener.onCameraStart();
}
});
} catch (Exception e) {
Log.w(TAG, e);
}
});
}
private void stopPreview() {
camera.ifPresent(camera -> {
try {
camera.stopPreview();
state = State.RESUMED;
} catch (Exception e) {
Log.w(TAG, e);
}
});
}
private Size getPreferredPreviewSize(@NonNull Parameters parameters) {
return CameraUtils.getPreferredPreviewSize(displayOrientation,
getMeasuredWidth(),
getMeasuredHeight(),
parameters);
}
private int getCameraPictureOrientation() {
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
outputOrientation = getCameraPictureRotation(getActivity().getWindowManager()
.getDefaultDisplay()
.getOrientation());
} else if (getCameraInfo().facing == CameraInfo.CAMERA_FACING_FRONT) {
outputOrientation = (360 - displayOrientation) % 360;
} else {
outputOrientation = displayOrientation;
}
return outputOrientation;
}
private @NonNull CameraInfo getCameraInfo() {
final CameraInfo info = new CameraInfo();
Camera.getCameraInfo(cameraId, info);
return info;
}
// XXX this sucks
private Activity getActivity() {
return (Activity) getContext();
}
public int getCameraPictureRotation(int orientation) {
final CameraInfo info = getCameraInfo();
final int rotation;
orientation = (orientation + 45) / 90 * 90;
if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else {
rotation = (info.orientation + orientation) % 360;
}
return rotation;
}
private class OnOrientationChange extends OrientationEventListener {
public OnOrientationChange(Context context) {
super(context);
disable();
}
@Override
public void onOrientationChanged(int orientation) {
camera.ifPresent(camera -> {
if (orientation != ORIENTATION_UNKNOWN) {
int newOutputOrientation = getCameraPictureRotation(orientation);
if (newOutputOrientation != outputOrientation) {
outputOrientation = newOutputOrientation;
Parameters params = camera.getParameters();
params.setRotation(outputOrientation);
try {
camera.setParameters(params);
} catch (Exception e) {
Log.e(TAG, "Exception updating camera parameters in orientation change", e);
}
}
}
});
}
}
private void enqueueTask(SerialAsyncTask<?> job) {
AsyncTask.SERIAL_EXECUTOR.execute(job);
}
public static abstract class SerialAsyncTask<Result> implements Runnable {
@Override
public final void run() {
if (!onWait()) {
Log.w(TAG, "skipping task, preconditions not met in onWait()");
return;
}
ThreadUtil.runOnMainSync(this::onPreMain);
final Result result = onRunBackground();
ThreadUtil.runOnMainSync(() -> onPostMain(result));
}
protected boolean onWait() {return true;}
protected void onPreMain() {}
protected Result onRunBackground() {return null;}
protected void onPostMain(Result result) {}
}
private abstract class PostInitializationTask<Result> extends SerialAsyncTask<Result> {
@Override protected boolean onWait() {
synchronized (QrCameraView.this) {
if (!camera.isPresent()) {
return false;
}
while (getMeasuredHeight() <= 0 || getMeasuredWidth() <= 0 || !surface.isReady()) {
Log.i(TAG, String.format("waiting. surface ready? %s", surface.isReady()));
waitFor();
}
return true;
}
}
}
private void waitFor() {
try {
wait(0);
} catch (InterruptedException ie) {
throw new AssertionError(ie);
}
}
public interface CameraViewListener {
void onImageCapture(@NonNull final byte[] imageBytes);
void onCameraFail();
void onCameraStart();
void onCameraStop();
}
public interface PreviewCallback {
void onPreviewFrame(@NonNull PreviewFrame frame);
}
public static class PreviewFrame {
private final @NonNull byte[] data;
private final int width;
private final int height;
private final int orientation;
public PreviewFrame(@NonNull byte[] data, int width, int height, int orientation) {
this.data = data;
this.width = width;
this.height = height;
this.orientation = orientation;
}
public @NonNull byte[] getData() {
return data;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getOrientation() {
return orientation;
}
}
private enum State {
PAUSED, RESUMED, ACTIVE
}
}

View file

@ -0,0 +1,7 @@
package org.signal.qr.kitkat;
import androidx.annotation.NonNull;
public interface ScanListener {
void onQrDataFound(@NonNull String data);
}

View file

@ -0,0 +1,88 @@
package org.signal.qr.kitkat;
import androidx.annotation.NonNull;
import com.google.zxing.DecodeHintType;
import org.signal.core.util.logging.Log;
import org.signal.qr.QrProcessor;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static org.signal.qr.kitkat.QrCameraView.PreviewCallback;
import static org.signal.qr.kitkat.QrCameraView.PreviewFrame;
public class ScanningThread extends Thread implements PreviewCallback {
private static final String TAG = Log.tag(ScanningThread.class);
private final QrProcessor processor = new QrProcessor();
private final AtomicReference<ScanListener> scanListener = new AtomicReference<>();
private final Map<DecodeHintType, String> hints = new HashMap<>();
private boolean scanning = true;
private PreviewFrame previewFrame;
public void setCharacterSet(String characterSet) {
hints.put(DecodeHintType.CHARACTER_SET, characterSet);
}
public void setScanListener(ScanListener scanListener) {
this.scanListener.set(scanListener);
}
@Override
public void onPreviewFrame(@NonNull PreviewFrame previewFrame) {
try {
synchronized (this) {
this.previewFrame = previewFrame;
this.notify();
}
} catch (RuntimeException e) {
Log.w(TAG, e);
}
}
@Override
public void run() {
while (true) {
PreviewFrame ourFrame;
synchronized (this) {
while (scanning && previewFrame == null) {
waitFor();
}
if (!scanning) return;
else ourFrame = previewFrame;
previewFrame = null;
}
String data = processor.getScannedData(ourFrame.getData(), ourFrame.getWidth(), ourFrame.getHeight());
ScanListener scanListener = this.scanListener.get();
if (data != null && scanListener != null) {
scanListener.onQrDataFound(data);
return;
}
}
}
public void stopScanning() {
synchronized (this) {
scanning = false;
notify();
}
}
private void waitFor() {
try {
wait(0);
} catch (InterruptedException ie) {
throw new AssertionError(ie);
}
}
}