mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-21 21:52:33 -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"
|
applicationId = "com.remotedisplay.player"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 12
|
versionCode = 13
|
||||||
versionName = "1.7.9"
|
versionName = "1.7.10"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo
|
||||||
|
|
||||||
class PowerAccessibilityService : AccessibilityService() {
|
class PowerAccessibilityService : AccessibilityService() {
|
||||||
|
|
||||||
|
|
@ -22,7 +23,53 @@ class PowerAccessibilityService : AccessibilityService() {
|
||||||
Log.i(TAG, "Service connected")
|
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() {}
|
override fun onInterrupt() {}
|
||||||
|
|
||||||
// Global actions
|
// Global actions
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,45 @@ class UpdateChecker(private val context: Context) {
|
||||||
// Check every 30 minutes
|
// Check every 30 minutes
|
||||||
private val CHECK_INTERVAL = 30 * 60 * 1000L
|
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() {
|
fun startPeriodicCheck() {
|
||||||
stopPeriodicCheck()
|
stopPeriodicCheck()
|
||||||
|
ensureInstallReceiver()
|
||||||
checkTimer = object : Runnable {
|
checkTimer = object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
checkForUpdate()
|
checkForUpdate()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue