1
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
<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 内更新安装 APK(PackageInstaller 会话安装,见 InstallerChannel.kt) -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<application
|
||||
android:label="视野"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
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 android.util.Log
|
||||
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
|
||||
|
||||
/// 安装完成轮询兜底开关:部分 ROM(如 MIUI)点确认后不回调
|
||||
/// onProgressChanged/onFinished,仅靠回调会永久停在「安装中」。
|
||||
@Volatile
|
||||
private var installSettled = false
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
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}"),
|
||||
).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
|
||||
Log.d(TAG, "install: session=$sessionId opened")
|
||||
// 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
|
||||
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)
|
||||
while (true) {
|
||||
val n = input.read(buf)
|
||||
if (n < 0) break
|
||||
out.write(buf, 0, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
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_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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun emit(event: String, vararg pairs: Pair<String, Any>) {
|
||||
// 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,54 +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
|
||||
|
||||
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)
|
||||
@@ -57,12 +13,6 @@ class MainActivity : FlutterActivity() {
|
||||
// configureFlutterEngine(onCreate 阶段)只注册通道与 viewFactory,
|
||||
// 实际 bindToLifecycle 由 Flutter 相机页 start 时触发(此时已 RESUMED)
|
||||
cameraChannel = CameraChannel(this, flutterEngine).also { it.register() }
|
||||
// App 内更新安装 APK(PackageInstaller 会话安装 + 进度回传)
|
||||
installerChannel = InstallerChannel(this, flutterEngine).also { it.register() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
Reference in New Issue
Block a user