app scaffold

This commit is contained in:
Guangfei.Zhao
2026-08-17 15:29:55 +08:00
commit 681688dfae
301 changed files with 18414 additions and 0 deletions
+9
View File
@@ -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>
@@ -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. 取消 / 权限拒绝 / 能力不可用必须回 FlutterErrorcode 用
* 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)
}
}
}
}
}