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
+33
View File
@@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins-dependencies
/build/
/coverage/
+33
View File
@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "6b182d2c7585eba26d4edce0f97630effd256c33"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33
base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33
- platform: android
create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33
base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33
- platform: ios
create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33
base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+3
View File
@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.
+1
View File
@@ -0,0 +1 @@
TODO: Add your license here.
+15
View File
@@ -0,0 +1,15 @@
# native_scan
A new Flutter plugin project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1 @@
include: ../../analysis_options.yaml
+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)
}
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+17
View File
@@ -0,0 +1,17 @@
# native_scan_example
Demonstrates how to use the native_scan plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1 @@
include: ../../../analysis_options.yaml
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.conti.native_scan_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.conti.native_scan_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="native_scan_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.conti.native_scan_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
@@ -0,0 +1,19 @@
// 原生侧接通后才能跑的集成测试。
//
// 现在 Kotlin / Swift 实现还是空的(见 07),所以只验证「能力查询」这条
// 不会崩、并且**如实返回 false**——静默返回 true 才是危险的假象。
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:native_scan/native_scan.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('isModeSupported 在原生未实现时返回 false 而不是抛错', (WidgetTester tester) async {
final NativeScan plugin = NativeScan();
// TODO(native): 原生实现落地后改为 expect(..., isTrue),并补 startScan 的用例。
expect(await plugin.isModeSupported(ScanMode.barcode), isFalse);
});
}
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1,648 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = Flutter/ephemeral/Packages/.packages/FlutterFramework; sourceTree = "<group>"; };
78DABEA22ED26510000E7860 /* native_scan */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = native_scan; path = ../../ios/native_scan; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78DABEA22ED26510000E7860 /* native_scan */,
784666492D4C4C64000A1A5F /* FlutterFramework */,
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.conti.nativeScanExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.conti.nativeScanExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.conti.nativeScanExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.conti.nativeScanExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.conti.nativeScanExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.conti.nativeScanExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Native Scan</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>native_scan_example</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
@@ -0,0 +1,29 @@
import Flutter
import UIKit
import XCTest
// If your plugin has been explicitly set to "type: .dynamic" in the Package.swift,
// you will need to add your plugin as a dependency of RunnerTests within Xcode.
@testable import native_scan
// This demonstrates a simple unit test of the Swift portion of this plugin's implementation.
//
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
class RunnerTests: XCTestCase {
func testGetPlatformVersion() {
let plugin = NativeScanPlugin()
let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: [])
let resultExpectation = expectation(description: "result block must be called.")
plugin.handle(call) { result in
XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion)
resultExpectation.fulfill()
}
waitForExpectations(timeout: 1)
}
}
@@ -0,0 +1,70 @@
// native_scan 的最小演示壳。
//
// 只用来手工验证原生实现接没接上:Kotlin / Swift 侧还没写(见 07),
// 所以现在点「扫码」必然走到 UNSUPPORTED_PLATFORM 分支——这是预期结果,
// 也正好演示了 07 §「OHOS 后续演进」要求的**不静默返回假数据**。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:native_scan/native_scan.dart';
void main() {
runApp(const ExampleApp());
}
/// 演示用的根 Widget。
class ExampleApp extends StatefulWidget {
/// 构造。
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
final NativeScan _scan = NativeScan();
String _result = '未扫码';
Future<void> _startScan() async {
try {
final ScanResult result = await _scan.startScan(
ScanOptions(mode: ScanMode.barcode, title: '扫描商品条码'),
);
if (!mounted) return;
setState(() {
_result = '${result.value}${result.durationMs}ms';
});
} on NativeScanException catch (e) {
if (!mounted) return;
// 取消不是错误,静默回到原状态即可(07 §使用规则)。
setState(() {
_result = e.isCancelled ? '已取消' : '${e.code}: ${e.message}';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('native_scan example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(_result),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
unawaited(_startScan());
},
child: const Text('扫码'),
),
],
),
),
),
);
}
}
+22
View File
@@ -0,0 +1,22 @@
name: native_scan_example
description: native_scan 的手动验证入口。补完原生实现后用它单独跑扫码,不用起整个 App。
publish_to: none
resolution: workspace
environment:
sdk: ^3.12.0
dependencies:
flutter:
sdk: flutter
native_scan: ^0.1.0
dev_dependencies:
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
flutter:
uses-material-design: true
@@ -0,0 +1,17 @@
// example 的 smoke test:只验证壳能起来。
//
// 真正的扫码行为依赖原生实现,单测里跑不了;原生接通后请写
// integration_test 而不是在这里 mock。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:native_scan_example/main.dart';
void main() {
testWidgets('渲染出扫码入口', (WidgetTester tester) async {
await tester.pumpWidget(const ExampleApp());
expect(find.text('未扫码'), findsOneWidget);
expect(find.byWidgetPredicate((Widget w) => w is FilledButton), findsOneWidget);
});
}
+38
View File
@@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh
@@ -0,0 +1,29 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint native_scan.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'native_scan'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'native_scan/Sources/native_scan/**/*'
s.dependency 'Flutter'
s.platform = :ios, '13.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'native_scan_privacy' => ['native_scan/Sources/native_scan/PrivacyInfo.xcprivacy']}
end
@@ -0,0 +1,36 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "native_scan",
platforms: [
.iOS("13.0")
],
products: [
.library(name: "native-scan", targets: ["native_scan"])
],
dependencies: [
.package(name: "FlutterFramework", path: "../FlutterFramework")
],
targets: [
.target(
name: "native_scan",
dependencies: [
.product(name: "FlutterFramework", package: "FlutterFramework")
],
resources: [
// If your plugin requires a privacy manifest, for example if it uses any required
// reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's
// privacy impact, and then uncomment these lines. For more information, see
// https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
// .process("PrivacyInfo.xcprivacy"),
// If you have other resources that need to be bundled with your plugin, refer to
// the following instructions to add them:
// https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
]
)
]
)
@@ -0,0 +1,21 @@
import Flutter
import UIKit
/// iOS
///
/// TODO(native): conti-docs/07-native-integration.md
///
/// 1. pigeon `ScanHostApi` ScanApi.g.swift
/// 2. register `ScanHostApiSetup.setUp(binaryMessenger:api:)`
/// 3. Info.plist NSCameraUsageDescription
/// 4. / / FlutterErrorcode
/// CANCELLED / PERMISSION_DENIED / UNAVAILABLE Dart
/// NativeScanErrorCode ****
///
/// Dart MissingPluginException
/// NativeScan UNSUPPORTED_PLATFORM
public class NativeScanPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
// TODO(native): ScanHostApiSetup.setUp(binaryMessenger: registrar.messenger(), api: ScanApiImpl())
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,432 @@
// Autogenerated from Pigeon (v27.3.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#else
#error("Unsupported platform.")
#endif
/// Error class for passing custom error details to Dart side.
final class PigeonError: Error {
let code: String
let message: String?
let details: Sendable?
init(code: String, message: String?, details: Sendable?) {
self.code = code
self.message = message
self.details = details
}
var localizedDescription: String {
return
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
}
}
private func wrapResult(_ result: Any?) -> [Any?] {
return [result]
}
private func wrapError(_ error: Any) -> [Any?] {
if let pigeonError = error as? PigeonError {
return [
pigeonError.code,
pigeonError.message,
pigeonError.details,
]
}
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
return [
"\(error)",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
enum ScanApiPigeonInternal {
static func isNullish(_ value: Any?) -> Bool {
guard let innerValue = value else {
return true
}
if case Optional<Any>.some(Optional<Any>.none) = value {
return true
}
return innerValue is NSNull
}
static func doubleEquals(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
static func doubleHash(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
static func deepEquals(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
switch (cleanLhs, cleanRhs) {
case (nil, nil):
return true
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEquals(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEquals(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEquals(lhsKey, rhsKey) {
if deepEquals(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEquals(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
return false
}
}
static func deepHash(value: Any?, hasher: inout Hasher) {
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHash(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHash(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHash(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHash(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHash(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
} else {
hasher.combine(0)
}
}
}
private func nilOrValue<T>(_ value: Any?) -> T? {
if value is NSNull { return nil }
return value as! T?
}
///
///
/// **使**07 §VIN
/// OCRVIN OCR +
/// schema
///
enum ScanMode: Int, CaseIterable {
/// /
case barcode = 0
/// VIN Code 39
case vin = 1
///
case plate = 2
}
///
///
/// Generated class from Pigeon that represents data sent in messages.
struct ScanOptions: Hashable, CustomStringConvertible {
///
var mode: ScanMode
/// null
var timeoutMs: Int64? = nil
///
var torchEnabled: Bool? = nil
/// `native_scan` i18n
var title: String? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> ScanOptions? {
let mode = pigeonVar_list[0] as! ScanMode
let timeoutMs: Int64? = nilOrValue(pigeonVar_list[1])
let torchEnabled: Bool? = nilOrValue(pigeonVar_list[2])
let title: String? = nilOrValue(pigeonVar_list[3])
return ScanOptions(
mode: mode,
timeoutMs: timeoutMs,
torchEnabled: torchEnabled,
title: title
)
}
func toList() -> [Any?] {
return [
mode,
timeoutMs,
torchEnabled,
title,
]
}
static func == (lhs: ScanOptions, rhs: ScanOptions) -> Bool {
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return ScanApiPigeonInternal.deepEquals(lhs.mode, rhs.mode) && ScanApiPigeonInternal.deepEquals(lhs.timeoutMs, rhs.timeoutMs) && ScanApiPigeonInternal.deepEquals(lhs.torchEnabled, rhs.torchEnabled) && ScanApiPigeonInternal.deepEquals(lhs.title, rhs.title)
}
func hash(into hasher: inout Hasher) {
hasher.combine("ScanOptions")
ScanApiPigeonInternal.deepHash(value: mode, hasher: &hasher)
ScanApiPigeonInternal.deepHash(value: timeoutMs, hasher: &hasher)
ScanApiPigeonInternal.deepHash(value: torchEnabled, hasher: &hasher)
ScanApiPigeonInternal.deepHash(value: title, hasher: &hasher)
}
public var description: String {
return "ScanOptions(mode: \(String(describing: mode)), timeoutMs: \(String(describing: timeoutMs)), torchEnabled: \(String(describing: torchEnabled)), title: \(String(describing: title)))"
}
}
///
///
/// Generated class from Pigeon that represents data sent in messages.
struct ScanResult: Hashable, CustomStringConvertible {
///
var mode: ScanMode
///
var value: String
/// 13 `scan_succeeded`
var durationMs: Int64
/// `CODE_39` / `QR_CODE`OCR null
var rawFormat: String? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> ScanResult? {
let mode = pigeonVar_list[0] as! ScanMode
let value = pigeonVar_list[1] as! String
let durationMs = pigeonVar_list[2] as! Int64
let rawFormat: String? = nilOrValue(pigeonVar_list[3])
return ScanResult(
mode: mode,
value: value,
durationMs: durationMs,
rawFormat: rawFormat
)
}
func toList() -> [Any?] {
return [
mode,
value,
durationMs,
rawFormat,
]
}
static func == (lhs: ScanResult, rhs: ScanResult) -> Bool {
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return ScanApiPigeonInternal.deepEquals(lhs.mode, rhs.mode) && ScanApiPigeonInternal.deepEquals(lhs.value, rhs.value) && ScanApiPigeonInternal.deepEquals(lhs.durationMs, rhs.durationMs) && ScanApiPigeonInternal.deepEquals(lhs.rawFormat, rhs.rawFormat)
}
func hash(into hasher: inout Hasher) {
hasher.combine("ScanResult")
ScanApiPigeonInternal.deepHash(value: mode, hasher: &hasher)
ScanApiPigeonInternal.deepHash(value: value, hasher: &hasher)
ScanApiPigeonInternal.deepHash(value: durationMs, hasher: &hasher)
ScanApiPigeonInternal.deepHash(value: rawFormat, hasher: &hasher)
}
public var description: String {
return "ScanResult(mode: \(String(describing: mode)), value: \(String(describing: value)), durationMs: \(String(describing: durationMs)), rawFormat: \(String(describing: rawFormat)))"
}
}
private class ScanApiPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
if let enumResultAsInt = enumResultAsInt {
return ScanMode(rawValue: enumResultAsInt)
}
return nil
case 130:
return ScanOptions.fromList(self.readValue() as! [Any?])
case 131:
return ScanResult.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
}
}
private class ScanApiPigeonCodecWriter: FlutterStandardWriter {
override func writeValue(_ value: Any) {
if let value = value as? ScanMode {
super.writeByte(129)
super.writeValue(value.rawValue)
} else if let value = value as? ScanOptions {
super.writeByte(130)
super.writeValue(value.toList())
} else if let value = value as? ScanResult {
super.writeByte(131)
super.writeValue(value.toList())
} else {
super.writeValue(value)
}
}
}
private class ScanApiPigeonCodecReaderWriter: FlutterStandardReaderWriter {
override func reader(with data: Data) -> FlutterStandardReader {
return ScanApiPigeonCodecReader(data: data)
}
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
return ScanApiPigeonCodecWriter(data: data)
}
}
class ScanApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
static let shared = ScanApiPigeonCodec(readerWriter: ScanApiPigeonCodecReaderWriter())
}
/// Dart
///
/// `FlutterError`
/// Dart API `NativeScanException`**
/// `PlatformException`**07 §使
///
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol ScanHostApi {
///
///
/// code `CANCELLED` null
///
func startScan(options: ScanOptions, completion: @escaping (Result<ScanResult, Error>) -> Void)
///
///
///
/// `startScan`
func isModeSupported(mode: ScanMode) throws -> Bool
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class ScanHostApiSetup {
static var codec: FlutterStandardMessageCodec { ScanApiPigeonCodec.shared }
/// Sets up an instance of `ScanHostApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ScanHostApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
///
///
/// code `CANCELLED` null
///
let startScanChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.native_scan.ScanHostApi.startScan\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
startScanChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let optionsArg = args[0] as! ScanOptions
api.startScan(options: optionsArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
startScanChannel.setMessageHandler(nil)
}
///
///
///
/// `startScan`
let isModeSupportedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.native_scan.ScanHostApi.isModeSupported\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
isModeSupportedChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let modeArg = args[0] as! ScanMode
do {
let result = try api.isModeSupported(mode: modeArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
isModeSupportedChannel.setMessageHandler(nil)
}
}
}
+98
View File
@@ -0,0 +1,98 @@
/// 扫码能力的对外 API。来源:conti-docs/07-native-integration.md。
///
/// **调用方只允许 import 这个文件**,不允许直接 import `src/generated/` 里的
/// 生成代码(07 §使用规则)。调用方包括 `feature_scan` 和 `core_webview` 的
/// JSBridge——后者正是 01 里「`core_*` 允许依赖 `native_*`」这条例外存在的
/// 原因。
///
/// 本包按 01 的硬约束**不依赖仓库内任何其他包**(连 `core_foundation` 也不),
/// 所以这里抛的是包内自定义的 [NativeScanException];转成统一错误体系里的
/// `NativeException` 由调用方完成。
library;
import 'package:flutter/services.dart';
import 'src/generated/scan_api.g.dart';
export 'src/generated/scan_api.g.dart' show ScanMode, ScanOptions, ScanResult;
/// 扫码失败的错误码。
///
/// 取值与 12 的 `NativeException.code` 对齐,调用方可以直接透传。
abstract final class NativeScanErrorCode {
/// 用户主动取消。
///
/// 这不是异常流程,调用方通常应当静默返回,**不弹错误提示、不上报**。
static const String cancelled = 'CANCELLED';
/// 相机权限被拒绝。
static const String permissionDenied = 'PERMISSION_DENIED';
/// 能力暂时不可用(相机被占用、初始化失败等)。
static const String unavailable = 'UNAVAILABLE';
/// 当前平台没有实现。
///
/// 见 07 §「OHOS 后续演进」:**不允许静默返回空值或占位假数据**——
/// 静默返回会让"这个平台其实没实现"的问题一直藏到用户手里。
static const String unsupportedPlatform = 'UNSUPPORTED_PLATFORM';
/// 超时。
static const String timeout = 'TIMEOUT';
}
/// 扫码相关的异常。
class NativeScanException implements Exception {
/// [code] 取自 [NativeScanErrorCode]。
const NativeScanException(this.code, this.message);
/// 稳定错误码。
final String code;
/// 面向开发者的描述。**不要直接展示给用户**——文案由调用方按 12 的
/// `ErrorPresenter` 决定。
final String message;
/// 是否是用户主动取消。
bool get isCancelled => code == NativeScanErrorCode.cancelled;
@override
String toString() => 'NativeScanException($code): $message';
}
/// 扫码。
class NativeScan {
/// [api] 仅供测试注入;生产走默认实例。
NativeScan({ScanHostApi? api}) : _api = api ?? ScanHostApi();
final ScanHostApi _api;
/// 打开扫码页并等待一次结果。
///
/// 用户取消时抛 [NativeScanException]`code == CANCELLED`)而不是返回 null——
/// 「取消」和「扫到了空字符串」必须能区分开。
Future<ScanResult> startScan(ScanOptions options) async {
try {
return await _api.startScan(options);
} on PlatformException catch (e) {
// 原生异常类型不外泄(07 §使用规则)。
throw NativeScanException(e.code, e.message ?? '扫码失败');
} on MissingPluginException {
throw const NativeScanException(NativeScanErrorCode.unsupportedPlatform, '当前平台未实现扫码能力');
}
}
/// 当前平台是否支持指定识别类型。
///
/// 调用方应当先查这个再决定要不要显示入口,而不是等 [startScan] 抛错——
/// 车牌识别的技术路径尚未确定(07 待确认项),首版可能只有部分平台支持。
Future<bool> isModeSupported(ScanMode mode) async {
try {
return await _api.isModeSupported(mode);
} on PlatformException {
return false;
} on MissingPluginException {
return false;
}
}
}
@@ -0,0 +1,335 @@
// Autogenerated from Pigeon (v27.3.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: unused_import, unused_shown_name
// ignore_for_file: type=lint
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List;
import 'package:flutter/services.dart';
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
Object? _extractReplyValueOrThrow(
List<Object?>? replyList,
String channelName, {
required bool isNullValid,
}) {
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
}
return replyList.firstOrNull;
}
bool _deepEquals(Object? a, Object? b) {
if (identical(a, b)) {
return true;
}
if (a is double && b is double) {
if (a.isNaN && b.isNaN) {
return true;
}
return a == b;
}
if (a is List && b is List) {
return a.length == b.length &&
a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
if (a.length != b.length) {
return false;
}
for (final MapEntry<Object?, Object?> entryA in a.entries) {
bool found = false;
for (final MapEntry<Object?, Object?> entryB in b.entries) {
if (_deepEquals(entryA.key, entryB.key)) {
if (_deepEquals(entryA.value, entryB.value)) {
found = true;
break;
} else {
return false;
}
}
}
if (!found) {
return false;
}
}
return true;
}
return a == b;
}
int _deepHash(Object? value) {
if (value is List) {
return Object.hashAll(value.map(_deepHash));
}
if (value is Map) {
int result = 0;
for (final MapEntry<Object?, Object?> entry in value.entries) {
result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value);
}
return result;
}
if (value is double && value.isNaN) {
// Normalize NaN to a consistent hash.
return 0x7FF8000000000000.hashCode;
}
if (value is double && value == 0.0) {
// Normalize -0.0 to 0.0 so they have the same hash code.
return 0.0.hashCode;
}
return value.hashCode;
}
/// 识别类型。
///
/// **即使首版只做条码,这个参数也必须先留出来**(07 §「待确认:VIN 码与车牌
/// 识别的技术路径」):车牌走的是专用 OCR、VIN 印刷字符走通用 OCR + 校验位
/// 过滤,技术路径还没定。参数先在 schema 里占好位,后面加识别类型就不用改
/// 接口签名——改签名意味着三端生成物和所有调用点一起动。
enum ScanMode {
/// 二维码 / 条形码(商品、库位)。
barcode,
/// VIN 码。可能是 Code 39 条码,也可能只有印刷字符。
vin,
/// 车牌。
plate,
}
/// 扫码入参。
class ScanOptions {
ScanOptions({required this.mode, this.timeoutMs, this.torchEnabled, this.title});
/// 识别类型。
ScanMode mode;
/// 超时毫秒数。null 表示不超时,由用户手动取消。
int? timeoutMs;
/// 是否默认打开闪光灯。
bool? torchEnabled;
/// 扫码页标题。由调用方传,`native_scan` 不依赖任何 i18n 资源。
String? title;
List<Object?> _toList() {
return <Object?>[mode, timeoutMs, torchEnabled, title];
}
Object encode() {
return _toList();
}
static ScanOptions decode(Object result) {
result as List<Object?>;
return ScanOptions(
mode: result[0]! as ScanMode,
timeoutMs: result[1] as int?,
torchEnabled: result[2] as bool?,
title: result[3] as String?,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! ScanOptions || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(mode, other.mode) &&
_deepEquals(timeoutMs, other.timeoutMs) &&
_deepEquals(torchEnabled, other.torchEnabled) &&
_deepEquals(title, other.title);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
@override
String toString() {
return 'ScanOptions(mode: $mode, timeoutMs: $timeoutMs, torchEnabled: $torchEnabled, title: $title)';
}
}
/// 扫码结果。
class ScanResult {
ScanResult({required this.mode, required this.value, required this.durationMs, this.rawFormat});
/// 实际生效的识别类型。
ScanMode mode;
/// 识别到的文本。
String value;
/// 从打开扫码页到出结果的耗时,供埋点用(见 13 的 `scan_succeeded`)。
int durationMs;
/// 原始码制(如 `CODE_39` / `QR_CODE`)。OCR 路径下为 null。
String? rawFormat;
List<Object?> _toList() {
return <Object?>[mode, value, durationMs, rawFormat];
}
Object encode() {
return _toList();
}
static ScanResult decode(Object result) {
result as List<Object?>;
return ScanResult(
mode: result[0]! as ScanMode,
value: result[1]! as String,
durationMs: result[2]! as int,
rawFormat: result[3] as String?,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! ScanResult || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(mode, other.mode) &&
_deepEquals(value, other.value) &&
_deepEquals(durationMs, other.durationMs) &&
_deepEquals(rawFormat, other.rawFormat);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
@override
String toString() {
return 'ScanResult(mode: $mode, value: $value, durationMs: $durationMs, rawFormat: $rawFormat)';
}
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is ScanMode) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is ScanOptions) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is ScanResult) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
final value = readValue(buffer) as int?;
return value == null ? null : ScanMode.values[value];
case 130:
return ScanOptions.decode(readValue(buffer)!);
case 131:
return ScanResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
/// Dart → 原生。
///
/// 用户取消、权限拒绝、平台未实现这三类都通过 `FlutterError` 抛出,
/// 由 Dart 侧的公共 API 转成 `NativeScanException`——**原生异常类型
/// `PlatformException`)不允许直接抛到业务代码里**(07 §使用规则)。
class ScanHostApi {
/// Constructor for [ScanHostApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
ScanHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
/// 打开扫码页并等待一次结果。
///
/// 用户取消时抛 code 为 `CANCELLED` 的错误,而不是返回 null——
/// 「取消」和「扫到了空字符串」必须能区分开。
Future<ScanResult> startScan(ScanOptions options) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.native_scan.ScanHostApi.startScan$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[options]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as ScanResult;
}
/// 当前平台是否支持指定识别类型。
///
/// 车牌识别的技术路径未定(见上),首版可能只有部分平台支持;
/// 调用方应当先查这个再决定要不要显示入口,而不是等 `startScan` 抛错。
Future<bool> isModeSupported(ScanMode mode) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.native_scan.ScanHostApi.isModeSupported$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[mode]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as bool;
}
}
@@ -0,0 +1,93 @@
// 唯一手写的接口契约文件。来源:07 §「Pigeon 的工程化」。
//
// 这不是可执行代码,只是给 pigeon 生成器读的 schema。改需求就改这里重新生成,
// **生成产物不手动修改**
// dart run pigeon --input pigeons/scan_api.dart
// (或 melos run gen:pigeon
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/src/generated/scan_api.g.dart',
dartOptions: DartOptions(),
kotlinOut: 'android/src/main/kotlin/com/conti/native_scan/ScanApi.g.kt',
kotlinOptions: KotlinOptions(package: 'com.conti.native_scan'),
swiftOut: 'ios/native_scan/Sources/native_scan/ScanApi.g.swift',
swiftOptions: SwiftOptions(),
dartPackageName: 'native_scan',
),
)
library;
import 'package:pigeon/pigeon.dart';
/// 识别类型。
///
/// **即使首版只做条码,这个参数也必须先留出来**(07 §「待确认:VIN 码与车牌
/// 识别的技术路径」):车牌走的是专用 OCR、VIN 印刷字符走通用 OCR + 校验位
/// 过滤,技术路径还没定。参数先在 schema 里占好位,后面加识别类型就不用改
/// 接口签名——改签名意味着三端生成物和所有调用点一起动。
enum ScanMode {
/// 二维码 / 条形码(商品、库位)。
barcode,
/// VIN 码。可能是 Code 39 条码,也可能只有印刷字符。
vin,
/// 车牌。
plate,
}
/// 扫码入参。
class ScanOptions {
ScanOptions({required this.mode, this.timeoutMs, this.torchEnabled, this.title});
/// 识别类型。
ScanMode mode;
/// 超时毫秒数。null 表示不超时,由用户手动取消。
int? timeoutMs;
/// 是否默认打开闪光灯。
bool? torchEnabled;
/// 扫码页标题。由调用方传,`native_scan` 不依赖任何 i18n 资源。
String? title;
}
/// 扫码结果。
class ScanResult {
ScanResult({required this.mode, required this.value, required this.durationMs, this.rawFormat});
/// 实际生效的识别类型。
ScanMode mode;
/// 识别到的文本。
String value;
/// 从打开扫码页到出结果的耗时,供埋点用(见 13 的 `scan_succeeded`)。
int durationMs;
/// 原始码制(如 `CODE_39` / `QR_CODE`)。OCR 路径下为 null。
String? rawFormat;
}
/// Dart → 原生。
///
/// 用户取消、权限拒绝、平台未实现这三类都通过 `FlutterError` 抛出,
/// 由 Dart 侧的公共 API 转成 `NativeScanException`——**原生异常类型
/// `PlatformException`)不允许直接抛到业务代码里**(07 §使用规则)。
@HostApi()
abstract class ScanHostApi {
/// 打开扫码页并等待一次结果。
///
/// 用户取消时抛 code 为 `CANCELLED` 的错误,而不是返回 null——
/// 「取消」和「扫到了空字符串」必须能区分开。
@async
ScanResult startScan(ScanOptions options);
/// 当前平台是否支持指定识别类型。
///
/// 车牌识别的技术路径未定(见上),首版可能只有部分平台支持;
/// 调用方应当先查这个再决定要不要显示入口,而不是等 `startScan` 抛错。
bool isModeSupported(ScanMode mode);
}
+32
View File
@@ -0,0 +1,32 @@
name: native_scan
description: 扫码能力(条码 / VIN / 车牌)。Pigeon 定义跨语言接口。
publish_to: none
version: 0.1.0
resolution: workspace
# ---------------------------------------------------------------------------
# native_* 的硬约束(01):只依赖 Flutter SDK 和 Pigeon 产物,
# **不依赖仓库内任何包**——包括 core_foundation。
# 因此本包抛的是包内自定义的 NativeScanException,由调用方转成 NativeException。
# ---------------------------------------------------------------------------
environment:
sdk: ^3.12.0
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
pigeon: ^27.3.0
flutter:
plugin:
platforms:
android:
package: com.conti.native_scan
pluginClass: NativeScanPlugin
ios:
pluginClass: NativeScanPlugin
@@ -0,0 +1,60 @@
// 唯一值得单测的东西:原生异常有没有被挡在包边界内(07 §使用规则)。
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:native_scan/native_scan.dart';
import 'package:native_scan/src/generated/scan_api.g.dart';
// pigeon 给 @HostApi 生成的是具体类(内部持有 BinaryMessenger),
// 所以这里只能 extends 覆写方法,不能 implements。
class _ThrowingApi extends ScanHostApi {
_ThrowingApi(this.error);
final Object error;
@override
Future<ScanResult> startScan(ScanOptions options) => Future<ScanResult>.error(error);
@override
Future<bool> isModeSupported(ScanMode mode) => Future<bool>.error(error);
}
void main() {
final ScanOptions options = ScanOptions(mode: ScanMode.barcode);
test('PlatformException 被转成 NativeScanException,原生类型不外泄', () async {
final NativeScan scan = NativeScan(
api: _ThrowingApi(PlatformException(code: 'CANCELLED', message: '用户取消')),
);
await expectLater(
scan.startScan(options),
throwsA(
isA<NativeScanException>()
.having((NativeScanException e) => e.code, 'code', NativeScanErrorCode.cancelled)
.having((NativeScanException e) => e.isCancelled, 'isCancelled', isTrue),
),
);
});
test('原生未实现时抛 UNSUPPORTED_PLATFORM,而不是静默返回', () async {
final NativeScan scan = NativeScan(api: _ThrowingApi(MissingPluginException()));
await expectLater(
scan.startScan(options),
throwsA(
isA<NativeScanException>().having(
(NativeScanException e) => e.code,
'code',
NativeScanErrorCode.unsupportedPlatform,
),
),
);
});
test('isModeSupported 出错时降级为 false,调用方据此隐藏入口', () async {
final NativeScan scan = NativeScan(api: _ThrowingApi(MissingPluginException()));
expect(await scan.isModeSupported(ScanMode.plate), isFalse);
});
}