This commit is contained in:
2026-09-01 15:18:05 +08:00
parent dcde05d67c
commit abccda0bd6
19 changed files with 381 additions and 160 deletions
@@ -8,6 +8,7 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import java.io.File
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -36,6 +37,11 @@ class InstallerChannel(
private var progressSink: EventChannel.EventSink? = null
private var activeSession: PackageInstaller.Session? = null
/// 安装完成轮询兜底开关:部分 ROM(如 MIUI)点确认后不回调
/// onProgressChanged/onFinished,仅靠回调会永久停在「安装中」。
@Volatile
private var installSettled = false
fun register() {
method.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
when (call.method) {
@@ -55,7 +61,9 @@ class InstallerChannel(
}
private fun install(path: String, result: MethodChannel.Result) {
installSettled = false
val file = File(path)
Log.d(TAG, "install: path=$path exists=${file.exists()} len=${file.length()}")
if (!file.exists()) {
result.error("FILE_NOT_FOUND", "APK 文件不存在: $path", null)
return
@@ -63,6 +71,7 @@ class InstallerChannel(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
!activity.packageManager.canRequestPackageInstalls()
) {
Log.d(TAG, "install: permission_required")
val intent = Intent(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
Uri.parse("package:${activity.packageName}"),
@@ -79,6 +88,7 @@ class InstallerChannel(
val sessionId = pm.packageInstaller.createSession(params)
val session = pm.packageInstaller.openSession(sessionId)
activeSession = session
Log.d(TAG, "install: session=$sessionId opened")
// API 36 起 registerSessionCallback(int, ...) 变体被移除,只剩全局注册形式,
// 回调按 sessionId 过滤,避免响应其他会话事件
val callback = object : PackageInstaller.SessionCallback() {
@@ -90,20 +100,30 @@ class InstallerChannel(
override fun onProgressChanged(id: Int, progressPercent: Float) {
if (id != sessionId) return
Log.d(TAG, "callback onProgressChanged=$progressPercent")
emit("progress", "progress" to progressPercent.toInt())
}
override fun onFinished(id: Int, success: Boolean) {
if (id != sessionId) return
Log.d(TAG, "callback onFinished success=$success")
installSettled = true
emit("finished", "success" to success)
pm.packageInstaller.unregisterSessionCallback(this)
activeSession = null
}
}
pm.packageInstaller.registerSessionCallback(callback, Handler(Looper.getMainLooper()))
// 安装前已装版本号(更新安装成功后必然变化,轮询兜底依据)
val installedCode = try {
pm.getPackageInfo(activity.packageName, 0).versionCode
} catch (e: Exception) {
-1
}
// 写 APK 到会话:1MB 缓冲流式拷贝,完成后 commit 弹系统确认框
Thread {
try {
Log.d(TAG, "writeThread: start")
session.openWrite("apk", 0, file.length()).use { out ->
file.inputStream().use { input ->
val buf = ByteArray(1 shl 20)
@@ -114,19 +134,47 @@ class InstallerChannel(
}
}
}
Log.d(TAG, "writeThread: written, commit")
// commit 的 status receiver 必须 MUTABLE:系统要向 intent 写入安装结果,
// FLAG_IMMUTABLE 直接抛 "The commit() status receiver should come from a
// mutable PendingIntent"(模拟器 AOSP 严格校验;部分 ROM 不校验但回调丢失)
val sender = PendingIntent.getActivity(
activity,
0,
Intent(activity, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
).intentSender
session.commit(sender)
} catch (e: Exception) {
Log.e(TAG, "writeThread: failed", e)
session.abandon()
emit("failed", "error" to (e.message ?: "安装会话写入失败"))
activeSession = null
}
}.start()
// 完成轮询兜底:回调缺失时靠版本号变化判定安装成功(2s 间隔,最长 120s)
Thread {
var waited = 0
while (!installSettled && waited < 120_000) {
Thread.sleep(2000)
waited += 2000
val now = try {
pm.getPackageInfo(activity.packageName, 0).versionCode
} catch (e: Exception) {
-1
}
if (now != installedCode && now != -1) {
Log.d(TAG, "poll: versionCode changed $installedCode->$now, settled")
installSettled = true
emit("finished", "success" to true)
return@Thread
}
}
if (!installSettled) {
Log.d(TAG, "poll: timeout after 120s, not settled")
emit("failed", "error" to "安装超时,请重试")
}
}.start()
result.success("installing")
} catch (e: Exception) {
result.error("INSTALL_FAILED", e.message, null)
@@ -134,6 +182,15 @@ class InstallerChannel(
}
private fun emit(event: String, vararg pairs: Pair<String, Any>) {
progressSink?.success(mapOf("event" to event, *pairs))
// EventSink.success 内部 dispatchPlatformMessage 要求主线程(@UiThread 校验),
// 写 APK 线程 / 轮询线程直接调用会崩溃,必须投递主线程
Handler(Looper.getMainLooper()).post {
Log.d(TAG, "emit: $event $pairs")
progressSink?.success(mapOf("event" to event, *pairs))
}
}
companion object {
private const val TAG = "InstallerChannel"
}
}
@@ -1,5 +1,10 @@
package com.example.observer
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.os.Bundle
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -7,6 +12,44 @@ class MainActivity : FlutterActivity() {
private var cameraChannel: CameraChannel? = null
private var installerChannel: InstallerChannel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleInstallIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleInstallIntent(intent)
}
/**
* Android 14+ 安装确认回调:PackageInstaller 会话 commit 后如需要用户确认
* (确认对话框/安装未知来源授权),系统不直接弹 packageinstaller 界面,
* 而是经 commit 传入的 statusReceiver(指向本 Activity 的 PendingIntent
* 回传 STATUS_PENDING_USER_ACTION,并在 EXTRA_INTENT 里附上确认界面 Intent
* ACTION_CONFIRM_INSTALL → InstallStart),由调用方负责启动。
* 不处理则确认框永不出现,安装卡在 0.8% 直至超时(AOSP 见
* PackageInstallerSession.sendOnUserActionRequired)。
*/
private fun handleInstallIntent(intent: Intent?) {
val status = intent?.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) ?: return
if (status != PackageInstaller.STATUS_PENDING_USER_ACTION) return
val confirm = if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}
if (confirm == null) return
try {
confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(confirm)
Log.d(TAG, "handleInstallIntent: started confirm ${confirm.component}")
} catch (e: Exception) {
Log.e(TAG, "handleInstallIntent: start confirm failed", e)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 自写原生相机(替代 camera_android_camerax 插件):
@@ -18,6 +61,10 @@ class MainActivity : FlutterActivity() {
installerChannel = InstallerChannel(this, flutterEngine).also { it.register() }
}
companion object {
private const val TAG = "MainActivity"
}
override fun onDestroy() {
cameraChannel?.destroy()
super.onDestroy()