Skip to content

fix: in-app updater downloads but never installs (#116, #99, #75) - #123

Merged
ProdigyV21 merged 1 commit into
mainfrom
fix/in-app-updater-install-receiver
Apr 5, 2026
Merged

fix: in-app updater downloads but never installs (#116, #99, #75)#123
ProdigyV21 merged 1 commit into
mainfrom
fix/in-app-updater-install-receiver

Conversation

@ProdigyV21

Copy link
Copy Markdown
Owner

Summary

Fixes the long-standing in-app updater bug where the APK downloads successfully but the install screen never appears. Reported multiple times across versions 1.9.3 - 1.9.73:

Root cause

PR #95 switched the PackageInstaller session callback from getActivity to getBroadcast against the action com.arvio.tv.INSTALL_COMPLETE, but no BroadcastReceiver was ever registered for that action anywhere in the project (verified by grep — the only reference was the Intent(...) construction in ApkInstaller.kt:123).

On Android 7+ the PackageInstaller session API requires user confirmation for non-privileged apps. It delivers the result by firing the supplied PendingIntent with:

  • EXTRA_STATUS == STATUS_PENDING_USER_ACTION, and
  • EXTRA_INTENT containing the system install-confirmation Activity the app must startActivity() to actually show the "Install?" screen.

With no receiver, the session commit succeeded silently, the callback went nowhere, and users saw the "Installing update..." toast followed by nothing. The APK sat in cache and the old process kept running.

Changes

  1. New ApkInstallReceiver (app/src/main/kotlin/com/arflix/tv/updater/ApkInstallReceiver.kt) that handles:

    • STATUS_PENDING_USER_ACTION → extracts EXTRA_INTENT, adds FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP | FLAG_GRANT_READ_URI_PERMISSION, and startActivity()s the confirm screen.
    • STATUS_SUCCESS → logs (no toast, the new APK is replacing the running process).
    • All STATUS_FAILURE_* values → logs and shows a user-friendly toast explaining what went wrong (storage, conflict, incompatible, blocked, aborted, invalid, etc.).
    • Wraps startActivity in try/catch so Chinese Android TV forks whose non-AOSP installer doesn't handle the standard confirm intent degrade gracefully with a user message instead of crashing.
  2. Register the receiver in AndroidManifest.xml with android:exported="false" and an intent filter using ${applicationId}.INSTALL_COMPLETE. The ${applicationId} template variable means the action is unique per build flavor (play, sideload, .staging), so the receiver can't collide with other ARVIO installs on the same device.

  3. Fix ApkInstaller.launchInstall:

    • Derive the broadcast action from context.packageName at runtime via ApkInstallReceiver.actionFor(context), replacing the hard-coded com.arvio.tv.INSTALL_COMPLETE string that was wrong for the .staging build flavor.
    • Use context.applicationContext when constructing the PendingIntent so it outlives the calling Activity.
    • Only set PendingIntent.FLAG_MUTABLE on API 31+ (has no effect on older APIs but avoids the lint warning and documents intent).
    • Wrap the ACTION_VIEW fallback path in try/catch so it no longer crashes on forks that reject the application/vnd.android.package-archive mime type.

Testing

  • Unit-testable paths: all new logic is in ApkInstallReceiver.onReceive, which can be driven by constructing Intent fixtures with each status value.
  • Manual test plan (requires two sideload-flavor APK builds on a real Android TV device or phone):
    1. Install the older version.
    2. Install this build as the "current" version.
    3. Publish a newer sideload APK.
    4. Trigger the in-app update from Settings.
    5. Verify the system "Install?" confirmation dialog actually appears.
    6. Tap Install and verify the app updates.

Risk

Low. The session-based path was already the default; this PR only fixes its broken callback handling. The fallback ACTION_VIEW path is unchanged except for defensive try/catch wrapping. No behavior change for users on Android <7 (where PackageInstaller session path is already skipped).

The action rename from com.arvio.tv.INSTALL_COMPLETE${applicationId}.INSTALL_COMPLETE is safe because the only sender (ApkInstaller.launchInstall) and the only receiver (ApkInstallReceiver) are both updated in lockstep in this PR.

PR #95 switched the PackageInstaller session callback from getActivity to
getBroadcast against action `com.arvio.tv.INSTALL_COMPLETE`, but no
BroadcastReceiver was ever registered for that action. On Android 7+ the
PackageInstaller session API delivers its result by firing the supplied
PendingIntent with STATUS_PENDING_USER_ACTION and an EXTRA_INTENT containing
the system install-confirmation Activity the app must startActivity() to
actually show the "Install?" screen.

With no receiver, the session commit succeeded silently, the callback went
nowhere, and the user saw "Installing update..." followed by nothing. The
APK sat in cache and the old process kept running. This has been broken
across multiple versions (1.9.3 through 1.9.73) and directly blocks users
from receiving any other fixes we ship.

Changes:
- New `ApkInstallReceiver` that handles STATUS_PENDING_USER_ACTION by
  launching the system confirm Activity with NEW_TASK + CLEAR_TOP + GRANT
  URI permission, and surfaces failure statuses as user-visible toasts
  instead of silently dropping them.
- Register the receiver in AndroidManifest.xml with an intent filter using
  `${applicationId}.INSTALL_COMPLETE` so the action name is unique per
  build flavor (play / sideload / staging) and can't collide with other
  ARVIO installs on the same device.
- Derive the broadcast action in `ApkInstaller` from `context.packageName`
  at runtime via `ApkInstallReceiver.actionFor()`, replacing the hard-coded
  `com.arvio.tv.INSTALL_COMPLETE` string that was wrong for the `.staging`
  build flavor.
- Use `context.applicationContext` when constructing the PendingIntent and
  only pass FLAG_MUTABLE on API 31+ (it has no effect on older APIs but
  keeps the lint clean).
- Wrap the ACTION_VIEW fallback path in a try/catch so it no longer crashes
  on Chinese Android TV forks whose non-AOSP installer rejects the standard
  Intent.

Closes #116
Closes #99
Closes #75
@ProdigyV21
ProdigyV21 merged commit c727a50 into main Apr 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant