Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
43
play-services-vision/build.gradle
Normal file
43
play-services-vision/build.gradle
Normal 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")
|
||||
}
|
||||
58
play-services-vision/core/build.gradle
Normal file
58
play-services-vision/core/build.gradle
Normal 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'
|
||||
16
play-services-vision/core/src/main/AndroidManifest.xml
Normal file
16
play-services-vision/core/src/main/AndroidManifest.xml
Normal 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>
|
||||
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
6
play-services-vision/src/main/AndroidManifest.xml
Normal file
6
play-services-vision/src/main/AndroidManifest.xml
Normal 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 />
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue