Merge pull request #67 from screentinker/fix/android-ota-install-completion

fix(android): OTA install completion + kiosk auto-confirm (1.7.10)
This commit is contained in:
screentinker 2026-06-09 16:14:12 -05:00 committed by GitHub
commit acd93377e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 88 additions and 4 deletions

View file

@ -1 +1 @@
1.7.9
1.7.10

View file

@ -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 {

View file

@ -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

View file

@ -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()