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,43 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
android {
namespace "com.google.android.gms.vision"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
buildFeatures {
aidl = true
}
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
apply from: '../gradle/publish-android.gradle'
description = 'microG implementation of play-services-vision'
dependencies {
// Dependencies from play-services-vision:20.1.3
api project(':play-services-base')
api project(':play-services-basement')
api project(':play-services-vision-common')
annotationProcessor project(":safe-parcel-processor")
}

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'
apply plugin: 'signing'
dependencies {
api project(':play-services-mlkit-barcode-scanning')
api project(':play-services-mlkit-face-detection')
api project(':play-services-vision')
implementation project(':play-services-base-core')
implementation "androidx.annotation:annotation:$annotationVersion"
implementation "com.google.zxing:core:3.5.2"
implementation 'org.opencv:opencv:4.11.0'
implementation "androidx.camera:camera-core:1.3.0"
implementation "androidx.camera:camera-camera2:1.3.0"
implementation "androidx.camera:camera-lifecycle:1.3.0"
implementation "androidx.camera:camera-view:1.3.0"
}
android {
namespace "org.microg.gms.vision.core"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
sourceSets {
main {
java.srcDirs += 'src/main/kotlin'
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
}
apply from: '../../gradle/publish-android.gradle'
description = 'microG service implementation for play-services-vision'

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
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest />

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode;
parcelable Barcode;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode.internal.client;
parcelable BarcodeDetectorOptions;

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode.internal.client;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions;
import com.google.android.gms.vision.internal.FrameMetadataParcel;
import com.google.android.gms.dynamic.IObjectWrapper;
interface INativeBarcodeDetector {
Barcode[] detectBytes(IObjectWrapper byteBuffer, in FrameMetadataParcel metadata) = 0;
Barcode[] detectBitmap(IObjectWrapper bitmap, in FrameMetadataParcel metadata) = 1;
void close() = 2;
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode.internal.client;
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.dynamic.IObjectWrapper;
interface INativeBarcodeDetectorCreator {
INativeBarcodeDetector create(IObjectWrapper context, in BarcodeDetectorOptions options) = 0;
}

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face;
parcelable Contour;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face;
parcelable Landmark;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face.internal.client;
parcelable DetectionOptions;

View file

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face.internal.client;
parcelable FaceParcel;

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face.internal.client;
import com.google.android.gms.vision.face.internal.client.FaceParcel;
import com.google.android.gms.dynamic.IObjectWrapper;
import com.google.android.gms.vision.internal.FrameMetadataParcel;
interface INativeFaceDetector {
FaceParcel[] detectFaceParcels(IObjectWrapper byteBuffer, in FrameMetadataParcel metadata) = 0;
boolean isNativeFaceDetectorAvailable(int type) = 1;
void closeDetectorJni() = 2;
FaceParcel[] detectFacesFromPlanes(IObjectWrapper planeFirst, IObjectWrapper planeSecond, IObjectWrapper planeThird, int firstPixelStride, int secondPixelStride, int thirdPixelStride, int firstRowStride, int secondRowStride, int thirdRowStride, in FrameMetadataParcel metadata) = 3;
}

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face.internal.client;
import com.google.android.gms.vision.face.internal.client.DetectionOptions;
import com.google.android.gms.dynamic.IObjectWrapper;
import com.google.android.gms.vision.face.internal.client.INativeFaceDetector;
interface INativeFaceDetectorCreator {
INativeFaceDetector newFaceDetector(IObjectWrapper context, in DetectionOptions detectionOptions) = 0;
}

View file

@ -0,0 +1,593 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.vision.barcode;
import android.graphics.Point;
import android.graphics.Rect;
import androidx.annotation.Nullable;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
/**
* Barcode represents a single recognized barcode and its value.
* <p>
* The barcode's raw, unmodified, and uninterpreted content is returned in the {@link #rawValue} field, while the barcode type (i.e. its encoding) can be found in the {@link #format} field.
* <p>
* Barcodes that contain structured data (commonly done with QR codes) are parsed and iff valid, the {@link #valueFormat} field is set to one of the value format constants (e.g. {@link #GEO}) and the corresponding field is set (e.g. {@link #geoPoint}).
*/
@PublicApi
public class Barcode extends AutoSafeParcelable {
/**
* Barcode value format constant for contact information.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int CONTACT_INFO = 1;
/**
* Barcode value format constant for email message details.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int EMAIL = 2;
/**
* Barcode value format constant for ISBNs.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int ISBN = 3;
/**
* Barcode value format constant for phone numbers.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int PHONE = 4;
/**
* Barcode value format constant for product codes.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int PRODUCT = 5;
/**
* Barcode value format constant for SMS details.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int SMS = 6;
/**
* Barcode value format constant for plain text.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int TEXT = 7;
/**
* Barcode value format constant for URLs/bookmarks.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int URL = 8;
/**
* Barcode value format constant for WiFi access point details.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int WIFI = 9;
/**
* Barcode value format constant for geographic coordinates.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int GEO = 10;
/**
* Barcode value format constant for calendar events.
* Specifies the format of a Barcode value via the {@link #valueFormat} field.
*/
public static final int CALENDAR_EVENT = 11;
public static final int DRIVER_LICENSE = 12;
/**
* Barcode format constant representing the union of all supported formats.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize.
* This is also the default setting.
*/
public static final int ALL_FORMATS = 0;
/**
* Barcode format constant for Code 128.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int CODE_128 = 1;
/**
* Barcode format constant for Code 39.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int CODE_39 = 2;
/**
* Barcode format constant for Code 93.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int CODE_93 = 4;
/**
* Barcode format constant for Codebar.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int CODABAR = 8;
/**
* Barcode format constant for Data Matrix.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int DATA_MATRIX = 16;
/**
* Barcode format constant for EAN-13.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int EAN_13 = 32;
/**
* Barcode format constant for EAN-8.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int EAN_8 = 64;
/**
* Barcode format constant for ITF (Interleaved Two-of-Five).
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int ITF = 128;
/**
* Barcode format constant for QR Code.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int QR_CODE = 256;
/**
* Barcode format constant for UPC-A.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int UPC_A = 512;
/**
* Barcode format constant for UPC-E.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int UPC_E = 1024;
/**
* Barcode format constant for PDF-417.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int PDF417 = 2048;
/**
* Barcode format constant for AZTEC.
* Pass into {@link BarcodeDetector.Builder#setBarcodeFormats(int)} to select formats to recognize,
* and also specifies a detected Barcode's {@link #format} via the format field.
*/
public static final int AZTEC = 4096;
@Field(1)
private final int versionCode = 1;
/**
* Barcode format, for example {@link #EAN_13}.
* <p>
* Note that this field may contain values not present in the current set of format constants.
* When mapping this value to something else, it is advisable to have a default/fallback case.
*/
@Field(2)
public int format;
/**
* Barcode value as it was encoded in the barcode. Structured values are not parsed, for example: 'MEBKM:TITLE:Google;URL://www.google.com;;' Does not include the supplement value.
*/
@Field(3)
public String rawValue;
/**
* Barcode value in a user-friendly format.
* May omit some of the information encoded in the barcode.
* For example, in the case above the display_value might be '//www.google.com'.
* If {@link #valueFormat}=={@link #TEXT}, this field will be equal to {@link #rawValue}.
* This value may be multiline, for example, when line breaks are encoded into the original {@link #TEXT} barcode value.
* May include the supplement value.
*/
@Field(4)
public String displayValue;
/**
* Format of the barcode value. For example, {@link #TEXT}, {@link #PRODUCT}, {@link #URL}, etc.
* <p>
* Note that this field may contain values not present in the current set of value format constants.
* When mapping this value to something else, it is advisable to have a default/fallback case.
*/
@Field(5)
public int valueFormat;
/**
* 4 corner points in clockwise direction starting with top-left.
* Due to the possible perspective distortions, this is not necessarily a rectangle.
*/
@Field(6)
public Point[] cornerPoints;
/**
* Parsed email details (set iff {@link #valueFormat} is {@link #EMAIL}).
*/
@Nullable
@Field(7)
public Barcode.Email email;
/**
* Parsed phone details (set iff {@link #valueFormat} is {@link #PHONE}).
*/
@Nullable
@Field(8)
public Barcode.Phone phone;
/**
* Parsed SMS details (set iff {@link #valueFormat} is {@link #SMS}).
*/
@Nullable
@Field(9)
public Barcode.Sms sms;
/**
* Parsed WiFi AP details (set iff {@link #valueFormat} is {@link #WIFI}).
*/
@Nullable
@Field(10)
public Barcode.WiFi wifi;
/**
* Parsed URL bookmark details (set iff {@link #valueFormat} is {@link #URL}).
*/
@Nullable
@Field(11)
public Barcode.UrlBookmark url;
/**
* Parsed geo coordinates (set iff {@link #valueFormat} is {@link #GEO}).
*/
@Nullable
@Field(12)
public Barcode.GeoPoint geoPoint;
/**
* Parsed calendar event details (set iff {@link #valueFormat} is {@link #CALENDAR_EVENT}).
*/
@Nullable
@Field(13)
public Barcode.CalendarEvent calendarEvent;
/**
* Parsed contact details (set iff {@link #valueFormat} is {@link #CONTACT_INFO}).
*/
@Nullable
@Field(14)
public Barcode.ContactInfo contactInfo;
/**
* Parsed driver's license details (set iff {@link #valueFormat} is {@link #DRIVER_LICENSE}).
*/
@Nullable
@Field(15)
public Barcode.DriverLicense driverLicense;
/**
* Barcode value as it was encoded in the barcode as byte array.
*/
@Field(16)
public byte[] rawBytes;
/**
* If outputUnrecognizedBarcodes is set, isRecognized can be set to false to indicate failure in decoding the detected barcode.
*/
@Field(17)
public boolean isRecognized;
/**
* Returns the barcode's axis-aligned bounding box.
*/
public Rect getBoundingBox() {
int left = Integer.MAX_VALUE, top = Integer.MIN_VALUE, right = Integer.MIN_VALUE, bottom = Integer.MAX_VALUE;
for (Point point : cornerPoints) {
left = Math.min(left, point.x);
top = Math.max(top, point.y);
right = Math.max(right, point.x);
bottom = Math.min(bottom, point.y);
}
return new Rect(left, top, right, bottom);
}
/**
* An address.
*/
public static class Address extends AutoSafeParcelable {
/**
* Address type.
*/
public static final int UNKNOWN = 0;
public static final int WORK = 1;
public static final int HOME = 2;
@Field(1)
private int versionCode = 1;
@Field(2)
public int type;
/**
* Formatted address, multiple lines when appropriate. This field always contains at least one line.
*/
@Field(3)
public String[] addressLines;
public static Creator<Address> CREATOR = new AutoCreator<>(Address.class);
}
/**
* DateTime data type used in calendar events. If hours/minutes/seconds are not specified in the barcode value, they will be set to -1.
*/
public static class CalendarDateTime extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
@Field(2)
public int year;
@Field(3)
public int month;
@Field(4)
public int day;
@Field(5)
public int hours;
@Field(6)
public int minutes;
@Field(7)
public int seconds;
@Field(8)
public boolean isUtc;
@Field(9)
public String rawValue;
public static Creator<CalendarDateTime> CREATOR = new AutoCreator<>(CalendarDateTime.class);
}
/**
* A calendar event extracted from QRCode.
*/
public static class CalendarEvent extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
@Field(2)
public String summary;
@Field(3)
public String description;
@Field(4)
public String location;
@Field(5)
public String organizer;
@Field(6)
public String status;
@Field(7)
public Barcode.CalendarDateTime start;
@Field(8)
public Barcode.CalendarDateTime end;
public static Creator<CalendarEvent> CREATOR = new AutoCreator<>(CalendarEvent.class);
}
/**
* A person's or organization's business card. For example a VCARD.
*/
public static class ContactInfo extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
@Field(2)
public Barcode.PersonName name;
@Field(3)
public String organization;
@Field(4)
public String title;
@Field(5)
public Barcode.Phone[] phones;
@Field(6)
public Barcode.Email[] emails;
@Field(7)
public String[] urls;
@Field(8)
public Barcode.Address[] addresses;
public static Creator<ContactInfo> CREATOR = new AutoCreator<>(ContactInfo.class);
}
/**
* A driver license or ID card.
*/
public static class DriverLicense extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
/**
* "DL" for driver licenses, "ID" for ID cards.
*/
@Field(2)
public String documentType;
/**
* Holder's first name.
*/
@Field(3)
public String firstName;
@Field(4)
public String middleName;
@Field(5)
public String lastName;
/**
* Gender. 1 - male, 2 - female.
*/
@Field(6)
public String gender;
/**
* Holder's street address.
*/
@Field(7)
public String addressStreet;
@Field(8)
public String addressCity;
@Field(9)
public String addressState;
@Field(10)
public String addressZip;
/**
* Driver license ID number.
*/
@Field(11)
public String licenseNumber;
/**
* The date format depends on the issuing country. MMDDYYYY for the US, YYYYMMDD for Canada.
*/
@Field(12)
public String issueDate;
@Field(13)
public String expiryDate;
@Field(14)
public String birthDate;
/**
* Country in which DL/ID was issued. US = "USA", Canada = "CAN".
*/
@Field(15)
public String issuingCountry;
public static Creator<DriverLicense> CREATOR = new AutoCreator<>(DriverLicense.class);
}
/**
* An email message from a 'MAILTO:' or similar QRCode type.
*/
public static class Email extends AutoSafeParcelable {
/**
* Email type.
*/
public static final int UNKNOWN = 0;
public static final int WORK = 1;
public static final int HOME = 2;
@Field(1)
private int versionCode = 1;
@Field(2)
public int type;
@Field(3)
public String address;
@Field(4)
public String subject;
@Field(5)
public String body;
public static Creator<Email> CREATOR = new AutoCreator<>(Email.class);
}
/**
* GPS coordinates from a 'GEO:' or similar QRCode type.
*/
public static class GeoPoint extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
@Field(2)
public double lat;
@Field(3)
public double lng;
public static Creator<GeoPoint> CREATOR = new AutoCreator<>(GeoPoint.class);
}
/**
* A person's name, both formatted version and individual name components.
*/
public static class PersonName extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
/**
* Properly formatted name.
*/
@Field(2)
public String formattedName;
/**
* Designates a text string to be set as the kana name in the phonebook. Used for Japanese contacts.
*/
@Field(3)
public String pronunciation;
@Field(4)
public String prefix;
@Field(5)
public String first;
@Field(6)
public String middle;
@Field(7)
public String last;
@Field(8)
public String suffix;
public static Creator<PersonName> CREATOR = new AutoCreator<>(PersonName.class);
}
/**
* A phone number from a 'TEL:' or similar QRCode type.
*/
public static class Phone extends AutoSafeParcelable {
/**
* Phone type.
*/
public static final int UNKNOWN = 0;
public static final int WORK = 1;
public static final int HOME = 2;
public static final int FAX = 3;
public static final int MOBILE = 4;
@Field(1)
private int versionCode = 1;
@Field(2)
public int type;
@Field(3)
public String number;
public static Creator<Phone> CREATOR = new AutoCreator<>(Phone.class);
}
/**
* An sms message from an 'SMS:' or similar QRCode type.
*/
public static class Sms extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
@Field(2)
public String message;
@Field(3)
public String phoneNumber;
public static Creator<Sms> CREATOR = new AutoCreator<>(Sms.class);
}
/**
* A URL and title from a 'MEBKM:' or similar QRCode type.
*/
public static class UrlBookmark extends AutoSafeParcelable {
@Field(1)
private int versionCode = 1;
@Field(2)
public String title;
/**
* Bookmark URL. Note that some common errors may be corrected here. For example, "http//...", "http:...", etc. will be replaced with "//".
*/
@Field(3)
public String url;
public static Creator<UrlBookmark> CREATOR = new AutoCreator<>(UrlBookmark.class);
}
/**
* A wifi network parameters from a 'WIFI:' or similar QRCode type.
*/
public static class WiFi extends AutoSafeParcelable {
/**
* WiFi encryption type.
*/
public static final int OPEN = 1;
public static final int WPA = 2;
public static final int WEP = 3;
@Field(1)
private int versionCode = 1;
@Field(2)
public String ssid;
@Nullable
@Field(3)
public String password;
@Field(4)
public int encryptionType;
public static Creator<WiFi> CREATOR = new AutoCreator<>(WiFi.class);
}
public static final Creator<Barcode> CREATOR = new AutoCreator<>(Barcode.class);
}

View file

@ -0,0 +1,135 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.IInterface;
import android.os.RemoteException;
import android.util.SparseArray;
import com.google.android.gms.dynamic.ObjectWrapper;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.Frame;
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.vision.internal.FrameMetadataParcel;
import org.microg.gms.common.PublicApi;
/**
* Recognizes barcodes (in a variety of 1D and 2D formats) in a supplied {@link Frame}.
* <p>
* Build new BarcodeDetector instances using {@link BarcodeDetector.Builder}. By default, BarcodeDetector searches for barcodes in every supported format. For the best performance it is highly recommended that you specify a narrower set of barcode formats to detect.
* <p>
* Recognition results are returned by {@link #detect(Frame)} as Barcode instances.
*/
@PublicApi
public class BarcodeDetector extends Detector<Barcode> {
private INativeBarcodeDetector remote;
private BarcodeDetector(INativeBarcodeDetector remote) {
this.remote = remote;
}
/**
* Recognizes barcodes in the supplied {@link Frame}.
*
* @return mapping of int to {@link Barcode}, where the int domain represents an opaque ID for the barcode. Identical barcodes (as determined by their raw value) will have the same ID across frames.
*/
@Override
public SparseArray<Barcode> detect(Frame frame) {
if (frame == null) throw new IllegalArgumentException("No frame supplied.");
SparseArray<Barcode> result = new SparseArray<>();
if (remote != null) {
FrameMetadataParcel metadataParcel = frame.getMetadata().createParcel();
Barcode[] barcodes = null;
if (frame.getBitmap() != null) {
try {
barcodes = remote.detectBitmap(ObjectWrapper.wrap(frame.getBitmap()), metadataParcel);
} catch (RemoteException e) {
// Ignore
}
} else {
try {
barcodes = remote.detectBytes(ObjectWrapper.wrap(frame.getGrayscaleImageData()), metadataParcel);
} catch (RemoteException e) {
// Ignore
}
}
if (barcodes != null) {
for (Barcode barcode : barcodes) {
result.append(barcode.rawValue.hashCode(), barcode);
}
}
}
return result;
}
@Override
public boolean isOperational() {
return remote != null && super.isOperational();
}
@Override
public void release() {
super.release();
try {
remote.close();
} catch (RemoteException e) {
// Ignore
}
remote = null;
}
/**
* Barcode detector builder.
*/
public static class Builder {
private Context context;
private BarcodeDetectorOptions options = new BarcodeDetectorOptions();
/**
* Builder for BarcodeDetector.
*/
public Builder(Context context) {
this.context = context;
}
/**
* Bit mask (containing values like {@link Barcode#QR_CODE} and so on) that selects which formats this barcode detector should recognize.
* <p>
* By default, the detector will recognize all supported formats. This corresponds to the special {@link Barcode#ALL_FORMATS} constant.
*/
public Builder setBarcodeFormats(int formats) {
options.formats = formats;
return this;
}
/**
* Builds a barcode detector instance using the provided settings. If the underlying native implementation is unavailable (e.g. hasn't been downloaded yet), the detector will always return an empty result set. In this case, it will report that it is non-operational via {@link BarcodeDetector#isOperational()}.
* <p>
* Note that this method may cause blocking disk reads and should not be called on an application's main thread. To avoid blocking the main thread, consider moving Detector construction to a background thread using {@link android.os.AsyncTask}. Enable {@link android.os.StrictMode} to automatically detect blocking operations on the main thread.
*
* @return new {@link BarcodeDetector} instance
*/
public BarcodeDetector build() {
// TODO: Actually implement dynamite or load from remote
INativeBarcodeDetector remote = null;
try {
Class<?> clazz = Class.forName("com.google.android.gms.vision.barcode.ChimeraNativeBarcodeDetectorCreator");
Object instance = clazz.getConstructor().newInstance();
INativeBarcodeDetectorCreator creator = INativeBarcodeDetectorCreator.Stub.asInterface(((IInterface) instance).asBinder());
remote = creator.create(ObjectWrapper.wrap(context), options);
} catch (Exception e) {
// Ignore
}
return new BarcodeDetector(remote);
}
}
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.barcode.internal.client;
import org.microg.safeparcel.AutoSafeParcelable;
public class BarcodeDetectorOptions extends AutoSafeParcelable {
@Field(1)
public int versionCode = 1;
@Field(2)
public int formats;
public static Creator<BarcodeDetectorOptions> CREATOR = new AutoCreator<>(BarcodeDetectorOptions.class);
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face;
import android.graphics.PointF;
import android.os.Parcel;
import androidx.annotation.NonNull;
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
import java.util.List;
@SafeParcelable.Class
public class Contour extends AbstractSafeParcelable {
@Field(1)
public int type;
@Field(2)
public List<PointF> points;
@Constructor
public Contour(@Param(1) int type, @Param(2) List<PointF> points) {
this.type = type;
this.points = points;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
public static final SafeParcelableCreatorAndWriter<Contour> CREATOR = findCreator(Contour.class);
}

View file

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face;
import android.os.Parcel;
import androidx.annotation.NonNull;
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
@SafeParcelable.Class
public class Landmark extends AbstractSafeParcelable {
@Field(1)
public int id;
@Field(2)
public Float x;
@Field(3)
public Float y;
@Field(4)
public int type;
@Constructor
public Landmark(@Param(1) int id, @Param(2) Float x, @Param(3) Float y, @Param(4) int type) {
this.id = id;
this.x = x;
this.y = y;
this.type = type;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
public static final SafeParcelableCreatorAndWriter<Landmark> CREATOR = findCreator(Landmark.class);
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face.internal.client;
import android.os.Parcel;
import androidx.annotation.NonNull;
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
@SafeParcelable.Class
public class DetectionOptions extends AbstractSafeParcelable {
@Field(2)
public int mode;
@Field(3)
public int landmarkType;
@Field(4)
public int classificationType;
@Field(5)
public boolean prominentFaceOnly;
@Field(6)
public boolean trackingEnabled;
@Field(7)
public float minFaceSize;
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
public static final SafeParcelableCreatorAndWriter<DetectionOptions> CREATOR = findCreator(DetectionOptions.class);
}

View file

@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.vision.face.internal.client;
import android.os.Parcel;
import androidx.annotation.NonNull;
import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelable;
import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter;
import com.google.android.gms.vision.face.Contour;
import com.google.android.gms.vision.face.Landmark;
@SafeParcelable.Class
public class FaceParcel extends AbstractSafeParcelable {
@Field(value = 1, defaultValue = "1")
public final int versionCode;
@Field(2)
public final int id;
@Field(3)
public final float centerX;
@Field(4)
public final float centerY;
@Field(5)
public final float width;
@Field(6)
public final float height;
@Field(7)
public final float eulerY;
@Field(8)
public final float eulerZ;
@Field(14)
public final float eulerX;
@Field(9)
public final Landmark[] landmarks;
@Field(10)
public final float leftEyeOpenProbability;
@Field(11)
public final float rightEyeOpenProbability;
@Field(12)
public final float smileProbability;
@Field(13)
public final Contour[] contours;
@Field(value = 15, defaultValue = "-1.0f")
public final float confidenceScore;
@Constructor
public FaceParcel(@Param(1) int versionCode, @Param(2) int id, @Param(3) float centerX, @Param(4) float centerY, @Param(5) float width, @Param(6) float height, @Param(7) float eulerY, @Param(8) float eulerZ, @Param(14) float eulerX, @Param(9) Landmark[] landmarks, @Param(10) float leftEyeOpenProbability, @Param(11) float rightEyeOpenProbability, @Param(12) float smileProbability, @Param(13) Contour[] contours, @Param(15) float confidenceScore) {
this.versionCode = versionCode;
this.id = id;
this.centerX = centerX;
this.centerY = centerY;
this.width = width;
this.height = height;
this.eulerY = eulerY;
this.eulerZ = eulerZ;
this.eulerX = eulerX;
this.landmarks = landmarks;
this.leftEyeOpenProbability = leftEyeOpenProbability;
this.rightEyeOpenProbability = rightEyeOpenProbability;
this.smileProbability = smileProbability;
this.contours = contours;
this.confidenceScore = confidenceScore;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
CREATOR.writeToParcel(this, dest, flags);
}
public static final SafeParcelableCreatorAndWriter<FaceParcel> CREATOR = findCreator(FaceParcel.class);
}