瀏覽代碼

1. 创建 printer 模块
2. 迁移 webview 打印功能

cc12458 3 周之前
父節點
當前提交
1b0ca95fce

+ 1 - 0
.idea/gradle.xml

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

+ 1 - 0
app/build.gradle.kts

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

+ 3 - 0
app/src/main/java/com/hzliuzhi/applet/container/MainActivity.kt

@@ -18,6 +18,7 @@ import com.hzliuzhi.applet.container.navigation.Host
 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.printer.Printer
 import com.hzliuzhi.applet.scanner.Scanner
 
 class MainActivity : AndroidActivity() {
@@ -61,6 +62,8 @@ class MainActivity : AndroidActivity() {
       }
       Scanner.getInstance(context).eventHandle(rememberCoroutineScope())
 
+      LocalActivity.current?.also { Printer.getInstance(it).eventHandle(rememberCoroutineScope()) }
+
       val tag = context.resources.getString(R.string.app_id)
       when {
         tag.startsWith("AIO") -> run {

+ 0 - 14
library/browser/src/main/java/com/hzliuzhi/applet/browser/print/Print.kt

@@ -1,14 +0,0 @@
-package com.hzliuzhi.applet.browser.print
-
-import com.google.gson.Gson
-import com.google.gson.JsonElement
-
-data class Print(
-  val url: String?,
-) {
-  companion object {
-    fun formJson(element: JsonElement): Print {
-      return Gson().fromJson(element, Print::class.java)
-    }
-  }
-}

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

@@ -1,151 +0,0 @@
-package com.hzliuzhi.applet.browser.print
-
-import android.content.Context
-import android.os.Bundle
-import android.os.CancellationSignal
-import android.os.ParcelFileDescriptor
-import android.print.PageRange
-import android.print.PrintAttributes
-import android.print.PrintDocumentAdapter
-import android.print.PrintDocumentInfo
-import android.print.PrintManager
-import android.webkit.WebView
-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 kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import okhttp3.Call
-import okhttp3.Callback
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import okio.buffer
-import okio.sink
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
-import java.io.IOException
-
-class PrintEventHandle(webView: WebView, scope: CoroutineScope) {
-  private val printManager: PrintManager = webView.context.getSystemService(Context.PRINT_SERVICE) as PrintManager
-
-  init {
-    android.util.Log.d("log:bridge", "PrintEventHandle 被注入")
-    SharedFlowHub.events
-      .filter { it.type.contains("print") }
-      .onEach { _event ->
-        when (_event.type) {
-
-          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:print" -> {
-            _event.cast<JsonElement, JsonElement>()?.also { event ->
-              val print = event.payload?.let { it -> Print.formJson(it) }
-              val callback = event.callback ?: {}
-              handlePrint(webView, print) { payload ->
-                payload.toJson()?.also { callback(it) }
-              }
-            }
-          }
-
-          else -> _event.callbackAs<Payload<Unit>>()?.invoke(Payload.error(message = "[print] 未实现 ${_event.type}"))
-        }
-      }
-      .launchIn(scope)
-  }
-
-  private fun handlePrint(webView: WebView, print: Print?, callback: ((Payload<Boolean?>) -> Unit)) {
-    val attributes = PrintAttributes.Builder().build()
-
-    print?.url.takeUnless { it.isNullOrEmpty() }?.also {
-      val context = webView.context.applicationContext
-      PdfPrintAdapter.download(
-        context = context, print = print!!,
-        onFinished = { file ->
-          val jobName = "${context.getString(android.R.string.untitled)}_${file.name}"
-          val adapter = PdfPrintAdapter(file, { callback(Payload.data(true));true }, { callback(Payload.error(message = it)); true })
-          printManager.print(jobName, adapter, attributes)
-        },
-        onError = { callback(Payload.error(message = it)) }
-      )
-    } ?: webView.post {
-      val jobName = "${webView.context.getString(android.R.string.untitled)}_page"
-      val adapter = webView.createPrintDocumentAdapter("WebViewDocument")
-      printManager.print(jobName, adapter, attributes)
-      callback(Payload.data(true))
-    }
-  }
-}
-
-private class PdfPrintAdapter(
-  private val file: File,
-  private val onFinished: () -> Boolean = { true },
-  private val onError: (String) -> Boolean = { true },
-) : PrintDocumentAdapter() {
-  override fun onLayout(
-    oldAttributes: PrintAttributes?,
-    newAttributes: PrintAttributes?,
-    cancellationSignal: CancellationSignal?,
-    callback: LayoutResultCallback?,
-    extras: Bundle?,
-  ) {
-    cancellationSignal?.takeIf { it.isCanceled }?.also { callback?.onLayoutCancelled(); return }
-
-    PrintDocumentInfo.Builder(file.name).apply {
-      setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
-      setPageCount(PrintDocumentInfo.PAGE_COUNT_UNKNOWN)
-    }.build().also { callback?.onLayoutFinished(it, true) }
-  }
-
-  override fun onWrite(
-    pages: Array<out PageRange>?,
-    destination: ParcelFileDescriptor,
-    cancellationSignal: CancellationSignal?,
-    callback: WriteResultCallback?,
-  ) {
-    runCatching {
-      FileInputStream(file).use { input ->
-        FileOutputStream(destination.fileDescriptor).use { output ->
-          input.copyTo(output)
-        }
-      }
-    }.onSuccess {
-      callback?.onWriteFinished(arrayOf(PageRange.ALL_PAGES))
-    }.onFailure {
-      callback?.onWriteFailed(it.message)
-      onError("打印错误: ${it.message ?: "失败"}").takeIf { remove -> remove }?.also { file.delete() }
-    }
-  }
-
-  override fun onFinish() {
-    super.onFinish()
-    onFinished().takeIf { remove -> remove }?.also { file.delete() }
-  }
-
-  companion object {
-    fun download(context: Context, print: Print, onFinished: (File) -> Unit, onError: (String) -> Unit) {
-      val request = Request.Builder().url(print.url!!).build()
-      OkHttpClient().newCall(request).enqueue(object : Callback {
-        override fun onFailure(call: Call, e: IOException) {
-          onError("下载失败: IO 错误")
-        }
-
-        override fun onResponse(call: Call, response: Response) {
-          if (!response.isSuccessful) onError("下载失败: ${response.message}").also { return }
-          runCatching {
-            File(context.cacheDir, "temp_${System.currentTimeMillis()}.pdf").apply {
-              sink().buffer().use { sikp ->
-                response.body?.source()?.let { sikp.writeAll(it) }
-              }
-            }
-          }.onSuccess { onFinished(it) }.onFailure { onError("下载失败: ${it.message}") }
-        }
-      })
-    }
-  }
-}
-
-

+ 25 - 14
library/browser/src/main/java/com/hzliuzhi/applet/browser/webview/WebViewBridge.kt

@@ -1,12 +1,12 @@
 package com.hzliuzhi.applet.browser.webview
 
 import android.annotation.SuppressLint
+import android.print.PrintDocumentAdapter
 import android.util.Log
 import android.webkit.JavascriptInterface
 import android.webkit.WebView
 import com.google.gson.Gson
 import com.google.gson.JsonElement
-import com.hzliuzhi.applet.browser.print.PrintEventHandle
 import com.hzliuzhi.applet.core.shared.Event
 import com.hzliuzhi.applet.core.shared.SharedFlowHub
 import com.hzliuzhi.applet.core.shared.SharedFlowHub.cast
@@ -23,7 +23,7 @@ import java.io.InputStreamReader
 class WebViewBridge(private val coroutineScope: CoroutineScope) {
   data class Message(
     val type: String,
-    val payload: JsonElement = Gson().toJsonTree(""),
+    val payload: JsonElement? = Gson().toJsonTree(""),
     val callbackId: String? = null,
   ) {
     companion object {
@@ -44,6 +44,8 @@ class WebViewBridge(private val coroutineScope: CoroutineScope) {
       type = "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:js",
       payload = this
     )
+
+    val isPayloadNull: Boolean get() = payload?.isJsonNull ?: true
   }
 
 
@@ -65,8 +67,6 @@ class WebViewBridge(private val coroutineScope: CoroutineScope) {
         webview.evaluateJavascript("Bridge.getInstance().dispatch(${JSONObject.quote(payload)})", it.callback)
       }
     }.launchIn(coroutineScope)
-
-    PrintEventHandle(webview, coroutineScope)
   }
 
 
@@ -75,19 +75,30 @@ class WebViewBridge(private val coroutineScope: CoroutineScope) {
   @SuppressLint("JavascriptInterface")
   internal suspend fun WebView.handleBridge(): Nothing = withContext(Dispatchers.Main) {
     addJavascriptInterface(this@WebViewBridge, "AndroidBridge")
-    messages.collect { SharedFlowHub.webViewEmit(it) }
+    messages.collect { SharedFlowHub.webViewEmit(this@handleBridge, it) }
   }
 
-  private fun SharedFlowHub.webViewEmit(message: Message) {
+  private fun SharedFlowHub.webViewEmit(webview: WebView, message: Message) {
     Log.d("log:bridge", "接收消息: $message")
-    Event<JsonElement, JsonElement>(
-      type = "$WEBVIEW_BRIDGE_EVENT:${message.type}",
-      payload = message.payload,
-      callback = { payload ->
-        val event = message.copy(payload = payload).toEvent()
-        emit(event)
-      }
-    ).also { emit(it) }
+    if (message.type == "print" && message.isPayloadNull) {
+      Event<PrintDocumentAdapter, JsonElement>(
+        type = message.type,
+        payload = webview.createPrintDocumentAdapter("WebViewDocument"),
+        callback = { payload ->
+          val event = message.copy(payload = payload).toEvent()
+          emit(event)
+        }
+      ).also { emit(it) }
+    } else {
+      Event<JsonElement, JsonElement>(
+        type = "$WEBVIEW_BRIDGE_EVENT:${message.type}",
+        payload = message.payload,
+        callback = { payload ->
+          val event = message.copy(payload = payload).toEvent()
+          emit(event)
+        }
+      ).also { emit(it) }
+    }
   }
 
   /* 注入到 JavaScript 中的方法 */

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

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

+ 43 - 0
library/device/printer/build.gradle.kts

@@ -0,0 +1,43 @@
+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)
+  implementation(libs.okhttp)
+}

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


+ 21 - 0
library/device/printer/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/printer/src/androidTest/java/com/hzliuzhi/applet/printer/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.hzliuzhi.applet.printer
+
+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.printer.test", appContext.packageName)
+  }
+}

+ 4 - 0
library/device/printer/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>

+ 12 - 0
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/PrintParams.kt

@@ -0,0 +1,12 @@
+package com.hzliuzhi.applet.printer
+
+import com.google.gson.Gson
+import com.google.gson.JsonElement
+
+data class PrintParams(
+  val url: String?,
+) {
+  companion object {
+    fun formJson(element: JsonElement): PrintParams? = Gson().fromJson(element, PrintParams::class.java)
+  }
+}

+ 91 - 0
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/Printer.kt

@@ -0,0 +1,91 @@
+package com.hzliuzhi.applet.printer
+
+import android.app.Activity
+import android.content.Context
+import android.print.PrintAttributes
+import android.print.PrintDocumentAdapter
+import android.print.PrintManager
+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.printer.impl.PrintPdfDocumentAdapter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+class Printer(context: Context) {
+  private val applicationContext = context.applicationContext
+
+  private val manager by lazy {
+    if (context is Activity) context.getSystemService(Context.PRINT_SERVICE) as PrintManager
+    else null
+  }
+
+
+  private var callback: ((JsonElement) -> Unit)? = null
+
+  companion object {
+    @Volatile
+    private var instance: Printer? = null
+    fun getInstance(context: Context): Printer {
+      return instance ?: synchronized(this) {
+        Printer(context).also { instance = it }
+      }
+    }
+  }
+
+  fun eventHandle(scope: CoroutineScope) {
+    SharedFlowHub.events
+      .filter { it.type.contains("print") }
+      .onEach { event ->
+        when (event.type) {
+          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:print" -> run {
+            event.cast<JsonElement, JsonElement>()?.also { event ->
+              callback = event.callback
+              event.payload
+                ?.let { PrintParams.formJson(it) }
+                ?.also { print(it) }
+                ?: invoke(Payload.error<Unit>(message = "[print] 参数解析错误"))
+            }
+          }
+
+          "print" -> run {
+            event.cast<PrintDocumentAdapter, JsonElement>()?.also { event ->
+              callback = event.callback
+              event.payload?.also {
+                print(adapter = it)
+              } ?: invoke(Payload.error<Unit>(message = "[print] 参数解析错误"))
+            }
+          }
+
+          else -> event.callbackAs<Payload<Unit>>()?.invoke(Payload.error(message = "[print] 未实现 ${event.type}"))
+        }
+      }
+      .launchIn(scope)
+  }
+
+  fun print(params: PrintParams) {
+    if (!params.url.isNullOrEmpty()) {
+      PrintPdfDocumentAdapter.download(
+        applicationContext, params.url,
+        { file -> print(file.name, PrintPdfDocumentAdapter(file, { invoke(Payload.data(true));true }, { invoke(Payload.error<Unit>(message = it)); true })) },
+        { message -> Payload.error<Unit>(message = message) }
+      )
+    }
+  }
+
+
+  private fun print(jobName: String? = null, adapter: PrintDocumentAdapter, attributes: PrintAttributes? = null) {
+    manager?.print(jobName ?: "${applicationContext.getString(android.R.string.untitled)}_page", adapter, attributes) ?: run {
+      invoke(Payload.error<Unit>(message = "[print] PrintManager 未找到"))
+    }
+  }
+
+  private fun invoke(payload: Payload<*>) {
+    payload.toJson()?.also { callback?.invoke(it) }
+    callback = null
+  }
+}

+ 85 - 0
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/impl/PrintPdfDocumentAdapter.kt

@@ -0,0 +1,85 @@
+package com.hzliuzhi.applet.printer.impl
+
+
+import android.content.Context
+import android.os.Bundle
+import android.os.CancellationSignal
+import android.os.ParcelFileDescriptor
+import android.print.PageRange
+import android.print.PrintAttributes
+import android.print.PrintDocumentAdapter
+import android.print.PrintDocumentInfo
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okio.buffer
+import okio.sink
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+
+class PrintPdfDocumentAdapter(
+  private val file: File,
+  private val onFinished: () -> Boolean = { true },
+  private val onError: (String) -> Boolean = { true },
+) : PrintDocumentAdapter() {
+  override fun onLayout(
+    oldAttributes: PrintAttributes?,
+    newAttributes: PrintAttributes?,
+    cancellationSignal: CancellationSignal?,
+    callback: LayoutResultCallback?,
+    extras: Bundle?,
+  ) {
+    cancellationSignal?.takeIf { it.isCanceled }?.also { callback?.onLayoutCancelled(); return }
+
+    PrintDocumentInfo.Builder(file.name).apply {
+      setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
+      setPageCount(PrintDocumentInfo.PAGE_COUNT_UNKNOWN)
+    }.build().also { callback?.onLayoutFinished(it, true) }
+  }
+
+  override fun onWrite(pages: Array<out PageRange>?, destination: ParcelFileDescriptor, cancellationSignal: CancellationSignal?, callback: WriteResultCallback?) {
+    runCatching {
+      FileInputStream(file).use { input ->
+        FileOutputStream(destination.fileDescriptor).use { output ->
+          input.copyTo(output)
+        }
+      }
+    }.onSuccess {
+      callback?.onWriteFinished(arrayOf(PageRange.ALL_PAGES))
+    }.onFailure { throwable ->
+      callback?.onWriteFailed(throwable.message)
+      onError("打印错误: ${throwable.message ?: "失败"}").takeIf { it }?.also { file.delete() }
+    }
+  }
+
+  override fun onFinish() {
+    super.onFinish()
+    onFinished().takeIf { it }?.also { file.delete() }
+  }
+
+  companion object {
+    fun download(context: Context, url: String, onFinished: (File) -> Unit, onError: (String) -> Unit) {
+      val request = Request.Builder().url(url).build()
+      OkHttpClient().newCall(request).enqueue(object : Callback {
+        override fun onFailure(call: Call, e: IOException) {
+          onError("下载失败: IO 错误")
+        }
+
+        override fun onResponse(call: Call, response: Response) {
+          if (!response.isSuccessful) onError("下载失败: ${response.message}").also { return }
+          runCatching {
+            File(context.cacheDir, "temp_${System.currentTimeMillis()}.pdf").apply {
+              sink().buffer().use { sink ->
+                response.body?.source()?.let { sink.writeAll(it) }
+              }
+            }
+          }.onSuccess { onFinished(it) }.onFailure { onError("下载失败: ${it.message}") }
+        }
+      })
+    }
+  }
+}

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

@@ -0,0 +1,17 @@
+package com.hzliuzhi.applet.printer
+
+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,4 +26,5 @@ include(":app")
 include(":core")
 include(":library:browser")
 include(":library:device:pulse")
+include(":library:device:printer")
 include(":library:device:scanner")