feat(update): silent self-update via PackageInstaller session — #2438
android / Build + lint + test (push) Successful in 3m54s
android / Build + lint + test (push) Successful in 3m54s
Replaces the ACTION_VIEW + application/vnd.android.package-archive handoff with a PackageInstaller session, and declares UPDATE_PACKAGES_WITHOUT_USER_ACTION so the update can land with no confirm dialog at all. The platform grants the silent path when the installer opts in via setRequireUserAction(USER_ACTION_NOT_REQUIRED), the installed app targets API 29+, the installer holds that permission, and the target is the installer itself. Minstrel updating Minstrel satisfies all four. Where it can't be granted — anything pre-S — the platform returns STATUS_PENDING_USER_ACTION and we show its dialog instead, so this degrades rather than failing. Prior art: Mihon, which is out-of-store and self-updating and whose updates are quiet for exactly this reason. It also confirmed REQUEST_INSTALL_PACKAGES is not what draws install warnings — Mihon declares it too. No setRequestUpdateOwnership(true), despite it reading like the obvious declaration for a self-updater. Ownership can only be claimed on initial installation (a no-op on update) and additionally wants the privileged ENFORCE_UPDATE_OWNERSHIP permission. It's an API for app stores claiming the apps they install. Also: the install now has an outcome. The old path fired an intent and assumed, so a failure and a user declining were indistinguishable. Sessions report back, so InstallOutcome distinguishes Installed / Cancelled / Failed, and cancelling returns to IDLE rather than showing an error — the user chose it. DOWNLOADING and INSTALLING became separate stages because the install half now genuinely waits, and "Downloading…" through a confirm dialog is a lie. The FileProvider and res/xml/file_paths.xml are gone. They existed only to expose the cached APK as a content:// URI for the old intent; a session takes a stream. Nothing else used that authority. Two judgement calls worth naming: - The pending-user-action intent is only launched if it resolves to a system component. Below API 34 a dynamically registered receiver can't declare itself unexported, so another app can broadcast at us, and an unchecked startActivity on an attacker-supplied extra would be an escalation primitive. The real confirm activity is a system app, so the check costs the legitimate path nothing. - Cancellation unregisters the receiver but deliberately does NOT abandon the session. By then it's committed, and killing an install because the user navigated away from the banner misreads their intent. Untestable here: no androidTest source set and no Robolectric, so the gesture-level behaviour is operator on-device verification.
This commit is contained in:
@@ -8,7 +8,16 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- In-app self-update. REQUEST_INSTALL_PACKAGES lets us hand an APK to the
|
||||
platform installer at all; UPDATE_PACKAGES_WITHOUT_USER_ACTION (API 31+)
|
||||
is what lets that install happen with NO confirm dialog. The platform
|
||||
grants the silent path only when the installer opts in via
|
||||
SessionParams.setRequireUserAction(USER_ACTION_NOT_REQUIRED), the
|
||||
installed app targets API 29+, the installer holds this permission, and
|
||||
the target is the installer itself — all true here, since Minstrel is
|
||||
updating Minstrel. See update/data/SelfUpdateSession.kt. -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
|
||||
@@ -48,15 +57,11 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<!-- The FileProvider that used to live here existed solely to expose the
|
||||
downloaded update APK as a content:// URI for the old ACTION_VIEW
|
||||
install intent. A PackageInstaller session takes a stream instead,
|
||||
so both the provider and res/xml/file_paths.xml are gone — nothing
|
||||
else in the app ever used that authority. -->
|
||||
|
||||
<!-- On-demand WorkManager initialization: MinstrelApplication
|
||||
implements Configuration.Provider and supplies the
|
||||
|
||||
Reference in New Issue
Block a user