PAYATHON 2026

Android 12+ 无 WiFi 时 PrintService 不显示 BT/USB 打印机

排障赵师傅

结论

addPrinters() 只会把打印机加入当前 PrinterDiscoverySession。它无法让系统打印对话框将这台打印机设为默认目标,也无法保证它固定出现在主列表中。自定义 PrintService 也不要求必须使用 WiFi,服务可以自行实现 Bluetooth、USB 或其他传输方式。

Android 的公开打印 API 不支持以下操作:

  • 强制打印机出现在主打印机列表
  • 跨打印会话预选指定打印机
  • 为 Chrome 设置默认打印机
  • 跳过系统打印对话框进行静默打印

当前代码还需要补上打印机验证逻辑。onValidatePrinters() 现在是空实现,onStartPrinterDiscovery() 收到的优先打印机列表也没有被处理,这可能影响系统重新验证历史打印机。补全这些逻辑后,最近使用的打印机可能更容易正常显示,但系统仍不一定会自动选中或固定展示它。

对于每天需要打印 50 至 100 次的 POS,建议绕过 Chrome Share → Print → Android PrintManager,改由受控的 Android 应用直接管理 Bluetooth/USB 连接和默认打印机。

为什么调用 addPrinters() 后仍未显示

Android 打印框中的“主打印机列表”不会完整列出所有已发现的打印机。哪些打印机会显示、如何排序、怎样记录最近使用情况,以及如何呈现打印服务入口,都由系统 Print Spooler 和设备厂商的打印界面决定。

以下两种状态并不相同:

  1. PrintService 已通过 addPrinters() 报告打印机。
  2. 系统把这台打印机放入当前界面的主列表并自动选中。

addPrinters() 只能完成第一项,没有“提升到主列表”或“设为默认打印机”的参数。

不同 Android 系统和厂商 ROM 可能采用不同的打印机展示策略。现有现象不足以证明 Android 12 开始正式屏蔽非 WiFi 打印机。能够确定的是,公开 API 没有提供控制这一界面的入口。

WiFi 不是 PrintService 的硬性要求

Chrome 帮助页面中关于 WiFi 或移动网络打印机的说明,主要面向普通用户,并不构成 PrintService 的传输协议限制。

Android 打印框会把文档交给用户选中的 PrintService。服务收到打印任务后,可以自行选择数据传输方式,包括:

  • Bluetooth Classic SPP/RFCOMM
  • USB Host
  • TCP/IP
  • 厂商 SDK
  • 其他私有传输协议

当前服务在用户手动选择后能够正常打印,说明 Bluetooth/USB 传输已经接入 Android 打印流程。系统没有额外配置可以把非网络打印机声明为“主列表打印机”。

补全打印机验证逻辑

系统可能会把以前发现过的 PrinterId 传给 onValidatePrinters(),让服务确认这些打印机是否仍然有效,因此这里不应留空。

onStartPrinterDiscovery() 的参数是系统希望优先发现的打印机列表。服务应先处理这些 ID,再进行普通发现。

override fun onCreatePrinterDiscoverySession(): PrinterDiscoverySession {
    return object : PrinterDiscoverySession() {

        private val savedPrinterId: PrinterId
            get() = generatePrinterId("saved_printer")

        private fun buildPrinterInfo(
            status: Int = PrinterInfo.STATUS_IDLE
        ): PrinterInfo {
            val id = savedPrinterId

            val capabilities = PrinterCapabilitiesInfo.Builder(id)
                .addMediaSize(RECEIPT_58MM, true)
                .addResolution(
                    PrintAttributes.Resolution(
                        "res203",
                        "203dpi",
                        203,
                        203
                    ),
                    true
                )
                .setColorModes(
                    PrintAttributes.COLOR_MODE_MONOCHROME,
                    PrintAttributes.COLOR_MODE_MONOCHROME
                )
                .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
                .build()

            return PrinterInfo.Builder(id, label, status)
                .setCapabilities(capabilities)
                .build()
        }

        override fun onStartPrinterDiscovery(
            priorityList: MutableList<PrinterId>
        ) {
            // 如果系统要求优先发现以前使用过的打印机,先报告它。
            if (savedPrinterId in priorityList) {
                addPrinters(listOf(buildPrinterInfo()))
                return
            }

            // 实际项目中可以先异步检查 Bluetooth/USB 状态,
            // 然后根据结果报告 IDLE 或 UNAVAILABLE。
            addPrinters(listOf(buildPrinterInfo()))
        }

        override fun onValidatePrinters(
            printerIds: MutableList<PrinterId>
        ) {
            if (savedPrinterId in printerIds) {
                addPrinters(listOf(buildPrinterInfo()))
            }
        }

        override fun onStartPrinterStateTracking(printerId: PrinterId) {
            if (printerId == savedPrinterId) {
                addPrinters(listOf(buildPrinterInfo()))
            }
        }

        override fun onStopPrinterStateTracking(printerId: PrinterId) = Unit

        override fun onStopPrinterDiscovery() = Unit

        override fun onDestroy() = Unit
    }
}

"saved_printer" 必须保持稳定。如果每次发现时都生成随机 UUID,系统会把同一台物理打印机识别为不同的打印机。

设备暂时断开时,可以使用原来的 PrinterId 报告 PrinterInfo.STATUS_UNAVAILABLE,连接恢复后再更新为 PrinterInfo.STATUS_IDLE。保留同一个 ID,有助于系统持续识别同一台打印机。

这些修改可以补全发现生命周期,但无法代替默认打印机 API。

无法通过公开 API 持久化系统默认打印机

PrinterDiscoverySession 只管理当前发现会话。PrintManager.print() 用于创建打印任务并打开系统打印界面,但它不接受 PrinterId、打印服务或默认目标参数。

应用可以在自己的配置中保存打印机信息,例如通过 DataStore 保存:

  • 逻辑打印机 ID
  • Bluetooth 设备标识
  • USB 设备匹配信息
  • 纸宽和字符集
  • 钱箱、切刀等能力配置

这些配置只能用于应用自己的打印通道,无法写入 Android 打印对话框的默认打印机设置。修改系统设置、调用隐藏 API 或使用 ADB 命令强行设置,都不适合作为正式产品方案,也很难兼容不同 Android 版本和厂商设备。

Chrome 可以使用非网络打印机

Chrome 会把页面生成打印文档,再交给 Android 打印框处理,通常不会区分最终使用的是 WiFi、Bluetooth 还是 USB。只要用户选中对应的 PrintService,非网络打印机就能工作。

这套流程面向交互式文档打印,默认由用户确认打印目标、纸张和参数。对于高频 POS 场景所需的静默打印、固定目标和自动重连,它没有提供相应能力。

Bluetooth/USB 打印任务本身可以执行,只是系统打印交互不适合每天几十次的收银打印。

POS 场景的推荐方案

方案一:Android 容器加 WebView 打印桥接

如果可以让 POS 页面运行在自有 Android 应用的 WebView 中,建议由原生层连接打印机,网页只提交结构化订单或已经生成的 ESC/POS 数据。

class PosPrintBridge(
    private val printer: ReceiptPrinter,
    private val executor: Executor
) {
    @JavascriptInterface
    fun printReceipt(base64Data: String) {
        executor.execute {
            val data = Base64.decode(base64Data, Base64.DEFAULT)
            printer.write(data)
        }
    }
}
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(
    PosPrintBridge(printer, executor),
    "PosPrinter"
)

网页调用示例:

const data = buildEscPosReceipt(order);
PosPrinter.printReceipt(toBase64(data));

应用可以通过这种方式保存默认打印机、维护连接和执行重试,也可以直接处理切纸、钱箱及 KOT 分单。addJavascriptInterface() 只能暴露给受信任的页面,WebView 不应随意打开第三方站点。

方案二:Chrome 加本地打印代理

如果必须继续使用完整 Chrome,可以安装一个本地 Android 辅助应用,在 127.0.0.1 上提供 HTTP 或 WebSocket 接口。网页把打印任务提交给本地代理,再由代理连接 Bluetooth/USB 打印机。

await fetch("http://127.0.0.1:18080/print", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${localToken}`
  },
  body: JSON.stringify({
    printerId: "kitchen",
    payload: receiptData
  })
});

本地打印代理是浏览器 POS 常用的集成方式,但不是 Android 官方标准。部署前需要在目标 Chrome 版本和设备策略下验证。HTTPS 页面访问本地 HTTP 服务时,可能会受到混合内容、CORS 或浏览器私有网络访问规则的限制。

代理至少需要满足以下要求:

  • 只监听 127.0.0.1,不要监听全部网络接口
  • 使用随机令牌或请求签名验证调用方
  • 校验允许的页面来源
  • 限制请求大小、打印机 ID 和指令类型
  • 防止重复提交和重放
  • 不把 CORS 当作身份认证
  • 对 USB 授权、Bluetooth 断线和打印失败提供明确状态

方案三:直接使用打印机厂商 SDK

如果打印机厂商提供稳定的 Android SDK,可以让原生应用通过 SDK 打印。设备需要状态查询、切刀、蜂鸣器、钱箱或错误恢复时,厂商 SDK 通常比通用 PrintService 更方便处理。

实施建议

如果产品允许安装受控的 Android 客户端,优先使用 WebView 加原生打印桥接。必须保留 Chrome 时,可以再考虑本地打印代理。系统 PrintService 适合作为偶尔使用的手动打印入口,不适合承担高频 POS 的默认打印流程。

Android 12 及以上版本的 Bluetooth 权限、USB 用户授权和后台执行限制仍需单独处理。这些限制会影响连接能否成功,但无法控制系统主打印机列表或默认选择。

备注:内容仅供参考。