mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-14 18:22:46 -06:00
fix(android): OTA install never completed; auto-confirm for kiosks (1.7.10)
The OTA downloaded + verified the new APK and committed a PackageInstaller session, but never handled STATUS_PENDING_USER_ACTION (which Android 13+ returns for non-device-owner installers) — so the session stalled and the update never installed. Reproduced on an Android 13 emulator: device stayed on the old version. - UpdateChecker: register a receiver for the session's INSTALL_COMPLETE broadcast; on PENDING_USER_ACTION launch the system confirm dialog (and log SUCCESS). - PowerAccessibilityService: when the package-installer dialog appears, auto-click the confirm button (by id, then label) so unattended kiosk screens update without a human tap. Scoped strictly to the package installer. Verified end-to-end on Android 13: device auto-updated 1.7.10 -> 1.7.11 with no interaction (receiver launched the dialog, accessibility confirmed it). Ships as 1.7.10 (also carries the Android 14+ crash + YouTube 152 fixes). NOTE: existing 1.7.7 devices still need a one-time manual reinstall to reach a build that has this fix; from 1.7.10 onward OTA is fully automatic.
This commit is contained in:
parent
91cf7ebee6
commit
5e3408be9a
|
|
@ -11,8 +11,8 @@ android {
|
|||
applicationId = "com.remotedisplay.player"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 12
|
||||
versionName = "1.7.9"
|
||||
versionCode = 13
|
||||
versionName = "1.7.10"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import android.util.DisplayMetrics
|
|||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
class PowerAccessibilityService : AccessibilityService() {
|
||||
|
||||
|
|
@ -22,7 +23,53 @@ class PowerAccessibilityService : AccessibilityService() {
|
|||
Log.i(TAG, "Service connected")
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
|
||||
private var lastConfirm = 0L
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
val pkg = event?.packageName?.toString() ?: return
|
||||
// Auto-confirm the system app-update dialog so OTA updates apply unattended
|
||||
// on kiosk screens (no one is there to tap "Update"). Scoped to the package
|
||||
// installer only, so this never touches anything else.
|
||||
if (!pkg.contains("packageinstaller", ignoreCase = true)) return
|
||||
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
|
||||
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
|
||||
autoConfirmInstall()
|
||||
}
|
||||
|
||||
private fun autoConfirmInstall() {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastConfirm < 1500) return // debounce repeated content events
|
||||
val root = rootInActiveWindow ?: return
|
||||
// Positive button by resource id first (locale-independent), then by label.
|
||||
val ids = listOf(
|
||||
"com.google.android.packageinstaller:id/ok_button",
|
||||
"com.android.packageinstaller:id/ok_button",
|
||||
"android:id/button1"
|
||||
)
|
||||
for (id in ids) {
|
||||
for (n in root.findAccessibilityNodeInfosByViewId(id)) {
|
||||
if (clickButton(n)) { lastConfirm = now; Log.i(TAG, "Auto-confirmed install via $id"); return }
|
||||
}
|
||||
}
|
||||
for (label in listOf("Update", "Install", "Reinstall", "Continue")) {
|
||||
for (n in root.findAccessibilityNodeInfosByText(label)) {
|
||||
if (clickButton(n)) { lastConfirm = now; Log.i(TAG, "Auto-confirmed install via '$label'"); return }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click the node or its nearest clickable+enabled ancestor (the button).
|
||||
private fun clickButton(node: AccessibilityNodeInfo?): Boolean {
|
||||
var cur = node
|
||||
var depth = 0
|
||||
while (cur != null && depth < 4) {
|
||||
if (cur.isClickable && cur.isEnabled) return cur.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
cur = cur.parent
|
||||
depth++
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onInterrupt() {}
|
||||
|
||||
// Global actions
|
||||
|
|
|
|||
|
|
@ -37,8 +37,45 @@ class UpdateChecker(private val context: Context) {
|
|||
// Check every 30 minutes
|
||||
private val CHECK_INTERVAL = 30 * 60 * 1000L
|
||||
|
||||
private var installReceiverRegistered = false
|
||||
|
||||
// The PackageInstaller session reports its status (incl. STATUS_PENDING_USER_ACTION,
|
||||
// which Android 13+ returns for non-device-owner installers) via this broadcast.
|
||||
// Without handling it the committed session just stalls and the update never
|
||||
// installs. On the action prompt we launch the confirm dialog; the accessibility
|
||||
// service auto-confirms it on kiosks.
|
||||
private fun ensureInstallReceiver() {
|
||||
if (installReceiverRegistered) return
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
when (intent.getIntExtra(android.content.pm.PackageInstaller.EXTRA_STATUS, -999)) {
|
||||
android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
else @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
if (confirm != null) {
|
||||
confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try { context.startActivity(confirm); Log.i(TAG, "Launched install confirmation") }
|
||||
catch (e: Exception) { Log.e(TAG, "Confirm launch failed: ${e.message}") }
|
||||
}
|
||||
}
|
||||
android.content.pm.PackageInstaller.STATUS_SUCCESS -> Log.i(TAG, "Update installed successfully")
|
||||
else -> Log.w(TAG, "Install status: ${intent.getStringExtra(android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
val filter = IntentFilter("com.remotedisplay.player.INSTALL_COMPLETE")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
@Suppress("UnspecifiedRegisterReceiverFlag") context.registerReceiver(receiver, filter)
|
||||
}
|
||||
installReceiverRegistered = true
|
||||
}
|
||||
|
||||
fun startPeriodicCheck() {
|
||||
stopPeriodicCheck()
|
||||
ensureInstallReceiver()
|
||||
checkTimer = object : Runnable {
|
||||
override fun run() {
|
||||
checkForUpdate()
|
||||
|
|
|
|||
Loading…
Reference in a new issue