This commit is contained in:
2026-08-26 09:48:26 +08:00
parent 6cac15a14d
commit 54d343b739
16 changed files with 448 additions and 24 deletions
@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<!-- App 内更新安装 APKPackageInstaller 会话安装,见 InstallerChannel.kt -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application
android:label="视野"
android:name="${applicationName}"
@@ -0,0 +1,139 @@
package com.example.observer
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import java.io.File
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
/**
* 原生 APK 安装通道(App 内更新安装):PackageInstaller 会话安装,
* 安装进度经 EventChannel 实时回传 Flutter(下载进度由 Flutter 侧 http 流式下载自算)。
*
* 通道:
* - MethodChannel "observer/installer"install(path)
* - result.success("installing"):已提交安装
* - result.success("permission_required"):未开「安装未知应用」,已拉起系统设置页
* - EventChannel "observer/installer/progress"{event: progress/finished/failed, ...}
*/
class InstallerChannel(
private val activity: FlutterActivity,
private val engine: FlutterEngine,
) {
private val messenger = engine.dartExecutor.binaryMessenger
private val method = MethodChannel(messenger, "observer/installer")
private val progress = EventChannel(messenger, "observer/installer/progress")
private var progressSink: EventChannel.EventSink? = null
private var activeSession: PackageInstaller.Session? = null
fun register() {
method.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
when (call.method) {
"install" -> install(call.argument<String>("path") ?: "", result)
else -> result.notImplemented()
}
}
progress.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
progressSink = events
}
override fun onCancel(arguments: Any?) {
progressSink = null
}
})
}
private fun install(path: String, result: MethodChannel.Result) {
val file = File(path)
if (!file.exists()) {
result.error("FILE_NOT_FOUND", "APK 文件不存在: $path", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
!activity.packageManager.canRequestPackageInstalls()
) {
val intent = Intent(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
Uri.parse("package:${activity.packageName}"),
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
activity.startActivity(intent)
result.success("permission_required")
return
}
try {
val pm = activity.packageManager
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
setSize(file.length())
}
val sessionId = pm.packageInstaller.createSession(params)
val session = pm.packageInstaller.openSession(sessionId)
activeSession = session
// API 36 起 registerSessionCallback(int, ...) 变体被移除,只剩全局注册形式,
// 回调按 sessionId 过滤,避免响应其他会话事件
val callback = object : PackageInstaller.SessionCallback() {
override fun onCreated(id: Int) {}
override fun onBadgingChanged(id: Int) {}
override fun onActiveChanged(id: Int, active: Boolean) {}
override fun onProgressChanged(id: Int, progressPercent: Float) {
if (id != sessionId) return
emit("progress", "progress" to progressPercent.toInt())
}
override fun onFinished(id: Int, success: Boolean) {
if (id != sessionId) return
emit("finished", "success" to success)
pm.packageInstaller.unregisterSessionCallback(this)
activeSession = null
}
}
pm.packageInstaller.registerSessionCallback(callback, Handler(Looper.getMainLooper()))
// 写 APK 到会话:1MB 缓冲流式拷贝,完成后 commit 弹系统确认框
Thread {
try {
session.openWrite("apk", 0, file.length()).use { out ->
file.inputStream().use { input ->
val buf = ByteArray(1 shl 20)
while (true) {
val n = input.read(buf)
if (n < 0) break
out.write(buf, 0, n)
}
}
}
val sender = PendingIntent.getActivity(
activity,
0,
Intent(activity, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
).intentSender
session.commit(sender)
} catch (e: Exception) {
session.abandon()
emit("failed", "error" to (e.message ?: "安装会话写入失败"))
activeSession = null
}
}.start()
result.success("installing")
} catch (e: Exception) {
result.error("INSTALL_FAILED", e.message, null)
}
}
private fun emit(event: String, vararg pairs: Pair<String, Any>) {
progressSink?.success(mapOf("event" to event, *pairs))
}
}
@@ -5,6 +5,7 @@ import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity() {
private var cameraChannel: CameraChannel? = null
private var installerChannel: InstallerChannel? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -13,6 +14,8 @@ class MainActivity : FlutterActivity() {
// configureFlutterEngineonCreate 阶段)只注册通道与 viewFactory
// 实际 bindToLifecycle 由 Flutter 相机页 start 时触发(此时已 RESUMED
cameraChannel = CameraChannel(this, flutterEngine).also { it.register() }
// App 内更新安装 APKPackageInstaller 会话安装 + 进度回传)
installerChannel = InstallerChannel(this, flutterEngine).also { it.register() }
}
override fun onDestroy() {