Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="org.opencv"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<application>
</application>
</manifest>

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.barcode;
public class ModuleDescriptor {
public static final String MODULE_ID = "com.google.android.gms.vision.barcode";
public static final int MODULE_VERSION = 1;
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.dynamite;
public class ModuleDescriptor {
public static final String MODULE_ID = "com.google.android.gms.vision.dynamite";
public static final int MODULE_VERSION = 1;
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.dynamite.barcode;
public class ModuleDescriptor {
public static final String MODULE_ID = "com.google.android.gms.vision.dynamite.barcode";
public static final int MODULE_VERSION = 1;
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.dynamite.face;
public class ModuleDescriptor {
public static final String MODULE_ID = "com.google.android.gms.vision.dynamite.face";
public static final int MODULE_VERSION = 1;
}

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.face;
public class ModuleDescriptor {
public static final String MODULE_ID = "com.google.android.gms.vision.face";
public static final int MODULE_VERSION = 1;
}

View file

@ -0,0 +1,11 @@
/**
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.dynamite.descriptors.com.google.mlkit.dynamite.face;
public class ModuleDescriptor {
public static final String MODULE_ID = "com.google.mlkit.dynamite.face";
public static final int MODULE_VERSION = 1;
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode
import android.content.Context
import androidx.annotation.Keep
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetector
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetectorCreator
import com.google.android.gms.dynamic.unwrap
import org.microg.gms.vision.barcode.BarcodeDetector
@Keep
class ChimeraNativeBarcodeDetectorCreator : INativeBarcodeDetectorCreator.Stub() {
override fun create(context: IObjectWrapper, options: BarcodeDetectorOptions): INativeBarcodeDetector {
return BarcodeDetector(context.unwrap<Context>()!!, options)
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode.mlkit
import android.content.Context
import androidx.annotation.Keep
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.mlkit.vision.barcode.aidls.IBarcodeScannerCreator
import com.google.mlkit.vision.barcode.aidls.IBarcodeScanner
import com.google.mlkit.vision.barcode.internal.BarcodeScannerOptions
import org.microg.gms.vision.barcode.BarcodeScanner
@Keep
class BarcodeScannerCreator : IBarcodeScannerCreator.Stub() {
override fun create(context: IObjectWrapper, options: BarcodeScannerOptions): IBarcodeScanner {
return BarcodeScanner(context.unwrap<Context>()!!, options)
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.client
import android.content.Context
import androidx.annotation.Keep
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetector
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetectorCreator
import org.microg.gms.vision.barcode.BarcodeDetector
@Keep
class DynamiteNativeBarcodeDetectorCreator : INativeBarcodeDetectorCreator.Stub() {
override fun create(context: IObjectWrapper, options: BarcodeDetectorOptions): INativeBarcodeDetector {
return BarcodeDetector(context.unwrap<Context>()!!, options)
}
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2025, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.client
import android.content.Context
import android.os.SystemClock
import android.util.Log
import androidx.annotation.Keep
import com.google.android.gms.common.GooglePlayServicesUtil
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.android.gms.vision.face.internal.client.DetectionOptions
import com.google.android.gms.vision.face.internal.client.INativeFaceDetector
import com.google.android.gms.vision.face.internal.client.INativeFaceDetectorCreator
import org.microg.gms.vision.face.TAG
import org.microg.gms.vision.face.FaceDetector
import org.opencv.android.OpenCVLoader
@Keep
class DynamiteNativeFaceDetectorCreator : INativeFaceDetectorCreator.Stub() {
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: DetectionOptions?): INativeFaceDetector? {
Log.d(TAG, "DynamiteNativeFaceDetectorCreator newFaceDetector faceDetectionOptions:${faceDetectionOptions.toString()}")
try {
val elapsedRealtime = SystemClock.elapsedRealtime()
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
Log.d(TAG, "newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
if (!OpenCVLoader.initLocal()) {
throw RuntimeException("Unable to load OpenCV")
}
Log.d(TAG, "DynamiteNativeFaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
return FaceDetector(remoteContext, faceDetectionOptions)
} catch (e: Throwable) {
Log.w(TAG, "DynamiteNativeFaceDetectorCreator newFaceDetector load failed ", e)
return null
}
}
}

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2025, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face
import android.content.Context
import android.os.SystemClock
import android.util.Log
import androidx.annotation.Keep
import com.google.android.gms.common.GooglePlayServicesUtil
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.android.gms.vision.face.internal.client.DetectionOptions
import com.google.android.gms.vision.face.internal.client.INativeFaceDetector
import com.google.android.gms.vision.face.internal.client.INativeFaceDetectorCreator
import org.microg.gms.vision.face.FaceDetector
import org.microg.gms.vision.face.TAG
import org.opencv.android.OpenCVLoader
@Keep
class ChimeraNativeFaceDetectorCreator : INativeFaceDetectorCreator.Stub() {
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: DetectionOptions?): INativeFaceDetector? {
Log.d(TAG, "ChimeraNativeFaceDetectorCreator newFaceDetector faceDetectionOptions:${faceDetectionOptions.toString()}")
try {
val elapsedRealtime = SystemClock.elapsedRealtime()
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
Log.d(TAG, "newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
if (!OpenCVLoader.initLocal()) {
throw RuntimeException("Unable to load OpenCV")
}
Log.d(TAG, "ChimeraNativeFaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
return FaceDetector(remoteContext, faceDetectionOptions)
} catch (e: Throwable) {
Log.w(TAG, "ChimeraNativeFaceDetectorCreator newFaceDetector load failed ", e)
return null
}
}
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2025, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face.mlkit
import android.content.Context
import android.os.SystemClock
import android.util.Log
import androidx.annotation.Keep
import com.google.android.gms.common.GooglePlayServicesUtil
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.mlkit.vision.face.FaceDetectionOptions
import com.google.mlkit.vision.face.aidls.IFaceDetector
import com.google.mlkit.vision.face.aidls.IFaceDetectorCreator
import org.microg.gms.vision.face.TAG
import org.microg.gms.vision.face.mlkit.FaceDetector
import org.opencv.android.OpenCVLoader
@Keep
class FaceDetectorCreator : IFaceDetectorCreator.Stub() {
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: FaceDetectionOptions?): IFaceDetector? {
Log.d(TAG, "MLKit newFaceDetector options:${faceDetectionOptions}")
try {
val elapsedRealtime = SystemClock.elapsedRealtime()
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
Log.d(TAG, "newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
if (!OpenCVLoader.initLocal()) {
throw RuntimeException("Unable to load OpenCV")
}
Log.d(TAG, "FaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
return FaceDetector(remoteContext, faceDetectionOptions)
} catch (e: Throwable) {
Log.w(TAG, "FaceDetectorCreator newFaceDetector load failed ", e)
return null
}
}
}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.mlkit.vision.face.bundled.internal
import android.content.Context
import android.os.SystemClock
import android.util.Log
import androidx.annotation.Keep
import com.google.android.gms.common.GooglePlayServicesUtil
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.mlkit.vision.face.FaceDetectionOptions
import com.google.mlkit.vision.face.aidls.IFaceDetector
import com.google.mlkit.vision.face.aidls.IFaceDetectorCreator
import org.microg.gms.vision.face.TAG
import org.microg.gms.vision.face.mlkit.FaceDetector
import org.opencv.android.OpenCVLoader
@Keep
class ThickFaceDetectorCreator : IFaceDetectorCreator.Stub() {
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: FaceDetectionOptions?): IFaceDetector? {
Log.d(TAG, "MLKit newFaceDetector options:${faceDetectionOptions}")
try {
val elapsedRealtime = SystemClock.elapsedRealtime()
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
Log.d(TAG, "ThickFaceDetectorCreator newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
if (!OpenCVLoader.initLocal()) {
throw RuntimeException("Unable to load OpenCV")
}
Log.d(TAG, "ThickFaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
return FaceDetector(remoteContext, faceDetectionOptions)
} catch (e: Throwable) {
Log.w(TAG, "ThickFaceDetectorCreator newFaceDetector load failed ", e)
return null
}
}
}

View file

@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.barcode
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.media.Image
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.annotation.RequiresApi
import com.google.zxing.BarcodeFormat
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 java.nio.ByteBuffer
import java.nio.IntBuffer
private const val TAG = "BarcodeDecodeHelper"
class BarcodeDecodeHelper(formats: List<BarcodeFormat>, multi: Boolean = true) {
private val reader = MultiBarcodeReader(
mapOf(
DecodeHintType.TRY_HARDER to true,
DecodeHintType.ALSO_INVERTED to true,
DecodeHintType.POSSIBLE_FORMATS to formats
)
)
fun decodeFromSource(source: LuminanceSource): List<Result> {
return try {
reader.multiDecode(BinaryBitmap(HybridBinarizer(source))).also {
if (it.isNotEmpty()) reader.reset()
}
} catch (e: NotFoundException) {
emptyList()
} catch (e: FormatException) {
emptyList()
} catch (e: ChecksumException) {
emptyList()
} catch (e: Exception) {
Log.w(TAG, "Exception with $this: $e")
emptyList()
}
}
fun decodeFromLuminanceBytes(rawBarcodeData: RawBarcodeData, rotate: Int): List<Result> {
Log.d(TAG, "decodeFromLuminanceBytes rotate:")
rawBarcodeData.rotateDetail(rotate)
return decodeFromSource(
PlanarYUVLuminanceSource(
rawBarcodeData.bytes, rawBarcodeData.width, rawBarcodeData.height,
0, 0, rawBarcodeData.width, rawBarcodeData.height, false
)
)
}
fun decodeFromLuminanceBytes(buffer: ByteBuffer, width: Int, height: Int, rotate: Int = 0): List<Result> {
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
buffer.rewind()
val rawBarcodeData = RawBarcodeData(bytes, width, height)
return decodeFromLuminanceBytes(rawBarcodeData, rotate)
}
@RequiresApi(19)
fun decodeFromImage(image: Image, rotate: Int = 0): List<Result> {
if (image.format !in SUPPORTED_IMAGE_FORMATS) return emptyList()
val rawBarcodeData =RawBarcodeData(getYUVBytesFromImage(image), image.width, image.height)
return decodeFromLuminanceBytes(rawBarcodeData, rotate)
}
private fun getYUVBytesFromImage(image: Image): ByteArray {
val planes = image.planes
val width = image.width
val height = image.height
val yuvBytes = ByteArray(width * height * 3 / 2)
var offset = 0
for (i in planes.indices) {
val buffer = planes[i].buffer
val rowStride = planes[i].rowStride
val pixelStride = planes[i].pixelStride
val planeWidth = if ((i == 0)) width else width / 2
val planeHeight = if ((i == 0)) height else height / 2
val planeBytes = ByteArray(buffer.capacity())
buffer[planeBytes]
for (row in 0 until planeHeight) {
for (col in 0 until planeWidth) {
yuvBytes[offset++] = planeBytes[row * rowStride + col * pixelStride]
}
}
}
return yuvBytes
}
fun decodeFromBitmap(bitmap: Bitmap): List<Result> {
val frameBuf: IntBuffer = IntBuffer.allocate(bitmap.byteCount)
bitmap.copyPixelsToBuffer(frameBuf)
return decodeFromSource(RGBLuminanceSource(bitmap.width, bitmap.height, frameBuf.array()))
}
companion object {
@RequiresApi(19)
val SUPPORTED_IMAGE_FORMATS =
listOfNotNull(ImageFormat.YUV_420_888, if (SDK_INT >= 23) ImageFormat.YUV_422_888 else null, if (SDK_INT >= 23) ImageFormat.YUV_444_888 else null)
}
}

View file

@ -0,0 +1,261 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.barcode
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.util.Log
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.ObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.android.gms.vision.barcode.Barcode
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetector
import com.google.android.gms.vision.internal.FrameMetadataParcel
import com.google.zxing.*
import com.google.zxing.client.result.*
import java.nio.ByteBuffer
import java.nio.IntBuffer
import java.util.*
private const val TAG = "BarcodeDetector"
class BarcodeDetector(val context: Context, val options: BarcodeDetectorOptions) : INativeBarcodeDetector.Stub() {
private val helper = BarcodeDecodeHelper(options.formats.gmsToZXingBarcodeFormats())
private var loggedOnce = false
override fun detectBitmap(wrappedBitmap: IObjectWrapper, metadata: FrameMetadataParcel): Array<Barcode> {
if (!loggedOnce) Log.d(TAG, "detectBitmap(${ObjectWrapper.unwrap(wrappedBitmap)}, $metadata)").also { loggedOnce = true }
val bitmap = wrappedBitmap.unwrap<Bitmap>() ?: return emptyArray()
return helper.decodeFromBitmap(bitmap)
.mapNotNull { runCatching { it.toGms(metadata) }.getOrNull() }.toTypedArray()
}
override fun detectBytes(wrappedByteBuffer: IObjectWrapper, metadata: FrameMetadataParcel): Array<Barcode> {
if (!loggedOnce) Log.d(TAG, "detectBytes(${ObjectWrapper.unwrap(wrappedByteBuffer)}, $metadata)").also { loggedOnce = true }
val bytes = wrappedByteBuffer.unwrap<ByteBuffer>() ?: return emptyArray()
return helper.decodeFromLuminanceBytes(bytes, metadata.width, metadata.height, metadata.rotation)
.mapNotNull { runCatching { it.toGms(metadata) }.getOrNull() }.toTypedArray()
}
override fun close() {
Log.d(TAG, "close()")
}
}
private fun Int.gmsToZXingBarcodeFormats(): List<BarcodeFormat> {
return listOfNotNull(
BarcodeFormat.AZTEC.takeIf { (this and Barcode.AZTEC) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODABAR.takeIf { (this and Barcode.CODABAR) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODE_39.takeIf { (this and Barcode.CODE_39) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODE_93.takeIf { (this and Barcode.CODE_93) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODE_128.takeIf { (this and Barcode.CODE_128) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.DATA_MATRIX.takeIf { (this and Barcode.DATA_MATRIX) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.EAN_8.takeIf { (this and Barcode.EAN_8) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.EAN_13.takeIf { (this and Barcode.EAN_13) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.ITF.takeIf { (this and Barcode.ITF) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.PDF_417.takeIf { (this and Barcode.PDF417) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.QR_CODE.takeIf { (this and Barcode.QR_CODE) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.UPC_A.takeIf { (this and Barcode.UPC_A) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.UPC_E.takeIf { (this and Barcode.UPC_E) > 0 || this == Barcode.ALL_FORMATS },
)
}
private fun BarcodeFormat.toGms(): Int = when (this) {
BarcodeFormat.AZTEC -> Barcode.AZTEC
BarcodeFormat.CODABAR -> Barcode.CODABAR
BarcodeFormat.CODE_39 -> Barcode.CODE_39
BarcodeFormat.CODE_93 -> Barcode.CODE_93
BarcodeFormat.CODE_128 -> Barcode.CODE_128
BarcodeFormat.DATA_MATRIX -> Barcode.DATA_MATRIX
BarcodeFormat.EAN_13 -> Barcode.EAN_13
BarcodeFormat.EAN_8 -> Barcode.EAN_8
BarcodeFormat.ITF -> Barcode.ITF
BarcodeFormat.PDF_417 -> Barcode.PDF417
BarcodeFormat.QR_CODE -> Barcode.QR_CODE
BarcodeFormat.UPC_A -> Barcode.UPC_A
BarcodeFormat.UPC_E -> Barcode.UPC_E
else -> throw UnsupportedOperationException()
}
private fun ParsedResultType.toGms(): Int = when (this) {
ParsedResultType.ADDRESSBOOK -> Barcode.CONTACT_INFO
ParsedResultType.CALENDAR -> Barcode.CALENDAR_EVENT
ParsedResultType.EMAIL_ADDRESS -> Barcode.EMAIL
ParsedResultType.GEO -> Barcode.GEO
ParsedResultType.ISBN -> Barcode.ISBN
ParsedResultType.PRODUCT -> Barcode.PRODUCT
ParsedResultType.SMS -> Barcode.SMS
ParsedResultType.TEL -> Barcode.PHONE
ParsedResultType.TEXT -> Barcode.TEXT
ParsedResultType.URI -> Barcode.URL
ParsedResultType.WIFI -> Barcode.WIFI
else -> Barcode.TEXT
}
private fun AddressBookParsedResult.toGms(): Barcode.ContactInfo {
val contactInfo = Barcode.ContactInfo()
// TODO: contactInfo.name
contactInfo.organization = org
contactInfo.title = title
contactInfo.phones = phoneNumbers.orEmpty().mapIndexed { i, a ->
Barcode.Phone().apply {
type = when (phoneTypes?.getOrNull(i)) {
"WORK" -> Barcode.Phone.WORK
"HOME" -> Barcode.Phone.HOME
"FAX" -> Barcode.Phone.FAX
"MOBILE" -> Barcode.Phone.MOBILE
else -> Barcode.Phone.UNKNOWN
}
number = a
}
}.toTypedArray()
contactInfo.emails = emails.orEmpty().mapIndexed { i, a ->
Barcode.Email().apply {
type = when (emailTypes?.getOrNull(i)) {
"WORK" -> Barcode.Email.WORK
"HOME" -> Barcode.Email.HOME
else -> Barcode.Email.UNKNOWN
}
address = a
}
}.toTypedArray()
contactInfo.urls = urLs
contactInfo.addresses = addresses.orEmpty().mapIndexed { i, a ->
Barcode.Address().apply {
type = when (addressTypes?.getOrNull(i)) {
"WORK" -> Barcode.Address.WORK
"HOME" -> Barcode.Address.HOME
else -> Barcode.Address.UNKNOWN
}
addressLines = a.split("\n").toTypedArray()
}
}.toTypedArray()
return contactInfo
}
private fun CalendarParsedResult.toGms(): Barcode.CalendarEvent {
fun createDateTime(timestamp: Long, isAllDay: Boolean) = Barcode.CalendarDateTime().apply {
val calendar = Calendar.getInstance()
calendar.time = Date(timestamp)
year = calendar.get(Calendar.YEAR)
month = calendar.get(Calendar.MONTH)
day = calendar.get(Calendar.DAY_OF_MONTH)
if (isAllDay) {
hours = -1
minutes = -1
seconds = -1
} else {
hours = calendar.get(Calendar.HOUR_OF_DAY)
minutes = calendar.get(Calendar.MINUTE)
seconds = calendar.get(Calendar.SECOND)
}
}
val event = Barcode.CalendarEvent()
event.summary = summary
event.description = description
event.location = location
event.organizer = organizer
event.start = createDateTime(startTimestamp, isStartAllDay)
event.end = createDateTime(endTimestamp, isEndAllDay)
return event
}
private fun EmailAddressParsedResult.toGms(): Barcode.Email {
val email = Barcode.Email()
email.address = tos?.getOrNull(0)
email.subject = subject
email.body = body
return email
}
private fun GeoParsedResult.toGms(): Barcode.GeoPoint {
val geo = Barcode.GeoPoint()
geo.lat = latitude
geo.lng = longitude
return geo
}
private fun TelParsedResult.toGms(): Barcode.Phone {
val phone = Barcode.Phone()
phone.number = number
return phone
}
private fun SMSParsedResult.toGms(): Barcode.Sms {
val sms = Barcode.Sms()
sms.message = body
sms.phoneNumber = numbers?.getOrNull(0)
return sms
}
private fun WifiParsedResult.toGms(): Barcode.WiFi {
val wifi = Barcode.WiFi()
wifi.ssid = ssid
wifi.password = password
wifi.encryptionType = when (networkEncryption) {
"OPEN" -> Barcode.WiFi.OPEN
"WEP" -> Barcode.WiFi.WEP
"WPA" -> Barcode.WiFi.WPA
"WPA2" -> Barcode.WiFi.WPA
else -> 0
}
return wifi
}
private fun URIParsedResult.toGms(): Barcode.UrlBookmark {
val url = Barcode.UrlBookmark()
url.url = uri
url.title = title
return url
}
private fun Result.toGms(metadata: FrameMetadataParcel): Barcode {
val barcode = Barcode()
barcode.format = barcodeFormat.toGms()
barcode.rawBytes = rawBytes
barcode.rawValue = text
barcode.cornerPoints = resultPoints.map {
Point(it.x.toInt(), it.y.toInt())
}.toTypedArray()
val parsed = ResultParser.parseResult(this)
barcode.displayValue = parsed.displayResult
barcode.valueFormat = parsed.type.toGms()
when (parsed) {
is EmailAddressParsedResult ->
barcode.email = parsed.toGms()
is TelParsedResult ->
barcode.phone = parsed.toGms()
is SMSParsedResult ->
barcode.sms = parsed.toGms()
is WifiParsedResult ->
barcode.wifi = parsed.toGms()
is URIParsedResult ->
barcode.url = parsed.toGms()
is GeoParsedResult ->
barcode.geoPoint = parsed.toGms()
is CalendarParsedResult ->
barcode.calendarEvent = parsed.toGms()
is AddressBookParsedResult ->
barcode.contactInfo = parsed.toGms()
}
return barcode
}

View file

@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.barcode
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.media.Image
import android.os.Build.VERSION.SDK_INT
import android.os.Parcel
import android.util.Log
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.ObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.mlkit.vision.barcode.aidls.IBarcodeScanner
import com.google.mlkit.vision.barcode.internal.*
import com.google.zxing.BarcodeFormat
import org.microg.gms.utils.warnOnTransactionIssues
import java.nio.ByteBuffer
private const val TAG = "BarcodeScanner"
class BarcodeScanner(val context: Context, val options: BarcodeScannerOptions) : IBarcodeScanner.Stub() {
private val helper =
BarcodeDecodeHelper(if (options.allPotentialBarcodesEnabled) BarcodeFormat.values().toList() else options.supportedFormats.mlKitToZXingBarcodeFormats())
private var loggedOnce = false
override fun init() {
Log.d(TAG, "init()")
}
override fun close() {
Log.d(TAG, "close()")
}
override fun detect(wrappedImage: IObjectWrapper, metadata: ImageMetadata): List<Barcode> {
if (!loggedOnce) Log.d(TAG, "detect(${ObjectWrapper.unwrap(wrappedImage)}, $metadata)").also { loggedOnce = true }
return when (metadata.format) {
-1 -> wrappedImage.unwrap<Bitmap>()?.let { helper.decodeFromBitmap(it) }
ImageFormat.NV21 -> wrappedImage.unwrap<ByteBuffer>()?.let { helper.decodeFromLuminanceBytes(it, metadata.width, metadata.height, metadata.rotation) }
ImageFormat.YUV_420_888 -> if (SDK_INT >= 19) wrappedImage.unwrap<Image>()?.let { image -> helper.decodeFromImage(image, metadata.rotation) } else null
else -> null
}?.map { it.toMlKit(metadata) } ?: emptyList()
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) }
}

View file

@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2023 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.barcode
import com.google.zxing.*
import com.google.zxing.multi.MultipleBarcodeReader
import kotlin.math.max
import kotlin.math.min
class MultiBarcodeReader(val hints: Map<DecodeHintType, *>) : MultipleBarcodeReader, Reader {
val delegate = MultiFormatReader().apply { setHints(hints) }
fun multiDecode(image: BinaryBitmap): List<Result> {
return doDecodeMultiple(image)
}
override fun decodeMultiple(image: BinaryBitmap): Array<Result> {
return multiDecode(image).toTypedArray()
}
override fun decodeMultiple(image: BinaryBitmap, hints: MutableMap<DecodeHintType, *>?): Array<Result> {
return multiDecode(image).toTypedArray()
}
override fun decode(image: BinaryBitmap): Result {
return delegate.decodeWithState(image)
}
override fun decode(image: BinaryBitmap, hints: MutableMap<DecodeHintType, *>?): Result {
return delegate.decodeWithState(image)
}
override fun reset() {
delegate.reset()
}
// Derived from com.google.zxing.multi GenericMultipleBarcodeReader
// Copyright 2009 ZXing authors
// Licensed under the Apache License, Version 2.0
private fun doDecodeMultiple(
image: BinaryBitmap,
results: MutableList<Result> = arrayListOf(),
xOffset: Int = 0,
yOffset: Int = 0,
currentDepth: Int = 0,
maxDepth: Int = 2
): List<Result> {
val result = kotlin.runCatching { delegate.decodeWithState(image) }.getOrNull() ?: return results
if (results.none { it.text == result.text }) {
results.add(translateResultPoints(result, xOffset, yOffset))
}
val resultPoints = result.resultPoints
if (resultPoints != null && resultPoints.isNotEmpty() && currentDepth + 1 < maxDepth) {
val width = image.width
val height = image.height
var minX = width.toFloat()
var minY = height.toFloat()
var maxX = 0.0f
var maxY = 0.0f
for (point in resultPoints) {
if (point != null) {
minX = min(point.x, minX)
minY = min(point.y, minY)
maxX = max(point.x, maxX)
maxY = max(point.y, maxY)
}
}
if (minX > 100.0f) {
this.doDecodeMultiple(image.crop(0, 0, minX.toInt(), height), results, xOffset, yOffset, currentDepth + 1, maxDepth)
}
if (minY > 100.0f) {
this.doDecodeMultiple(image.crop(0, 0, width, minY.toInt()), results, xOffset, yOffset, currentDepth + 1, maxDepth)
}
if (maxX < (width - 100).toFloat()) {
this.doDecodeMultiple(image.crop(maxX.toInt(), 0, width - maxX.toInt(), height), results, xOffset + maxX.toInt(), yOffset, currentDepth + 1, maxDepth)
}
if (maxY < (height - 100).toFloat()) {
this.doDecodeMultiple(image.crop(0, maxY.toInt(), width, height - maxY.toInt()), results, xOffset, yOffset + maxY.toInt(), currentDepth + 1, maxDepth)
}
}
return results
}
private fun translateResultPoints(result: Result, xOffset: Int, yOffset: Int): Result {
val oldResultPoints = result.resultPoints
if (oldResultPoints == null) {
return result
} else {
val newResultPoints = arrayOfNulls<ResultPoint>(oldResultPoints.size)
for (i in oldResultPoints.indices) {
val oldPoint = oldResultPoints[i]
if (oldPoint != null) {
newResultPoints[i] = ResultPoint(oldPoint.x + xOffset.toFloat(), oldPoint.y + yOffset.toFloat())
}
}
val newResult = Result(result.text, result.rawBytes, result.numBits, newResultPoints, result.barcodeFormat, result.timestamp)
newResult.putAllMetadata(result.resultMetadata)
return newResult
}
}
}

View file

@ -0,0 +1,150 @@
/**
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.barcode
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.RequiresApi
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import com.google.mlkit.vision.barcode.internal.Barcode
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.MultiFormatReader
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.common.HybridBinarizer
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@RequiresApi(21)
class QRCodeScannerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private var cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private var onQRCodeScanned: ((Barcode?) -> Unit)? = null
private val previewView: PreviewView = PreviewView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
init {
addView(previewView)
addView(ScanOverlayView(context))
}
fun startScanner(onScanned: (Barcode?) -> Unit) {
this.onQRCodeScanned = onScanned
startCamera()
}
private fun startCamera() {
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) }
val imageAnalyzer = ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build().also {
it.setAnalyzer(cameraExecutor, QRCodeAnalyzer { result ->
post { onQRCodeScanned?.invoke(result) }
})
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(context as androidx.lifecycle.LifecycleOwner, cameraSelector, preview, imageAnalyzer)
} catch (e: Exception) {
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(context))
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
cameraExecutor.shutdown()
}
}
private class ScanOverlayView(context: Context) : View(context) {
private val cornerLength = 160f
private val cornerThickness = 10f
private val paint = Paint().apply {
isAntiAlias = true
strokeWidth = cornerThickness
style = Paint.Style.STROKE
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val frameSize = width.coerceAtMost(height) * 0.6f
val left = (width - frameSize) / 2f
val top = (height - frameSize) / 2f
val right = left + frameSize
val bottom = top + frameSize
val frame = RectF(left, top, right, bottom)
val colors = listOf(0xFF4285F4.toInt(), 0xFFEA4335.toInt(), 0xFFFBBC05.toInt(), 0xFF34A853.toInt())
paint.color = colors[0]
canvas.drawLine(frame.left, frame.top, frame.left + cornerLength, frame.top, paint)
canvas.drawLine(frame.left, frame.top, frame.left, frame.top + cornerLength, paint)
paint.color = colors[1]
canvas.drawLine(frame.right, frame.top, frame.right - cornerLength, frame.top, paint)
canvas.drawLine(frame.right, frame.top, frame.right, frame.top + cornerLength, paint)
paint.color = colors[2]
canvas.drawLine(frame.left, frame.bottom, frame.left + cornerLength, frame.bottom, paint)
canvas.drawLine(frame.left, frame.bottom, frame.left, frame.bottom - cornerLength, paint)
paint.color = colors[3]
canvas.drawLine(frame.right, frame.bottom, frame.right - cornerLength, frame.bottom, paint)
canvas.drawLine(frame.right, frame.bottom, frame.right, frame.bottom - cornerLength, paint)
}
}
@RequiresApi(21)
private class QRCodeAnalyzer(private val onQRCodeScanned: (Barcode?) -> Unit) : ImageAnalysis.Analyzer {
private val reader = MultiFormatReader().apply {
setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE)))
}
override fun analyze(image: ImageProxy) {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
val source = PlanarYUVLuminanceSource(bytes, image.width, image.height, 0, 0, image.width, image.height, false)
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
try {
val result = reader.decode(binaryBitmap)
onQRCodeScanned(result.toMlKit())
} catch (e: NotFoundException) {
onQRCodeScanned(null)
} finally {
image.close()
}
}
}

View file

@ -0,0 +1,95 @@
package org.microg.gms.vision.barcode
import android.util.Log
import android.view.Surface
class RawBarcodeData(var bytes: ByteArray, var width: Int, var height: Int) {
fun rotateDetail(rotate: Int){
when (rotate) {
Surface.ROTATION_90 -> rotateDegree90()
Surface.ROTATION_180 -> rotateDegree180()
Surface.ROTATION_270 -> rotateDegree270()
else -> this
}
}
private fun rotateDegree90(){
val rotatedData = ByteArray(bytes.size)
var index = 0
// Rotate Y plane
for (col in 0 until width) {
for (row in height - 1 downTo 0) {
rotatedData[index++] = bytes[row * width + col]
}
}
// Rotate UV planes (UV interleaved)
val uvHeight = height / 2
for (col in 0 until width step 2) {
for (row in uvHeight - 1 downTo 0) {
rotatedData[index++] = bytes[width * height + row * width + col]
rotatedData[index++] = bytes[width * height + row * width + col + 1]
}
}
bytes = rotatedData
val temp = width
width = height
height = temp
}
private fun rotateDegree180() {
val rotatedData = ByteArray(bytes.size)
var index = 0
// Rotate Y plane
for (row in height - 1 downTo 0) {
for (col in width - 1 downTo 0) {
rotatedData[index++] = bytes[row * width + col]
}
}
// Rotate UV planes (UV interleaved)
val uvHeight = height / 2
val uvWidth = width / 2
for (row in uvHeight - 1 downTo 0) {
for (col in uvWidth - 1 downTo 0) {
val offset = width * height + row * width + col * 2
rotatedData[index++] = bytes[offset]
rotatedData[index++] = bytes[offset + 1]
}
}
bytes = rotatedData
}
private fun rotateDegree270(){
val rotatedData = ByteArray(bytes.size)
var index = 0
// Rotate Y plane
for (col in width - 1 downTo 0) {
for (row in 0 until height) {
rotatedData[index++] = bytes[row * width + col]
}
}
// Rotate UV planes (UV interleaved)
val uvHeight = height / 2
for (col in width - 1 downTo 0 step 2) {
for (row in 0 until uvHeight) {
rotatedData[index++] = bytes[width * height + row * width + col - 1]
rotatedData[index++] = bytes[width * height + row * width + col]
}
}
bytes = rotatedData
val temp = width
width = height
height = temp
}
override fun toString(): String {
return "RawBarcodeData(bytes=${bytes.size}, width=$width, height=$height)"
}
}

View file

@ -0,0 +1,238 @@
/**
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.barcode
import android.graphics.Point
import com.google.mlkit.vision.barcode.internal.Address
import com.google.mlkit.vision.barcode.internal.Barcode
import com.google.mlkit.vision.barcode.internal.CalendarDateTime
import com.google.mlkit.vision.barcode.internal.CalendarEvent
import com.google.mlkit.vision.barcode.internal.ContactInfo
import com.google.mlkit.vision.barcode.internal.Email
import com.google.mlkit.vision.barcode.internal.GeoPoint
import com.google.mlkit.vision.barcode.internal.ImageMetadata
import com.google.mlkit.vision.barcode.internal.Phone
import com.google.mlkit.vision.barcode.internal.Sms
import com.google.mlkit.vision.barcode.internal.UrlBookmark
import com.google.mlkit.vision.barcode.internal.WiFi
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result
import com.google.zxing.client.result.AddressBookParsedResult
import com.google.zxing.client.result.CalendarParsedResult
import com.google.zxing.client.result.EmailAddressParsedResult
import com.google.zxing.client.result.GeoParsedResult
import com.google.zxing.client.result.ParsedResultType
import com.google.zxing.client.result.ResultParser
import com.google.zxing.client.result.SMSParsedResult
import com.google.zxing.client.result.TelParsedResult
import com.google.zxing.client.result.URIParsedResult
import com.google.zxing.client.result.WifiParsedResult
import java.util.Calendar
import java.util.Date
import kotlin.collections.mapIndexed
import kotlin.collections.orEmpty
import kotlin.text.split
fun Int.mlKitToZXingBarcodeFormats(): List<BarcodeFormat> {
return listOfNotNull(
BarcodeFormat.AZTEC.takeIf { (this and Barcode.AZTEC) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODABAR.takeIf { (this and Barcode.CODABAR) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODE_39.takeIf { (this and Barcode.CODE_39) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODE_93.takeIf { (this and Barcode.CODE_93) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.CODE_128.takeIf { (this and Barcode.CODE_128) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.DATA_MATRIX.takeIf { (this and Barcode.DATA_MATRIX) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.EAN_8.takeIf { (this and Barcode.EAN_8) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.EAN_13.takeIf { (this and Barcode.EAN_13) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.ITF.takeIf { (this and Barcode.ITF) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.PDF_417.takeIf { (this and Barcode.PDF417) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.QR_CODE.takeIf { (this and Barcode.QR_CODE) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.UPC_A.takeIf { (this and Barcode.UPC_A) > 0 || this == Barcode.ALL_FORMATS },
BarcodeFormat.UPC_E.takeIf { (this and Barcode.UPC_E) > 0 || this == Barcode.ALL_FORMATS },
)
}
fun BarcodeFormat.toMlKit(): Int = when (this) {
BarcodeFormat.AZTEC -> Barcode.AZTEC
BarcodeFormat.CODABAR -> Barcode.CODABAR
BarcodeFormat.CODE_39 -> Barcode.CODE_39
BarcodeFormat.CODE_93 -> Barcode.CODE_93
BarcodeFormat.CODE_128 -> Barcode.CODE_128
BarcodeFormat.DATA_MATRIX -> Barcode.DATA_MATRIX
BarcodeFormat.EAN_8 -> Barcode.EAN_8
BarcodeFormat.EAN_13 -> Barcode.EAN_13
BarcodeFormat.ITF -> Barcode.ITF
BarcodeFormat.PDF_417 -> Barcode.PDF417
BarcodeFormat.QR_CODE -> Barcode.QR_CODE
BarcodeFormat.UPC_A -> Barcode.UPC_A
BarcodeFormat.UPC_E -> Barcode.UPC_E
else -> Barcode.UNKNOWN_FORMAT
}
fun ParsedResultType.toMlKit(): Int = when (this) {
ParsedResultType.ADDRESSBOOK -> Barcode.CONTACT_INFO
ParsedResultType.CALENDAR -> Barcode.CALENDAR_EVENT
ParsedResultType.EMAIL_ADDRESS -> Barcode.EMAIL
ParsedResultType.GEO -> Barcode.GEO
ParsedResultType.ISBN -> Barcode.ISBN
ParsedResultType.PRODUCT -> Barcode.PRODUCT
ParsedResultType.SMS -> Barcode.SMS
ParsedResultType.TEL -> Barcode.PHONE
ParsedResultType.TEXT -> Barcode.TEXT
ParsedResultType.URI -> Barcode.URL
ParsedResultType.WIFI -> Barcode.WIFI
else -> Barcode.UNKNOWN_TYPE
}
fun AddressBookParsedResult.toMlKit(): ContactInfo {
val contactInfo = ContactInfo()
// TODO: contactInfo.name
contactInfo.organization = org
contactInfo.title = title
contactInfo.phones = phoneNumbers.orEmpty().mapIndexed { i, a ->
Phone().apply {
type = when (phoneTypes?.getOrNull(i)) {
"WORK" -> Phone.WORK
"HOME" -> Phone.HOME
"FAX" -> Phone.FAX
"MOBILE" -> Phone.MOBILE
else -> Phone.UNKNOWN
}
number = a
}
}.toTypedArray()
contactInfo.emails = emails.orEmpty().mapIndexed { i, a ->
Email().apply {
type = when (emailTypes?.getOrNull(i)) {
"WORK" -> Email.WORK
"HOME" -> Email.HOME
else -> Email.UNKNOWN
}
address = a
}
}.toTypedArray()
contactInfo.urls = urLs
contactInfo.addresses = addresses.orEmpty().mapIndexed { i, a ->
Address().apply {
type = when (addressTypes?.getOrNull(i)) {
"WORK" -> Address.WORK
"HOME" -> Address.HOME
else -> Address.UNKNOWN
}
addressLines = a.split("\n").toTypedArray()
}
}.toTypedArray()
return contactInfo
}
fun CalendarParsedResult.toMlKit(): CalendarEvent {
fun createDateTime(timestamp: Long, isAllDay: Boolean) = CalendarDateTime().apply {
val calendar = Calendar.getInstance()
calendar.time = Date(timestamp)
year = calendar.get(Calendar.YEAR)
month = calendar.get(Calendar.MONTH)
day = calendar.get(Calendar.DAY_OF_MONTH)
if (isAllDay) {
hours = -1
minutes = -1
seconds = -1
} else {
hours = calendar.get(Calendar.HOUR_OF_DAY)
minutes = calendar.get(Calendar.MINUTE)
seconds = calendar.get(Calendar.SECOND)
}
}
val event = CalendarEvent()
event.summary = summary
event.description = description
event.location = location
event.organizer = organizer
event.start = createDateTime(startTimestamp, isStartAllDay)
event.end = createDateTime(endTimestamp, isEndAllDay)
return event
}
fun EmailAddressParsedResult.toMlKit(): Email {
val email = Email()
email.address = tos?.getOrNull(0)
email.subject = subject
email.body = body
return email
}
fun GeoParsedResult.toMlKit(): GeoPoint {
val geo = GeoPoint()
geo.lat = latitude
geo.lng = longitude
return geo
}
fun TelParsedResult.toMlKit(): Phone {
val phone = Phone()
phone.number = number
return phone
}
fun SMSParsedResult.toMlKit(): Sms {
val sms = Sms()
sms.message = body
sms.phoneNumber = numbers?.getOrNull(0)
return sms
}
fun WifiParsedResult.toMlKit(): WiFi {
val wifi = WiFi()
wifi.ssid = ssid
wifi.password = password
wifi.encryptionType = when (networkEncryption) {
"OPEN" -> WiFi.OPEN
"WEP" -> WiFi.WEP
"WPA" -> WiFi.WPA
"WPA2" -> WiFi.WPA
else -> 0
}
return wifi
}
fun URIParsedResult.toMlKit(): UrlBookmark {
val url = UrlBookmark()
url.url = uri
url.title = title
return url
}
fun Result.toMlKit(metadata: ImageMetadata? = null): Barcode {
val barcode = Barcode()
barcode.format = barcodeFormat.toMlKit()
barcode.rawBytes = rawBytes
barcode.rawValue = text
barcode.cornerPoints = resultPoints.map {
when (metadata?.rotation ?: -1) {
1 -> Point(metadata!!.height - it.y.toInt(), it.x.toInt())
2 -> Point(metadata!!.width - it.x.toInt(), metadata.height - it.y.toInt())
3 -> Point(it.y.toInt(), metadata!!.width - it.x.toInt())
else -> Point(it.x.toInt(), it.y.toInt())
}
}.toTypedArray()
val parsed = ResultParser.parseResult(this)
barcode.displayValue = parsed.displayResult
barcode.valueType = parsed.type.toMlKit()
when (parsed) {
is EmailAddressParsedResult -> barcode.email = parsed.toMlKit()
is TelParsedResult -> barcode.phone = parsed.toMlKit()
is SMSParsedResult -> barcode.sms = parsed.toMlKit()
is WifiParsedResult -> barcode.wifi = parsed.toMlKit()
is URIParsedResult -> barcode.urlBookmark = parsed.toMlKit()
is GeoParsedResult -> barcode.geoPoint = parsed.toMlKit()
is CalendarParsedResult -> barcode.calendarEvent = parsed.toMlKit()
is AddressBookParsedResult -> barcode.contactInfo = parsed.toMlKit()
}
return barcode
}

View file

@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2025, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.face
import android.content.Context
import android.util.Log
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.android.gms.vision.face.Contour
import com.google.android.gms.vision.face.Landmark
import com.google.android.gms.vision.face.internal.client.DetectionOptions
import com.google.android.gms.vision.face.internal.client.FaceParcel
import com.google.android.gms.vision.face.internal.client.INativeFaceDetector
import com.google.android.gms.vision.internal.FrameMetadataParcel
import com.google.mlkit.vision.face.Face
import java.nio.ByteBuffer
class FaceDetector(val context: Context, private val options: DetectionOptions?) : INativeFaceDetector.Stub() {
private val mFaceDetector by lazy { FaceDetectorHelper(context) }
override fun closeDetectorJni() {
Log.d(TAG, "closeDetectorJni")
mFaceDetector.release()
}
override fun isNativeFaceDetectorAvailable(i: Int): Boolean {
Log.d(TAG, "isNativeFaceDetectorAvailable type:${i}")
return true
}
override fun detectFacesFromPlanes(
planeFirst: IObjectWrapper?,
planeSencond: IObjectWrapper?,
planeThird: IObjectWrapper?,
firstPixelStride: Int,
secondPixelStride: Int,
thirdPixelStride: Int,
firstRowStride: Int,
secondRowStride: Int,
thirdRowStride: Int,
metadataParcel: FrameMetadataParcel?
): Array<FaceParcel> {
Log.d(
TAG,
"detectFacesFromPlanes planeFirst:${planeFirst} ,planeSecond:${planeSencond} ,planeThird:${planeThird}," + "firstPixelStride:${firstPixelStride} ,secondPixelStride:${secondPixelStride} ,thirdPixelStride:${thirdPixelStride} ," + "firstRowStride:${firstRowStride} ,secondRowStride:${secondRowStride} ,thirdRowStride:${thirdRowStride}," + "metadataParcel:${metadataParcel}"
)
val yBuffer = planeFirst?.unwrap<ByteBuffer>() ?: return emptyArray()
val uBuffer = planeSencond?.unwrap<ByteBuffer>() ?: return emptyArray()
val vBuffer = planeThird?.unwrap<ByteBuffer>() ?: return emptyArray()
val width = metadataParcel?.width ?: return emptyArray()
val height = metadataParcel?.height ?: return emptyArray()
val rotation = metadataParcel.rotation
val nv21 = ByteArray(width * height * 3 / 2)
var offset = 0
for (row in 0 until height) {
yBuffer.position(row * firstRowStride)
yBuffer.get(nv21, offset, width)
offset += width
}
val chromaWidth = width / 2
val chromaHeight = height / 2
for (row in 0 until chromaHeight) {
for (col in 0 until chromaWidth) {
val uIndex = row * secondRowStride + col * secondPixelStride
val vIndex = row * thirdRowStride + col * thirdPixelStride
nv21[offset++] = vBuffer.get(vIndex)
nv21[offset++] = uBuffer.get(uIndex)
}
}
return mFaceDetector.detectFaces(nv21, width, height, rotation).map {
it.toFaceParcel()
}.toTypedArray().also {
it.forEach { Log.d(TAG, "detectFacesFromPlanes: $it") }
}
}
override fun detectFaceParcels(wrapper: IObjectWrapper?, metadata: FrameMetadataParcel?): Array<FaceParcel> {
Log.d(TAG, "detectFaceParcels byteBuffer:${wrapper} ,metadataParcel:${metadata}")
if (wrapper == null || metadata == null) return emptyArray()
val buffer = wrapper.unwrap<ByteBuffer>() ?: return emptyArray()
return mFaceDetector.detectFaces(buffer.array(), metadata.width, metadata.height, metadata.rotation).map {
it.toFaceParcel()
}.toTypedArray().also {
it.forEach { Log.d(TAG, "detectFaceParcels: $it") }
}
}
}
private fun com.google.mlkit.vision.face.aidls.FaceParcel.toFaceParcel() = FaceParcel(
1,
id,
(boundingBox.left + boundingBox.width() / 2).toFloat(),
(boundingBox.top + boundingBox.height() / 2).toFloat(),
boundingBox.width().toFloat(),
boundingBox.height().toFloat(),
panAngle,
rollAngle,
tiltAngle,
landmarkParcelList.map { landmark -> Landmark(landmark.type, landmark.position.x, landmark.position.y, landmark.type) }.toTypedArray(),
leftEyeOpenProbability,
rightEyeOpenProbability,
smileProbability,
contourParcelList.map { contour -> Contour(contour.type, contour.pointsList) }.toTypedArray(),
confidenceScore
)

View file

@ -0,0 +1,278 @@
/**
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.face
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import android.util.Log
import com.google.mlkit.vision.face.FaceContour
import com.google.mlkit.vision.face.FaceLandmark
import com.google.mlkit.vision.face.aidls.ContourParcel
import com.google.mlkit.vision.face.aidls.FaceParcel
import com.google.mlkit.vision.face.aidls.LandmarkParcel
import org.opencv.android.Utils
import org.opencv.core.Core
import org.opencv.core.CvType
import org.opencv.core.Mat
import org.opencv.core.MatOfByte
import org.opencv.core.Size
import org.opencv.imgproc.Imgproc
import org.opencv.objdetect.FaceDetectorYN
import java.io.ByteArrayOutputStream
import kotlin.math.hypot
const val TAG = "FaceDetection"
class FaceDetectorHelper(context: Context) {
private var faceDetectorYN: FaceDetectorYN? = null
private var inputSize = Size(320.0, 320.0)
init {
try {
val buffer: ByteArray
context.assets.open("face_detection_yunet_2023mar.onnx").use {
val size = it.available()
buffer = ByteArray(size)
it.read(buffer)
}
faceDetectorYN = FaceDetectorYN.create("onnx", MatOfByte(*buffer), MatOfByte(), inputSize, 0.7f, 0.3f, 5000)
} catch (e: Exception) {
throw RuntimeException("faceDetectorYN initialization failed")
}
}
fun detectFaces(bitmap: Bitmap, rotation: Int): List<FaceParcel> {
Log.d(TAG, "detectFaces: source is bitmap")
val rootMat = bitmapToMat(bitmap) ?: return emptyList()
return processMat(rootMat, rotation)
}
fun detectFaces(nv21ByteArray: ByteArray, width: Int, height: Int, rotation: Int): List<FaceParcel> {
Log.d(TAG, "detectFaces: source is nv21Buffer")
val rootMat = nv21ToMat(nv21ByteArray, width, height) ?: return emptyList()
return processMat(rootMat, rotation)
}
fun detectFaces(image: Image, rotation: Int): List<FaceParcel> {
Log.d(TAG, "detectFaces: source is image")
val rootMat = imageToMat(image) ?: return emptyList()
return processMat(rootMat, rotation)
}
fun release() {
try {
faceDetectorYN = null
} catch (e: Exception) {
Log.d(TAG, "release failed", e)
}
}
private fun processMat(mat: Mat, rotation: Int): List<FaceParcel> {
val faceDetector = faceDetectorYN ?: return emptyList()
val facesMat = Mat()
val degree = degree(rotation)
Log.d(TAG, "processMat: degree: $degree")
when (degree) {
2 -> Core.rotate(mat, facesMat, Core.ROTATE_90_COUNTERCLOCKWISE)
3 -> Core.rotate(mat, facesMat, Core.ROTATE_180)
4 -> Core.rotate(mat, facesMat, Core.ROTATE_90_CLOCKWISE)
else -> mat.copyTo(facesMat)
}
val matSize = Size(facesMat.cols().toDouble(), facesMat.rows().toDouble())
Log.d(TAG, "processMat: inputSize: $inputSize")
if (inputSize != matSize) {
inputSize = matSize
faceDetector.inputSize = matSize
}
Log.d(TAG, "processMat: matSize: $matSize")
val result = Mat()
val status = faceDetectorYN!!.detect(facesMat, result)
Log.d(TAG, "processMat: detect: $status facesMat: ${result.size()}")
return parseDetections(result)
}
/**
* faces: detection results stored in a 2D cv::Mat of shape [num_faces, 15]
* 0-1: x, y of bbox top left corner
* 2-3: width, height of bbox
* 4-5: x, y of right eye (blue point in the example image)
* 6-7: x, y of left eye (red point in the example image)
* 8-9: x, y of nose tip (green point in the example image)
* 10-11: x, y of right corner of mouth (pink point in the example image)
* 12-13: x, y of left corner of mouth (yellow point in the example image)
* 14: face score
*/
private fun parseDetections(detections: Mat): List<FaceParcel> {
val faces = mutableListOf<FaceParcel>()
val faceData = FloatArray(detections.cols() * detections.channels())
for (i in 0 until detections.rows()) {
detections.get(i, 0, faceData)
val confidence = faceData[14]
val boundingBox = Rect(faceData[0].toInt(), faceData[1].toInt(), (faceData[0] + faceData[2]).toInt(), (faceData[1] + faceData[3]).toInt())
val leftEyeMark = LandmarkParcel(FaceLandmark.LEFT_EYE, PointF(faceData[4], faceData[5]))
val mouthLeftMark = LandmarkParcel(FaceLandmark.MOUTH_LEFT, PointF(faceData[10], faceData[11]))
val noseBaseMark = LandmarkParcel(FaceLandmark.NOSE_BASE, PointF(faceData[8], faceData[9]))
val rightEyeMark = LandmarkParcel(FaceLandmark.RIGHT_EYE, PointF(faceData[6], faceData[7]))
val mouthRightMark = LandmarkParcel(FaceLandmark.MOUTH_RIGHT, PointF(faceData[12], faceData[13]))
// These are calculated for better compatibility, the model doesn't actually provide proper values here
val mouthBottomMark = LandmarkParcel(FaceLandmark.MOUTH_BOTTOM, calculateMidPoint(mouthLeftMark, mouthRightMark))
val leftCheekMark = LandmarkParcel(FaceLandmark.LEFT_CHEEK, calculateMidPoint(leftEyeMark, mouthLeftMark))
val leftEarMark = LandmarkParcel(FaceLandmark.LEFT_EAR, PointF(boundingBox.right.toFloat(), noseBaseMark.position.y))
val rightCheekMark = LandmarkParcel(FaceLandmark.RIGHT_CHEEK, calculateMidPoint(rightEyeMark, mouthRightMark))
val rightEarMark = LandmarkParcel(FaceLandmark.RIGHT_EAR, PointF(boundingBox.left.toFloat(), noseBaseMark.position.y))
val smilingProbability = calculateSmilingProbability(mouthLeftMark, mouthRightMark)
val leftEyeOpenProbability = calculateEyeOpenProbability(rightEyeMark, mouthRightMark)
val rightEyeOpenProbability = calculateEyeOpenProbability(leftEyeMark, mouthLeftMark)
val faceContour = ContourParcel(FaceContour.FACE, arrayListOf(
PointF(boundingBox.left.toFloat(), boundingBox.top.toFloat()),
PointF(boundingBox.left.toFloat(), boundingBox.bottom.toFloat()),
PointF(boundingBox.right.toFloat(), boundingBox.bottom.toFloat()),
PointF(boundingBox.right.toFloat(), boundingBox.top.toFloat()),
))
val leftEyebrowTopContour = ContourParcel(FaceContour.LEFT_EYEBROW_TOP, arrayListOf(leftEyeMark.position))
val leftEyebrowBottomContour = ContourParcel(FaceContour.LEFT_EYEBROW_BOTTOM, arrayListOf(leftEyeMark.position))
val rightEyebrowTopContour = ContourParcel(FaceContour.RIGHT_EYEBROW_TOP, arrayListOf(rightEyeMark.position))
val rightEyebrowBottomContour = ContourParcel(FaceContour.RIGHT_EYEBROW_BOTTOM, arrayListOf(rightEyeMark.position))
val leftEyeContour = ContourParcel(FaceContour.LEFT_EYE, arrayListOf(leftEyeMark.position))
val rightEyeContour = ContourParcel(FaceContour.RIGHT_EYE, arrayListOf(rightEyeMark.position))
val upperLipTopContour = ContourParcel(FaceContour.UPPER_LIP_TOP, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
val upperLipBottomContour = ContourParcel(FaceContour.UPPER_LIP_BOTTOM, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
val lowerLipTopContour = ContourParcel(FaceContour.LOWER_LIP_TOP, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
val lowerLipBottomContour = ContourParcel(FaceContour.LOWER_LIP_BOTTOM, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
val noseBridgeContour = ContourParcel(FaceContour.NOSE_BRIDGE, arrayListOf(noseBaseMark.position))
val noseBottomContour = ContourParcel(FaceContour.NOSE_BOTTOM, arrayListOf(noseBaseMark.position))
val leftCheekContour = ContourParcel(FaceContour.LEFT_CHEEK, arrayListOf(leftCheekMark.position))
val rightCheekContour = ContourParcel(FaceContour.RIGHT_CHEEK, arrayListOf(rightCheekMark.position))
faces.add(FaceParcel(
i,
boundingBox,
0f,
0f,
0f,
leftEyeOpenProbability,
rightEyeOpenProbability,
smilingProbability,
confidence,
arrayListOf(mouthBottomMark, leftCheekMark, leftEarMark, leftEyeMark, mouthLeftMark, noseBaseMark, rightCheekMark, rightEarMark, rightEyeMark, mouthRightMark),
arrayListOf(faceContour, leftEyebrowTopContour, leftEyebrowBottomContour, rightEyebrowTopContour, rightEyebrowBottomContour, leftEyeContour, rightEyeContour, upperLipTopContour, upperLipBottomContour, lowerLipTopContour, lowerLipBottomContour, noseBridgeContour, noseBottomContour, leftCheekContour, rightCheekContour)
).also {
Log.d(TAG, "parseDetections: face->$it")
})
}
Log.d(TAG, "parseDetections: faces->${faces.size}")
return faces
}
private fun calculateSmilingProbability(rightMouthCorner: LandmarkParcel, leftMouthCorner: LandmarkParcel): Float {
val mouthWidth = hypot(
(rightMouthCorner.position.x - leftMouthCorner.position.x).toDouble(), (rightMouthCorner.position.y - leftMouthCorner.position.y).toDouble()
).toFloat()
return (mouthWidth / 100).coerceIn(0f, 1f)
}
private fun calculateEyeOpenProbability(eye: LandmarkParcel, mouthCorner: LandmarkParcel): Float {
val eyeMouthDistance = hypot(
(eye.position.x - mouthCorner.position.x).toDouble(), (eye.position.y - mouthCorner.position.y).toDouble()
).toFloat()
return (eyeMouthDistance / 50).coerceIn(0f, 1f)
}
private fun calculateMidPoint(eye: LandmarkParcel, mouth: LandmarkParcel): PointF {
return PointF((eye.position.x + mouth.position.x) / 2, (eye.position.y + mouth.position.y) / 2)
}
private fun yuv420ToBitmap(image: Image): Bitmap? {
val width = image.width
val height = image.height
val yBuffer = image.planes[0].buffer
val uBuffer = image.planes[1].buffer
val vBuffer = image.planes[2].buffer
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val nv21 = ByteArray(ySize + uSize + vSize)
yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)
return nv21toBitmap(nv21, width, height)
}
private fun nv21toBitmap(byteArray: ByteArray, width: Int, height: Int): Bitmap? {
try {
val yuvImage = YuvImage(byteArray, ImageFormat.NV21, width, height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, out)
val jpegBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
} catch (e: Exception) {
Log.w(TAG, "nv21toBitmap: failed ", e)
return null
}
}
private fun bitmapToMat(bitmap: Bitmap): Mat? {
try {
val mat = Mat(bitmap.height, bitmap.width, CvType.CV_8UC4)
Utils.bitmapToMat(bitmap, mat)
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGBA2BGR)
return mat
} catch (e: Exception) {
Log.w(TAG, "bitmapToMat: failed", e)
return null
}
}
private fun imageToMat(image: Image): Mat? {
val bitmap = when (image.format) {
ImageFormat.JPEG -> {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
ImageFormat.YUV_420_888 -> {
yuv420ToBitmap(image)
}
else -> {
null
}
}
return bitmap?.let { bitmapToMat(it) }
}
private fun nv21ToMat(nv21ByteArray: ByteArray, width: Int, height: Int): Mat? {
val bitmap = nv21toBitmap(nv21ByteArray, width, height)
return bitmap?.let { bitmapToMat(it) }
}
private fun degree(rotation: Int): Int {
if (rotation == 0) return 1
if (rotation == 1) return 4
if (rotation == 2) return 3
if (rotation == 3) return 2
return 1
}
}

View file

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: 2025, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.vision.face.mlkit
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.media.Image
import android.util.Log
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.unwrap
import com.google.mlkit.vision.face.FaceDetectionOptions
import com.google.mlkit.vision.face.FrameMetadataParcel
import com.google.mlkit.vision.face.aidls.FaceParcel
import com.google.mlkit.vision.face.aidls.IFaceDetector
import org.microg.gms.vision.face.TAG
import org.microg.gms.vision.face.FaceDetectorHelper
import java.nio.ByteBuffer
class FaceDetector(val context: Context, val options: FaceDetectionOptions?) : IFaceDetector.Stub() {
private var mFaceDetector: FaceDetectorHelper? = null
override fun detectFaces(wrapper: IObjectWrapper?, metadata: FrameMetadataParcel?): List<FaceParcel> {
Log.d(TAG, "MLKit detectFaces method: metadata:${metadata}")
if (wrapper == null || metadata == null || mFaceDetector == null) return arrayListOf()
val format = metadata.format
val rotation = metadata.rotation
if (format == -1) {
val bitmap = wrapper.unwrap<Bitmap>() ?: return arrayListOf()
return mFaceDetector?.detectFaces(bitmap, rotation) ?: arrayListOf()
}
if (format == ImageFormat.NV21) {
val byteBuffer = wrapper.unwrap<ByteBuffer>() ?: return arrayListOf()
return mFaceDetector?.detectFaces(byteBuffer.array(), metadata.width, metadata.height, rotation) ?: arrayListOf()
}
if (format == ImageFormat.YUV_420_888) {
val image = wrapper.unwrap<Image>() ?: return arrayListOf()
return mFaceDetector?.detectFaces(image, rotation) ?: arrayListOf()
}
return arrayListOf()
}
override fun initDetector() {
Log.d(TAG, "MLKit initDetector method isInitialized")
if (mFaceDetector == null) {
try {
mFaceDetector = FaceDetectorHelper(context)
} catch (e: Exception) {
Log.d(TAG, "initDetector: failed", e)
}
}
}
override fun close() {
Log.d(TAG, "MLKit close")
mFaceDetector?.release()
mFaceDetector = null
}
}