app scaffold
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
@@ -0,0 +1,77 @@
|
||||
group = "com.conti.native_scan"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.3.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:9.0.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.conti.native_scan"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
java.srcDirs("src/main/kotlin")
|
||||
}
|
||||
getByName("test") {
|
||||
java.srcDirs("src/test/kotlin")
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
|
||||
it.outputs.upToDateWhen { false }
|
||||
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = "native_scan"
|
||||
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.conti.native_scan">
|
||||
</manifest>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.conti.native_scan
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
|
||||
/**
|
||||
* 扫码插件的 Android 入口。
|
||||
*
|
||||
* TODO(native): 手写原生实现本次不在范围内,见 conti-docs/07-native-integration.md。
|
||||
* 补完时需要做的事:
|
||||
* 1. 让某个类实现 pigeon 生成的 `ScanHostApi`(见 ScanApi.g.kt,不要手改生成物);
|
||||
* 2. 在 onAttachedToEngine 里 `ScanHostApi.setUp(binding.binaryMessenger, impl)`;
|
||||
* 3. 实现 ActivityAware 拿到 Activity(扫码要起页面、要申请相机权限);
|
||||
* 4. 取消 / 权限拒绝 / 能力不可用必须回 FlutterError,code 用
|
||||
* CANCELLED / PERMISSION_DENIED / UNAVAILABLE —— 与 Dart 侧
|
||||
* NativeScanErrorCode 一一对应,**不允许返回占位假数据**。
|
||||
*
|
||||
* 在此之前,Dart 侧调用会收到 MissingPluginException,
|
||||
* 并被 NativeScan 转成 UNSUPPORTED_PLATFORM,属预期行为。
|
||||
*/
|
||||
class NativeScanPlugin : FlutterPlugin {
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
// TODO(native): ScanHostApi.setUp(binding.binaryMessenger, ScanApiImpl(...))
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
// TODO(native): ScanHostApi.setUp(binding.binaryMessenger, null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
// Autogenerated from Pigeon (v27.3.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
package com.conti.native_scan
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.BasicMessageChannel
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MessageCodec
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
private object ScanApiPigeonUtils {
|
||||
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
fun doubleEquals(a: Double, b: Double): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun floatEquals(a: Float, b: Float): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun doubleHash(d: Double): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (d == 0.0) 0.0 else d
|
||||
val bits = java.lang.Double.doubleToLongBits(normalized)
|
||||
return (bits xor (bits ushr 32)).toInt()
|
||||
}
|
||||
|
||||
fun floatHash(f: Float): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (f == 0.0f) 0.0f else f
|
||||
return java.lang.Float.floatToIntBits(normalized)
|
||||
}
|
||||
|
||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if (a == null || b == null) {
|
||||
return false
|
||||
}
|
||||
if (a is ByteArray && b is ByteArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is IntArray && b is IntArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is LongArray && b is LongArray) {
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is DoubleArray && b is DoubleArray) {
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!doubleEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is FloatArray && b is FloatArray) {
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!floatEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Array<*> && b is Array<*>) {
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!deepEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is List<*> && b is List<*>) {
|
||||
if (a.size != b.size) return false
|
||||
val iterA = a.iterator()
|
||||
val iterB = b.iterator()
|
||||
while (iterA.hasNext() && iterB.hasNext()) {
|
||||
if (!deepEquals(iterA.next(), iterB.next())) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Map<*, *> && b is Map<*, *>) {
|
||||
if (a.size != b.size) return false
|
||||
for (entry in a) {
|
||||
val key = entry.key
|
||||
var found = false
|
||||
for (bEntry in b) {
|
||||
if (deepEquals(key, bEntry.key)) {
|
||||
if (deepEquals(entry.value, bEntry.value)) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Double && b is Double) {
|
||||
return doubleEquals(a, b)
|
||||
}
|
||||
if (a is Float && b is Float) {
|
||||
return floatEquals(a, b)
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
fun deepHash(value: Any?): Int {
|
||||
return when (value) {
|
||||
null -> 0
|
||||
is ByteArray -> value.contentHashCode()
|
||||
is IntArray -> value.contentHashCode()
|
||||
is LongArray -> value.contentHashCode()
|
||||
is DoubleArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + doubleHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is FloatArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + floatHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Array<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is List<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Map<*, *> -> {
|
||||
var result = 0
|
||||
for (entry in value) {
|
||||
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
|
||||
}
|
||||
result
|
||||
}
|
||||
is Double -> doubleHash(value)
|
||||
is Float -> floatHash(value)
|
||||
else -> value.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||
* @property code The error code.
|
||||
* @property message The error message.
|
||||
* @property details The error details. Must be a datatype supported by the api codec.
|
||||
*/
|
||||
class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : RuntimeException()
|
||||
|
||||
/**
|
||||
* 识别类型。
|
||||
*
|
||||
* **即使首版只做条码,这个参数也必须先留出来**(07 §「待确认:VIN 码与车牌
|
||||
* 识别的技术路径」):车牌走的是专用 OCR、VIN 印刷字符走通用 OCR + 校验位
|
||||
* 过滤,技术路径还没定。参数先在 schema 里占好位,后面加识别类型就不用改
|
||||
* 接口签名——改签名意味着三端生成物和所有调用点一起动。
|
||||
*/
|
||||
enum class ScanMode(val raw: Int) {
|
||||
/** 二维码 / 条形码(商品、库位)。 */
|
||||
BARCODE(0),
|
||||
/** VIN 码。可能是 Code 39 条码,也可能只有印刷字符。 */
|
||||
VIN(1),
|
||||
/** 车牌。 */
|
||||
PLATE(2);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): ScanMode? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码入参。
|
||||
*
|
||||
* Generated class from Pigeon that represents data sent in messages.
|
||||
*/
|
||||
data class ScanOptions (
|
||||
/** 识别类型。 */
|
||||
val mode: ScanMode,
|
||||
/** 超时毫秒数。null 表示不超时,由用户手动取消。 */
|
||||
val timeoutMs: Long? = null,
|
||||
/** 是否默认打开闪光灯。 */
|
||||
val torchEnabled: Boolean? = null,
|
||||
/** 扫码页标题。由调用方传,`native_scan` 不依赖任何 i18n 资源。 */
|
||||
val title: String? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): ScanOptions {
|
||||
val mode = pigeonVar_list[0] as ScanMode
|
||||
val timeoutMs = pigeonVar_list[1] as Long?
|
||||
val torchEnabled = pigeonVar_list[2] as Boolean?
|
||||
val title = pigeonVar_list[3] as String?
|
||||
return ScanOptions(mode, timeoutMs, torchEnabled, title)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
mode,
|
||||
timeoutMs,
|
||||
torchEnabled,
|
||||
title,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
val other = other as ScanOptions
|
||||
return ScanApiPigeonUtils.deepEquals(this.mode, other.mode) && ScanApiPigeonUtils.deepEquals(this.timeoutMs, other.timeoutMs) && ScanApiPigeonUtils.deepEquals(this.torchEnabled, other.torchEnabled) && ScanApiPigeonUtils.deepEquals(this.title, other.title)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.mode)
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.timeoutMs)
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.torchEnabled)
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.title)
|
||||
return result
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "ScanOptions(mode=$mode, timeoutMs=$timeoutMs, torchEnabled=$torchEnabled, title=$title)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码结果。
|
||||
*
|
||||
* Generated class from Pigeon that represents data sent in messages.
|
||||
*/
|
||||
data class ScanResult (
|
||||
/** 实际生效的识别类型。 */
|
||||
val mode: ScanMode,
|
||||
/** 识别到的文本。 */
|
||||
val value: String,
|
||||
/** 从打开扫码页到出结果的耗时,供埋点用(见 13 的 `scan_succeeded`)。 */
|
||||
val durationMs: Long,
|
||||
/** 原始码制(如 `CODE_39` / `QR_CODE`)。OCR 路径下为 null。 */
|
||||
val rawFormat: String? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): ScanResult {
|
||||
val mode = pigeonVar_list[0] as ScanMode
|
||||
val value = pigeonVar_list[1] as String
|
||||
val durationMs = pigeonVar_list[2] as Long
|
||||
val rawFormat = pigeonVar_list[3] as String?
|
||||
return ScanResult(mode, value, durationMs, rawFormat)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
mode,
|
||||
value,
|
||||
durationMs,
|
||||
rawFormat,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
val other = other as ScanResult
|
||||
return ScanApiPigeonUtils.deepEquals(this.mode, other.mode) && ScanApiPigeonUtils.deepEquals(this.value, other.value) && ScanApiPigeonUtils.deepEquals(this.durationMs, other.durationMs) && ScanApiPigeonUtils.deepEquals(this.rawFormat, other.rawFormat)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.mode)
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.value)
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.durationMs)
|
||||
result = 31 * result + ScanApiPigeonUtils.deepHash(this.rawFormat)
|
||||
return result
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "ScanResult(mode=$mode, value=$value, durationMs=$durationMs, rawFormat=$rawFormat)"
|
||||
}
|
||||
}
|
||||
private open class ScanApiPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
ScanMode.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ScanOptions.fromList(it)
|
||||
}
|
||||
}
|
||||
131.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ScanResult.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is ScanMode -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw.toLong())
|
||||
}
|
||||
is ScanOptions -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is ScanResult -> {
|
||||
stream.write(131)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dart → 原生。
|
||||
*
|
||||
* 用户取消、权限拒绝、平台未实现这三类都通过 `FlutterError` 抛出,
|
||||
* 由 Dart 侧的公共 API 转成 `NativeScanException`——**原生异常类型
|
||||
* (`PlatformException`)不允许直接抛到业务代码里**(07 §使用规则)。
|
||||
*
|
||||
* Generated interface from Pigeon that represents a handler of messages from Flutter.
|
||||
*/
|
||||
interface ScanHostApi {
|
||||
/**
|
||||
* 打开扫码页并等待一次结果。
|
||||
*
|
||||
* 用户取消时抛 code 为 `CANCELLED` 的错误,而不是返回 null——
|
||||
* 「取消」和「扫到了空字符串」必须能区分开。
|
||||
*/
|
||||
fun startScan(options: ScanOptions, callback: (Result<ScanResult>) -> Unit)
|
||||
/**
|
||||
* 当前平台是否支持指定识别类型。
|
||||
*
|
||||
* 车牌识别的技术路径未定(见上),首版可能只有部分平台支持;
|
||||
* 调用方应当先查这个再决定要不要显示入口,而不是等 `startScan` 抛错。
|
||||
*/
|
||||
fun isModeSupported(mode: ScanMode): Boolean
|
||||
|
||||
companion object {
|
||||
/** The codec used by ScanHostApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
ScanApiPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `ScanHostApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: ScanHostApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.native_scan.ScanHostApi.startScan$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val optionsArg = args[0] as ScanOptions
|
||||
api.startScan(optionsArg) { result: Result<ScanResult> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(ScanApiPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(ScanApiPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.native_scan.ScanHostApi.isModeSupported$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val modeArg = args[0] as ScanMode
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.isModeSupported(modeArg))
|
||||
} catch (exception: Throwable) {
|
||||
ScanApiPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user