Skip to content

Commit 64cc88b

Browse files
nqmgamingclaude
andcommitted
chore(install): trace the notification install path
The flow crosses an activity, a store, a notifier and a broadcast receiver, and every failure so far looked identical from the outside: no notification, a dialog instead. Which branch it took was not answerable from a log. Every decision now logs under one grep-able tag: the mode that was read, the parse result, risks found, whether notifications were available, whether staging produced anything, whether the prompt posted, and every fallback to the dialog. Staging logs how many APKs it copied. adb logcat | grep NotifInstall Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ff28990 commit 64cc88b

2 files changed

Lines changed: 25 additions & 4 deletions

File tree

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ class DialogInstallActivity : ComponentActivity() {
154154
/** Headless parse budget. Exceeding it falls back to the dialog rather than hanging. */
155155
private const val PARSE_TIMEOUT_MS = 30_000L
156156

157+
/**
158+
* One grep-able tag for the whole notification-install path. The flow crosses an activity,
159+
* a store, a notifier and a broadcast receiver, so "which branch did it take" is otherwise
160+
* not answerable from a log.
161+
*/
162+
private const val LOG = "NotifInstall"
163+
157164
/** Rounded at the top only — the bottom edge runs off the screen. */
158165
private val SHEET_SHAPE = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp)
159166
}
@@ -225,6 +232,7 @@ class DialogInstallActivity : ComponentActivity() {
225232
* as they do from the dialog.
226233
*/
227234
private fun installFromNotificationAction() {
235+
Timber.i("$LOG: firing install from the notification action")
228236
viewModel.confirmInstall(trackDialogTarget = true)
229237
lifecycleScope.launch {
230238
// confirmInstall publishes the real session id asynchronously (ackpine mints it).
@@ -267,8 +275,9 @@ class DialogInstallActivity : ComponentActivity() {
267275
private fun HeadlessNotificationInstall(mode: ExternalOpenMode, uri: Uri) {
268276
val context = LocalContext.current
269277
LaunchedEffect(uri) {
278+
Timber.i("$LOG: mode=$mode, parsing $uri")
270279
runCatching { parseAndPush(context, uri) }.onFailure { e ->
271-
Timber.e(e, "Headless parse failed for $uri")
280+
Timber.e(e, "$LOG: parse threw - giving up")
272281
finish()
273282
return@LaunchedEffect
274283
}
@@ -277,24 +286,32 @@ class DialogInstallActivity : ComponentActivity() {
277286
viewModel.uiState.map { it.pendingApkInfo }.filterNotNull().first()
278287
}
279288
if (apkInfo == null) {
280-
Timber.w("Headless parse produced nothing — falling back to the dialog")
289+
Timber.w("$LOG: no parse result within ${PARSE_TIMEOUT_MS}ms - falling back to the dialog")
281290
skipInitialParse = true
282291
fallbackToDialog()
283292
return@LaunchedEffect
284293
}
285294

295+
Timber.i("$LOG: parsed ${apkInfo.packageName}, ${apkInfo.splitEntries.size} split(s)")
296+
286297
// Auto mode installs without asking, but not past a risk the user has never seen.
287298
// A downgrade or a signature mismatch still gets the prompt.
288299
val risks = detectInstallRisks(apkInfo)
289300
if (mode == ExternalOpenMode.AutoNotification && risks.isEmpty()) {
301+
Timber.i("$LOG: auto mode, no risks - installing without asking")
290302
installFromNotificationAction()
291303
return@LaunchedEffect
292304
}
293305

294306
// Checked before stashing: a prompt that cannot be posted would strand the install
295307
// with nothing on screen, and the dialog is the only fallback left.
296-
val entry = if (promptNotifier.canPost()) viewModel.stashPendingInstall() else null
308+
if (risks.isNotEmpty()) Timber.i("$LOG: ${risks.size} risk(s) - asking rather than auto-installing")
309+
val canPost = promptNotifier.canPost()
310+
if (!canPost) Timber.w("$LOG: notifications unavailable")
311+
val entry = if (canPost) viewModel.stashPendingInstall() else null
312+
if (canPost && entry == null) Timber.w("$LOG: nothing to stash - staging the APKs must have failed")
297313
if (entry == null || !promptNotifier.prompt(entry)) {
314+
Timber.w("$LOG: falling back to the dialog")
298315
entry?.let {
299316
viewModel.restorePendingInstall(it)
300317
PendingInstallStore.consume(it.id)
@@ -303,6 +320,7 @@ class DialogInstallActivity : ComponentActivity() {
303320
fallbackToDialog()
304321
return@LaunchedEffect
305322
}
323+
Timber.i("$LOG: prompt ${entry.id} posted for ${entry.packageName}, ${entry.apkUris.size} staged uri(s)")
306324
finish()
307325
}
308326
}
@@ -324,6 +342,7 @@ class DialogInstallActivity : ComponentActivity() {
324342
if (pendingId != null) {
325343
val entry = PendingInstallStore.consume(pendingId)
326344
promptNotifier.cancel(pendingId)
345+
Timber.i("$LOG: resuming $pendingId, found=${entry != null}")
327346
if (entry == null) {
328347
// The process died while the prompt sat in the shade, taking the parse with it.
329348
// Nothing installable is left, so don't pretend otherwise.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,9 @@ class InstallViewModel(
624624
target,
625625
)
626626
}
627-
}.onFailure { Timber.e(it, "Could not stage APKs for a pending install") }.getOrNull()
627+
}.onSuccess { Timber.i("NotifInstall: staged ${it.size} apk(s) under $PENDING_INSTALL_DIR") }
628+
.onFailure { Timber.e(it, "NotifInstall: could not stage APKs for a pending install") }
629+
.getOrNull()
628630
}
629631

630632
/**

0 commit comments

Comments
 (0)