فهرست منبع

Merge branch 'feture/scanner' into develop

cc12458 1 ماه پیش
والد
کامیت
7cbd94de33
23فایلهای تغییر یافته به همراه445 افزوده شده و 79 حذف شده
  1. 1 0
      .idea/gradle.xml
  2. 1 0
      app/build.gradle.kts
  3. 29 21
      app/src/main/java/com/hzliuzhi/applet/container/MainActivity.kt
  4. 0 7
      app/src/main/java/com/hzliuzhi/applet/container/scanner/ScanResult.kt
  5. 0 40
      app/src/main/java/com/hzliuzhi/applet/container/scanner/ScannerListener.kt
  6. 1 1
      core/src/main/java/com/hzliuzhi/applet/core/shared/Payload.kt
  7. 58 3
      library/browser/src/main/assets/browser/bridge.js
  8. 1 1
      library/browser/src/main/java/com/hzliuzhi/applet/browser/print/PrintEventHandler.kt
  9. 7 4
      library/browser/src/main/java/com/hzliuzhi/applet/browser/webview/WebViewBridge.kt
  10. 1 2
      library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/PulseEventHandler.kt
  11. 1 0
      library/device/scanner/.gitignore
  12. 42 0
      library/device/scanner/build.gradle.kts
  13. 0 0
      library/device/scanner/consumer-rules.pro
  14. 21 0
      library/device/scanner/proguard-rules.pro
  15. 24 0
      library/device/scanner/src/androidTest/java/com/hzliuzhi/applet/scanner/ExampleInstrumentedTest.kt
  16. 4 0
      library/device/scanner/src/main/AndroidManifest.xml
  17. 14 0
      library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/ScanParams.kt
  18. 12 0
      library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/ScanResult.kt
  19. 123 0
      library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/Scanner.kt
  20. 12 0
      library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/ScannerImpl.kt
  21. 75 0
      library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/impl/NLScanner.kt
  22. 17 0
      library/device/scanner/src/test/java/com/hzliuzhi/applet/scanner/ExampleUnitTest.kt
  23. 1 0
      settings.gradle.kts

+ 1 - 0
.idea/gradle.xml

@@ -16,6 +16,7 @@
             <option value="$PROJECT_DIR$/library/browser" />
             <option value="$PROJECT_DIR$/library/device" />
             <option value="$PROJECT_DIR$/library/device/pulse" />
+            <option value="$PROJECT_DIR$/library/device/scanner" />
           </set>
         </option>
       </GradleProjectSettings>

+ 1 - 0
app/build.gradle.kts

@@ -111,5 +111,6 @@ dependencies {
   implementation(project(":core"))
   implementation(project(":library:browser"))
   implementation(project(":library:device:pulse"))
+  implementation(project(":library:device:scanner"))
   implementation(libs.gson)
 }

+ 29 - 21
app/src/main/java/com/hzliuzhi/applet/container/MainActivity.kt

@@ -3,6 +3,7 @@ package com.hzliuzhi.applet.container
 import android.annotation.SuppressLint
 import android.os.Bundle
 import android.view.KeyEvent
+import android.widget.Toast
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.LocalActivity
 import androidx.activity.compose.setContent
@@ -11,21 +12,19 @@ import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.Scaffold
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.navigation.NavHostController
 import androidx.navigation.compose.rememberNavController
 import com.hzliuzhi.applet.browser.webview.WebViewBridge
 import com.hzliuzhi.applet.container.navigation.Host
-import com.hzliuzhi.applet.container.scanner.ScannerListener
-import com.hzliuzhi.applet.container.scanner.rememberScannerListener
-import com.hzliuzhi.applet.core.shared.Event
 import com.hzliuzhi.applet.core.shared.Payload
 import com.hzliuzhi.applet.core.shared.SharedFlowHub
 import com.hzliuzhi.applet.core.theme.SixTheme
-import com.hzliuzhi.applet.device.pulse.PulseEventHandle
+import com.hzliuzhi.applet.scanner.Scanner
 
 class MainActivity : ComponentActivity() {
   private var navController: NavHostController? = null
-  private var scanner: ScannerListener? = null
 
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
@@ -42,29 +41,38 @@ class MainActivity : ComponentActivity() {
         }
       }
 
-      LocalActivity.current?.also {
-        scanner = rememberScannerListener { result ->
-          val code = result.code
-          code.takeIf { code.isNotEmpty() }?.also {
-            val message = Payload.data(data = result).toEvent()?.let { payload ->
-              WebViewBridge.Message(type = "scan", payload = payload)
-            }?.let { message ->
-              Event<WebViewBridge.Message, String>(
-                type = "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:js",
-                payload = message
-              )
-            }?.let { event ->
-              SharedFlowHub.emit(event)
-            }
+      val context = LocalContext.current
+      val owner = LocalLifecycleOwner.current
+      Scanner.getInstance(context).observe(owner) { result ->
+        if (result == null) {
+          Payload.error<Unit>(message = "扫码失败: 结果为空").toJson()
+        } else {
+          Toast.makeText(context, result.message, Toast.LENGTH_SHORT).show()
+          when {
+            result.code.startsWith("six:") -> null
+            else -> Payload.data(data = result).toJson()
+          }
+        }?.also {
+          val event = WebViewBridge.Message(type = "scan", payload = it).toEvent()
+          SharedFlowHub.emit(event)
+        }
+      }
+      Scanner.getInstance(context).eventHandle(rememberCoroutineScope())
+
+      val tag = context.resources.getString(R.string.app_id)
+      when {
+        tag.startsWith("AIO") -> run {
+          LocalActivity.current?.also {
+            com.hzliuzhi.applet.device.pulse.PulseEventHandle(it, rememberCoroutineScope())
           }
         }
-        PulseEventHandle(it, rememberCoroutineScope())
       }
     }
   }
 
   @SuppressLint("RestrictedApi")
   override fun dispatchKeyEvent(event: KeyEvent): Boolean {
-    return scanner?.dispatchKeyEvent(event)?.takeIf { it } ?: super.dispatchKeyEvent(event)
+    val context = applicationContext
+    return Scanner.getInstance(context).dispatchKeyEvent(event).takeIf { it } ?: super.dispatchKeyEvent(event)
   }
 }

+ 0 - 7
app/src/main/java/com/hzliuzhi/applet/container/scanner/ScanResult.kt

@@ -1,7 +0,0 @@
-package com.hzliuzhi.applet.container.scanner
-
-data class ScanResult(
-  val code: String,
-  val state: Int = 0,
-  val type: Int = -1,
-)

+ 0 - 40
app/src/main/java/com/hzliuzhi/applet/container/scanner/ScannerListener.kt

@@ -1,40 +0,0 @@
-package com.hzliuzhi.applet.container.scanner
-
-import android.view.KeyEvent
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-
-class ScannerListener(
-  private val interval: Long = 50L,
-  private val onResult: (ScanResult) -> Unit,
-) {
-  private val scanBuffer = StringBuilder()
-  private var lastInputTime = 0L
-
-  fun dispatchKeyEvent(event: KeyEvent): Boolean {
-    if (event.action == KeyEvent.ACTION_DOWN) {
-      val now = System.currentTimeMillis()
-      if (now - lastInputTime > interval) scanBuffer.clear()
-      lastInputTime = now
-
-      event.unicodeChar.takeUnless { it == 0 }?.toChar()?.also { char ->
-        if (char == '\n' || char == '\r') {
-          val code = scanBuffer.toString()
-          if (code.isNotEmpty()) {
-            onResult(ScanResult(code = code))
-            scanBuffer.clear()
-          }
-          return true;
-        } else {
-          scanBuffer.append(char)
-        }
-      }
-    }
-    return false
-  }
-}
-
-@Composable
-fun rememberScannerListener(onResult: (ScanResult) -> Unit): ScannerListener {
-  return remember { ScannerListener { onResult(it) } }
-}

+ 1 - 1
core/src/main/java/com/hzliuzhi/applet/core/shared/Payload.kt

@@ -11,7 +11,7 @@ data class Payload<D>(val data: D? = null, val code: Int, val message: String?)
     fun <D> data(data: D, message: String? = null) = Payload(code = 0, data = data, message = message)
   }
 
-  fun toEvent(): JsonElement? {
+  fun toJson(): JsonElement? {
     return Gson().toJsonTree(this, Payload::class.java)
   }
 

+ 58 - 3
library/browser/src/main/assets/browser/bridge.js

@@ -25,14 +25,26 @@ class Bridge extends EventTarget {
     return promise;
   }
 
+  static scan({ signal, ...payload } = { signal: null }) {
+    const { promise, ...resolvers } = Promise.withResolvers();
+
+    signal?.addEventListener('abort', () => {
+      this.getInstance().#postMessage('scan:stop', null, resolvers);
+      resolvers.reject({message: '取消扫码'})
+    }, { once: true, signal });
+
+    this.getInstance().#postMessage('scan:start', payload, resolvers);
+    return promise;
+  }
+
   dispatch(message) {
     try {
       const { type, callbackId, payload }  = JSON.parse(message);
       if (callbackId) {
         const { resolve, reject } = this.#pool.get(callbackId) ?? {};
         this.#pool.delete(callbackId);
-        if (payload.code === 0) resolve(payload.data)
-        else reject(payload.message)
+        if (payload.code === 0) resolve?.(payload.data)
+        else reject?.(payload.message)
       } else {
         event = new CustomEvent(type, { detail: payload });
         super.dispatchEvent(event);
@@ -59,4 +71,47 @@ class Bridge extends EventTarget {
 window['Bridge'] = Bridge;
 window['bridge'] = Bridge.getInstance();
 
-window.print = Bridge.print.bind(Bridge);
+window.print = Bridge.print.bind(Bridge);
+
+// polyfill.js
+
+if (Crypto && typeof Crypto.prototype.randomUUID !== 'function') {
+  if (typeof Crypto.prototype.getRandomValues === 'function' && typeof Uint8Array === 'function') {
+    Crypto.prototype.randomUUID = function () {
+      return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, function (c) {
+        const num = Number(c);
+        return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16);
+      });
+    };
+  } else {
+    Crypto.prototype.randomUUID = function () {
+      let timestamp = new Date().getTime();
+      let per = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0;
+      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+        let random = Math.random() * 16;
+        if (timestamp > 0) {
+          random = (timestamp + random) % 16 | 0;
+          timestamp = Math.floor(timestamp / 16);
+        } else {
+          random = (per + random) % 16 | 0;
+          per = Math.floor(per / 16);
+        }
+        return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16);
+      });
+    };
+  }
+}
+
+if (typeof Promise.withResolvers !== 'function') {
+  Promise.withResolvers = function () {
+    let resolve;
+    let reject;
+
+    const promise = new Promise((res, rej) => {
+      resolve = res;
+      reject = rej;
+    });
+
+    return { promise, resolve, reject };
+  };
+}

+ 1 - 1
library/browser/src/main/java/com/hzliuzhi/applet/browser/print/PrintEventHandler.kt

@@ -46,7 +46,7 @@ class PrintEventHandle(webView: WebView, scope: CoroutineScope) {
               val print = event.payload?.let { it -> Print.formJson(it) }
               val callback = event.callback ?: {}
               handlePrint(webView, print) { payload ->
-                payload.toEvent()?.also { callback(it) }
+                payload.toJson()?.also { callback(it) }
               }
             }
           }

+ 7 - 4
library/browser/src/main/java/com/hzliuzhi/applet/browser/webview/WebViewBridge.kt

@@ -39,6 +39,11 @@ class WebViewBridge(private val coroutineScope: CoroutineScope) {
         null
       }
     }
+
+    fun toEvent() = Event<Message, String>(
+      type = "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:js",
+      payload = this
+    )
   }
 
 
@@ -79,10 +84,8 @@ class WebViewBridge(private val coroutineScope: CoroutineScope) {
       type = "$WEBVIEW_BRIDGE_EVENT:${message.type}",
       payload = message.payload,
       callback = { payload ->
-        Event<Message, String>(
-          type = "$WEBVIEW_BRIDGE_EVENT:js",
-          payload = message.copy(payload = payload)
-        ).also { emit(it) }
+        val event = message.copy(payload = payload).toEvent()
+        emit(event)
       }
     ).also { emit(it) }
   }

+ 1 - 2
library/device/pulse/src/main/java/com/hzliuzhi/applet/device/pulse/PulseEventHandler.kt

@@ -1,7 +1,6 @@
 package com.hzliuzhi.applet.device.pulse
 
 import android.app.Activity
-import android.util.Log
 import com.google.gson.Gson
 import com.google.gson.JsonElement
 import com.hzliuzhi.applet.core.shared.Payload
@@ -39,7 +38,7 @@ class PulseEventHandle(private val activity: Activity, scope: CoroutineScope) {
               val pulse = event.payload?.let { it -> Pulse.formJson(it) }
               val callback = event.callback ?: {}
               handlePulse(pulse) { payload ->
-                payload.copyWith(data = payload.data?.toMap()).toEvent()?.also { callback(it) }
+                payload.copyWith(data = payload.data?.toMap()).toJson()?.also { callback(it) }
               }
             }
           }

+ 1 - 0
library/device/scanner/.gitignore

@@ -0,0 +1 @@
+/build

+ 42 - 0
library/device/scanner/build.gradle.kts

@@ -0,0 +1,42 @@
+plugins {
+  alias(libs.plugins.android.library)
+  alias(libs.plugins.kotlin.android)
+}
+
+android {
+  namespace = "com.hzliuzhi.applet.scanner"
+  compileSdk = 35
+
+  defaultConfig {
+    minSdk = 26
+
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    consumerProguardFiles("consumer-rules.pro")
+  }
+
+  buildTypes {
+    release {
+      isMinifyEnabled = false
+      proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+    }
+  }
+  compileOptions {
+    sourceCompatibility = JavaVersion.VERSION_11
+    targetCompatibility = JavaVersion.VERSION_11
+  }
+  kotlinOptions {
+    jvmTarget = "11"
+  }
+}
+
+dependencies {
+
+  implementation(libs.androidx.core.ktx)
+  implementation(libs.androidx.appcompat)
+  testImplementation(libs.junit)
+  androidTestImplementation(libs.androidx.junit)
+  androidTestImplementation(libs.androidx.espresso.core)
+
+  implementation(project(":core"))
+  implementation(libs.gson)
+}

+ 0 - 0
library/device/scanner/consumer-rules.pro


+ 21 - 0
library/device/scanner/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 24 - 0
library/device/scanner/src/androidTest/java/com/hzliuzhi/applet/scanner/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.hzliuzhi.applet.scanner
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+  @Test
+  fun useAppContext() {
+    // Context of the app under test.
+    val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+    assertEquals("com.hzliuzhi.applet.scanner.test", appContext.packageName)
+  }
+}

+ 4 - 0
library/device/scanner/src/main/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+</manifest>

+ 14 - 0
library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/ScanParams.kt

@@ -0,0 +1,14 @@
+package com.hzliuzhi.applet.scanner
+
+import com.google.gson.Gson
+import com.google.gson.JsonElement
+
+data class ScanParams(
+  val timeout: Int = 3,
+) {
+  companion object {
+    fun formJson(element: JsonElement): ScanParams {
+      return Gson().fromJson(element, ScanParams::class.java)
+    }
+  }
+}

+ 12 - 0
library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/ScanResult.kt

@@ -0,0 +1,12 @@
+package com.hzliuzhi.applet.scanner
+
+data class ScanResult(
+  val code: String,
+  val state: Int = 0,
+  val type: Int = -1,
+  val message: String? = null,
+) {
+  companion object {
+    fun timeout() = ScanResult(code = "", state = -1, message = "扫码失败: 超时")
+  }
+}

+ 123 - 0
library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/Scanner.kt

@@ -0,0 +1,123 @@
+package com.hzliuzhi.applet.scanner
+
+import android.content.Context
+import android.os.Build
+import android.view.KeyEvent
+import androidx.lifecycle.LiveData
+import com.google.gson.JsonElement
+import com.hzliuzhi.applet.core.shared.Payload
+import com.hzliuzhi.applet.core.shared.SharedFlowHub
+import com.hzliuzhi.applet.core.shared.SharedFlowHub.callbackAs
+import com.hzliuzhi.applet.core.shared.SharedFlowHub.cast
+import com.hzliuzhi.applet.scanner.impl.NLScanner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+class Scanner private constructor(context: Context, private val impl: ScannerImpl) : LiveData<ScanResult?>() {
+  private val applicationContext = context.applicationContext
+  private var receiver: () -> Unit = {}
+
+  private var callback: ((JsonElement) -> Unit)? = null
+  private var scanTimeout: Job? = null
+
+  override fun onActive() {
+    super.onActive()
+    receiver = impl.registerReceiver(applicationContext) {
+      scanTimeout?.cancel()
+      if (callback == null) it?.takeIf { it.code.isNotEmpty() }.also { value = it }
+      else Payload.data(it, message = "[scan:start] 扫码开始").toJson()?.let { it1 -> callback?.invoke(it1) }
+
+      scanTimeout = null
+      callback = null
+    }
+  }
+
+  override fun onInactive() {
+    super.onInactive()
+    receiver()
+  }
+
+  fun eventHandle(scope: CoroutineScope) {
+    SharedFlowHub.events
+      .filter { it.type.contains("scan") }
+      .onEach { event ->
+        when (event.type) {
+          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:scan:start" -> run {
+            event.cast<JsonElement, JsonElement>()?.also { event ->
+              callback = event.callback
+
+              event.payload
+                ?.let { ScanParams.formJson(it) }
+                ?.let { impl.start(applicationContext, it) }
+                ?.also { seconds ->
+                  scanTimeout?.cancel()
+                  scanTimeout = scope.launch {
+                    delay(seconds * 1000L + 500L)
+                    Payload.data(ScanResult.timeout()).toJson()?.also { callback?.invoke(it) }
+                    callback = null
+                  }
+                } ?: run {
+                Payload.error<Unit>(message = "[scan:start] 参数解析错误").toJson()?.also { callback?.invoke(it) }
+                callback = null
+              }
+            }
+          }
+
+          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:scan:stop" -> run {
+            event.cast<JsonElement, JsonElement>()?.also { event ->
+              impl.stop(applicationContext).also { callback = null }
+              Payload(data = null, code = 0, message = "[scan:stop] 扫码停止成功").toJson()?.also { event.callback?.invoke(it) }
+            }
+          }
+
+          else -> Payload.error<Unit>(message = "[scan] 未实现 ${event.type}").toJson()?.also { event.callbackAs<JsonElement>()?.invoke(it) }
+        }
+      }
+      .launchIn(scope)
+  }
+
+  companion object {
+    @Volatile
+    private var instance: Scanner? = null
+    fun getInstance(context: Context): Scanner {
+      return instance ?: synchronized(this) {
+        instance ?: run {
+          val impl = when {
+            Build.MODEL.startsWith(NLScanner.MODEL_PREFIX) -> NLScanner()
+            else -> ScannerImpl()
+          }
+          Scanner(context.applicationContext, impl).also { instance = it }
+        }
+      }
+    }
+  }
+
+  private val scanBuffer = StringBuilder()
+  private var lastInputTime = 0L
+  fun dispatchKeyEvent(event: KeyEvent, interval: Long = 50L): Boolean {
+    if (event.action == KeyEvent.ACTION_DOWN) {
+      val now = System.currentTimeMillis()
+      if (now - lastInputTime > interval) scanBuffer.clear()
+      lastInputTime = now
+
+      event.unicodeChar.takeUnless { it == 0 }?.toChar()?.also { char ->
+        if (char == '\n' || char == '\r') {
+          val code = scanBuffer.toString()
+          if (code.isNotEmpty()) {
+            value = ScanResult(code = code, message = "扫码成功: $code")
+            scanBuffer.clear()
+          }
+          return true
+        } else {
+          scanBuffer.append(char)
+        }
+      }
+    }
+    return false
+  }
+}

+ 12 - 0
library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/ScannerImpl.kt

@@ -0,0 +1,12 @@
+package com.hzliuzhi.applet.scanner
+
+import android.content.Context
+
+open class ScannerImpl {
+  open fun registerReceiver(context: Context, onResult: (ScanResult?) -> Unit): () -> Unit {
+    return {}
+  }
+
+  open fun start(context: Context, params: ScanParams) = 0
+  open fun stop(context: Context) {}
+}

+ 75 - 0
library/device/scanner/src/main/java/com/hzliuzhi/applet/scanner/impl/NLScanner.kt

@@ -0,0 +1,75 @@
+package com.hzliuzhi.applet.scanner.impl
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.core.content.ContextCompat
+import com.hzliuzhi.applet.scanner.ScanParams
+import com.hzliuzhi.applet.scanner.ScanResult
+import com.hzliuzhi.applet.scanner.ScannerImpl
+
+class NLScanner : ScannerImpl() {
+  override fun registerReceiver(context: Context, onResult: (ScanResult?) -> Unit): () -> Unit {
+    val filter = IntentFilter(Constants.ACTION_RESULT)
+
+    return object : BroadcastReceiver() {
+      override fun onReceive(context: Context?, intent: Intent?) {
+        val result = intent?.takeIf { it.action == Constants.ACTION_RESULT }?.let { parseResult(it) }
+        onResult(result)
+      }
+    }.let { receiver ->
+      ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_EXPORTED);
+      { context.unregisterReceiver(receiver) }
+    }
+  }
+
+  override fun start(context: Context, params: ScanParams): Int {
+    val seconds = params.timeout.coerceIn(1, 9);
+
+    Intent(Constants.ACTION_START).apply {
+      putExtra(Constants.EXTRA_SCAN_TIMEOUT, seconds)
+      context.applicationContext.sendBroadcast(this)
+    }
+
+    return seconds
+  }
+
+  override fun stop(context: Context) {
+    Intent(Constants.ACTION_END).apply {
+      context.sendBroadcast(this)
+    }
+  }
+
+
+  private fun parseResult(intent: Intent) = runCatching {
+    val state = intent.getStringExtra(Constants.EXTRA_RESULT_STATE) ?: "";
+    val code = intent.getStringExtra(Constants.EXTRA_RESULT_RESULT) ?: "";
+    val type = intent.getIntExtra(Constants.EXTRA_RESULT_TYPE, -1);
+    ScanResult(
+      code = code,
+      type = type,
+      state = state.takeIf { it == "ok" }?.let { 0 } ?: -1,
+      message = state.takeIf { it == "ok" }?.let { "扫码成功: $code" } ?: "扫码失败:$state",
+    )
+  }.getOrNull()
+
+  companion object {
+    const val MODEL_PREFIX = "NLS-" // NLS-MT90
+  }
+}
+
+private object Constants {
+  const val ACTION_RESULT = "nlscan.action.SCANNER_RESULT"
+  const val ACTION_START = "nlscan.action.SCANNER_TRIG"
+  const val ACTION_END = "nlscan.action.STOP_SCAN"
+
+  const val EXTRA_RESULT_RESULT = "SCAN_BARCODE1"
+  const val EXTRA_RESULT_TYPE = "SCAN_BARCODE_TYPE"
+  const val EXTRA_RESULT_STATE = "SCAN_STATE"
+
+  /**
+   * 扫描超时 (单位秒,默认 3s 且不超过 9s)
+   */
+  const val EXTRA_SCAN_TIMEOUT = "SCAN_TIMEOUT"
+}

+ 17 - 0
library/device/scanner/src/test/java/com/hzliuzhi/applet/scanner/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package com.hzliuzhi.applet.scanner
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+  @Test
+  fun addition_isCorrect() {
+    assertEquals(4, 2 + 2)
+  }
+}

+ 1 - 0
settings.gradle.kts

@@ -26,3 +26,4 @@ include(":app")
 include(":core")
 include(":library:browser")
 include(":library:device:pulse")
+include(":library:device:scanner")