Skip to content

Commit 7b3fa2a

Browse files
nqmgamingclaude
andcommitted
fix(install): point the staged parse at the staged copies
Staging the APKs was not enough: the install kept reading the source anyway. confirmInstall takes its URIs from apkInfo.splitEntries whenever there are any, and only falls back to pendingApkUris otherwise. Stashing put the staged copies in pendingApkUris but handed back an apkInfo whose splitEntries still pointed at the file the other app gave us — and every package has at least one split entry, including a plain single APK. So the staged copies were never touched and the install read the source URI exactly as before. Whether that fails depends on who the source is, which is why it looked fixed. A MediaStore URI stays readable through our media access, so opening from the system file list worked. A URI from another app's FileProvider — ZArchiver's, in the report — carries a one-shot grant that dies with the activity, and the install hit "Permission Denial: opening provider ru.zdevs.zarchiver.provider.FileProvider ... not exported". Same code path, opposite outcome, so testing through MediaStore proved nothing. The stashed parse is now rewritten to reference the copies. Every split is staged rather than only the selected ones: opening the dialog from the notification can change the selection afterwards, and an unselected split still pointing at the dead source would fail the moment it was ticked. Not yet verified on device — the phone dropped off adb before this build could be installed, and it needs testing from ZArchiver specifically, not from a MediaStore URI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 64cc88b commit 7b3fa2a

1 file changed

Lines changed: 30 additions & 12 deletions

File tree

app/src/main/java/app/pwhs/universalinstaller/presentation/install/InstallViewModel.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -563,19 +563,37 @@ class InstallViewModel(
563563
*/
564564
suspend fun stashPendingInstall(): PendingInstallStore.Entry? {
565565
val apkInfo = _pendingApkInfo.value ?: return null
566-
val uris = if (apkInfo.splitEntries.isNotEmpty()) {
567-
apkInfo.splitEntries.filter { it.selected }.map { it.uri }
568-
} else {
569-
pendingApkUris
570-
}
571-
if (uris.isNullOrEmpty()) return null
566+
val splits = apkInfo.splitEntries
572567
val fileName = pendingFileName ?: return null
573568

574-
// Copy now, while the grant is still alive. What the parse leaves in pendingApkUris is
575-
// the caller's own URI for a plain APK (only archives get extracted into our cache), and
576-
// that URI dies with this activity — installing from it later fails with
577-
// "Permission Denial: opening provider <their>.FileProvider ... not exported".
578-
val installableUris = copyForLaterInstall(uris) ?: return null
569+
// Copy now, while the grant is still alive. What the parse leaves behind is the caller's
570+
// own URI for a plain APK (only archives get extracted into our cache), and that URI dies
571+
// with this activity — installing from it later fails with "Permission Denial: opening
572+
// provider <their>.FileProvider ... not exported".
573+
//
574+
// Every split is staged, not just the selected ones, because opening the dialog from the
575+
// notification lets the selection change afterwards; an unselected split left pointing at
576+
// the dead source would fail the moment it was ticked.
577+
val sourceUris = if (splits.isNotEmpty()) splits.map { it.uri } else pendingApkUris
578+
if (sourceUris.isNullOrEmpty()) return null
579+
val stagedUris = copyForLaterInstall(sourceUris) ?: return null
580+
581+
// Rewrite the parse to point at the copies. [confirmInstall] reads the URIs off
582+
// apkInfo.splitEntries whenever there are any, and only falls back to pendingApkUris
583+
// otherwise — so staging without this rewrite installs from the dead source anyway.
584+
val stagedInfo = if (splits.isNotEmpty()) {
585+
apkInfo.copy(
586+
splitEntries = splits.mapIndexed { index, entry -> entry.copy(uri = stagedUris[index]) },
587+
)
588+
} else {
589+
apkInfo
590+
}
591+
val installableUris = if (splits.isNotEmpty()) {
592+
stagedInfo.splitEntries.filter { it.selected }.map { it.uri }
593+
} else {
594+
stagedUris
595+
}
596+
if (installableUris.isEmpty()) return null
579597

580598
val entry = PendingInstallStore.put(
581599
apkUris = installableUris,
@@ -587,7 +605,7 @@ class InstallViewModel(
587605
obbEntries = pendingObbEntries,
588606
attachedObbs = _attachedObbFiles.value,
589607
isDowngrade = isDowngrade(apkInfo),
590-
apkInfo = apkInfo,
608+
apkInfo = stagedInfo,
591609
)
592610

593611
_pendingApkInfo.value = null

0 commit comments

Comments
 (0)