Update install failures become visible: blocked, aborted, failed (D-155)

The PackageInstaller session's final status now reaches UpdateRepository:
InstallResultReceiver hands it by session id to the waiting AndroidApkInstaller
call (ApkInstaller.install returns a PendingInstall). installOutcome() maps
STATUS_FAILURE_BLOCKED to UpdateFailure.Blocked, STATUS_FAILURE_ABORTED back to
Available without an error, and every other failure to UpdateFailure.Install
with the installer's EXTRA_STATUS_MESSAGE. The cached APK is deleted once the
installer answers, so "Try again" downloads again. UpdateBanner's Failed variant
shows the reason on S-4 and S-22; two core:design strings (en/sv) with their
docs/ux/strings.csv rows. Unit tests for the mapping, the repository outcomes
and the receiver; goldens: update_banner re-recorded, S-17-install-blocked,
S-17-install-failed and s22_about_install_blocked new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-21 21:20:12 +02:00
parent 1c9856bfba
commit d686ee0120
25 changed files with 327 additions and 27 deletions
@@ -127,6 +127,7 @@ import se.hagstedt.kvitt.model.ColorToken
import se.hagstedt.kvitt.model.ExpenseEffect
import se.hagstedt.kvitt.model.Share
import se.hagstedt.kvitt.model.SplitMode
import se.hagstedt.kvitt.model.UpdateFailure
import se.hagstedt.kvitt.model.UpdateState
import kotlinx.datetime.LocalDate
import androidx.compose.ui.res.stringResource
@@ -300,6 +301,8 @@ fun UpdateBannerSheet() {
UpdateBanner(UpdateState.Available(Samples.update), online = false, {}, {}, {})
UpdateBanner(UpdateState.Downloading(Samples.update, 0.64f), online = true, {}, {}, {})
UpdateBanner(UpdateState.Failed(Samples.update), online = true, {}, {}, {})
UpdateBanner(UpdateState.Failed(Samples.update, UpdateFailure.Blocked), online = true, {}, {}, {})
UpdateBanner(UpdateState.Failed(Samples.update, UpdateFailure.Install(Samples.INSTALLER_MESSAGE)), online = true, {}, {}, {})
}
@Composable
@@ -102,6 +102,9 @@ object Samples {
val update = UpdateInfo(versionCode = 12, versionName = "1.2", sizeBytes = 8_400_000)
/** A real `EXTRA_STATUS_MESSAGE` of a signature mismatch (D-155). */
const val INSTALLER_MESSAGE = "INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package se.hagstedt.kvitt signatures do not match newer version; ignoring!"
val ratioShares = listOf(Share(jacob.id, 20_600, 5000), Share(anna.id, 20_600, 5000))
val icaShares = listOf(Share(jacob.id, 62_380, 5000), Share(anna.id, 62_380, 5000))
@@ -38,6 +38,7 @@ import se.hagstedt.kvitt.design.format.RelativeTime
import se.hagstedt.kvitt.design.icon.KvittIcon
import se.hagstedt.kvitt.design.theme.KvittDimens
import se.hagstedt.kvitt.model.SyncSummary
import se.hagstedt.kvitt.model.UpdateFailure
import se.hagstedt.kvitt.model.UpdateState
import kotlin.time.Instant
@@ -184,7 +185,9 @@ fun Banner(
/**
* The in-app update banner (design-spec §3.6, S-17): Available (Lantern, "Uppdatera" disabled offline with "Kräver
* anslutning" as body), Downloading (progress, no actions), Failed (Error, "Försök igen"). NeedsInstallPermission looks
* anslutning" as body), Downloading (progress, no actions), Failed (Error, "Försök igen"; the title and body follow the
* reason, D-155: download, blocked by the phone's security settings, or another install failure with the installer's
* own message). NeedsInstallPermission looks
* like Available. Nothing for [UpdateState.None].
*/
@Composable
@@ -226,9 +229,16 @@ fun UpdateBanner(state: UpdateState, online: Boolean, onUpdate: () -> Unit, onLa
},
)
}
is UpdateState.Failed -> Banner(BannerKind.Error, KvittIcon.Error, stringResource(R.string.update_failed), modifier) { content ->
KvittButton(stringResource(R.string.update_later), onLater, kind = ButtonKind.Text, contentColor = content)
KvittButton(stringResource(R.string.common_retry), onRetry, kind = ButtonKind.ErrorFilled, icon = KvittIcon.Refresh)
is UpdateState.Failed -> {
val (title, body) = when (val reason = state.reason) {
UpdateFailure.Download -> stringResource(R.string.update_failed) to null
UpdateFailure.Blocked -> stringResource(R.string.update_install_failed) to stringResource(R.string.update_install_blocked)
is UpdateFailure.Install -> stringResource(R.string.update_install_failed) to reason.detail
}
Banner(BannerKind.Error, KvittIcon.Error, title, modifier, body = body) { content ->
KvittButton(stringResource(R.string.update_later), onLater, kind = ButtonKind.Text, contentColor = content)
KvittButton(stringResource(R.string.common_retry), onRetry, kind = ButtonKind.ErrorFilled, icon = KvittIcon.Refresh)
}
}
}
}
@@ -328,6 +328,10 @@
<string name="update_downloading_spoken">Laddar ner version %1$s</string>
<!-- AC-16.4 -->
<string name="update_failed">Nedladdningen misslyckades. Försök igen.</string>
<!-- D-155: the installer refused or failed; the banner's body is the reason -->
<string name="update_install_failed">Kunde inte installera uppdateringen</string>
<!-- D-155: STATUS_FAILURE_BLOCKED (device policy or package verifier); Android's own setting names per locale -->
<string name="update_install_blocked">Installationen blockerades av telefonens säkerhetsinställningar (Auto Blocker eller Installera okända appar). Ändra inställningen och försök igen.</string>
<string name="update_permission_title">Tillåt installation</string>
<!-- AC-16.3; bold the two Android setting names with <annotation>; Android's own labels per locale -->
<string name="update_permission_body">Kvitt uppdateras direkt från er server, inte från Play Butik. Därför frågar Android en gång om Kvitt får <b>Installera okända appar</b>. Slå på <b>Tillåt från den här källan</b> och gå sedan tillbaka till Kvitt. Uppdateringen fortsätter av sig själv.</string>
@@ -330,6 +330,10 @@
<string name="update_downloading_spoken">Downloading version %1$s</string>
<!-- AC-16.4 -->
<string name="update_failed">Download failed, try again</string>
<!-- D-155: the installer refused or failed; the banner's body is the reason -->
<string name="update_install_failed">Could not install the update</string>
<!-- D-155: STATUS_FAILURE_BLOCKED (device policy or package verifier); Android's own setting names per locale -->
<string name="update_install_blocked">The phone\'s security settings blocked the installation (Auto Blocker or Install unknown apps). Change the setting and try again.</string>
<string name="update_permission_title">Allow installation</string>
<!-- AC-16.3; bold the two Android setting names with <annotation>; Android's own labels per locale -->
<string name="update_permission_body">Kvitt updates straight from your own server, not from the Play Store, so Android asks once whether Kvitt may <b>Install unknown apps</b>. Turn on <b>Allow from this source</b> and come back to Kvitt. The update continues by itself.</string>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 51 KiB