An app's own log: a devlog contract, and a Runtime tab for an APK component

Android forbids one app reading another's logcat, so an APK this server
delivers has had no way to say what it did to somebody holding the phone
and nothing else. It can now expose its own bounded log through a
ContentProvider at `<applicationId>.devlog`, guarded by a permission
declared here; README.md's "An app's own log" is the whole contract, and
any project this server delivers can implement it.

The phone reads that provider while the Runtime tab is open and forwards
what is new into the component's runtime log on this machine, so the tab
renders from the same store a service's does and the history outlives the
phone. `LogKind::Runtime` stays one kind with two sources rather than
growing a third, and this server parses nothing -- what arrives is one
line of text each, appended, exactly as a service's stdout is.

The log button is now unconditional, like the gear beside it: with the
tab able to say which of several reasons there is nothing to read, its
absence was the one thing that could not say anything at all.

Supersedes ai-app posting its ring to ai-server over the tunnel, which
put a phone's lines under the wrong component and only ever worked for
that one project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 16:43:42 -04:00
1 parent 3727c7c67b
commit 013d7116d7
11 files changed
+772 -44

No files matched your search

@@ -1743,6 +1743,7 @@ private fun AppListScreen(
},
onRemove = {
forgetVariants(context, entry.key)
forgetDevLogCursors(context, entry.key)
manage(entry, removes = true) {
removeApp(entry.key)
}
@@ -3392,9 +3393,13 @@ private fun ComponentCard(
// whatever the text does. Unconditional, so the settings
// button sits in the same place whether or not there is a
// log beside it -- the log's own absence must not move it.
if (component.hasLogs) {
IconGlyphButton(LOG_GLYPH, "Show ${component.name}'s log") { showingLog = true }
}
// Unconditional like the gear beside it, and for the
// same reason: a component always has both kinds of log
// to ask about, and a button that comes and goes makes
// its own absence the answer. There is nothing here yet
// and nobody has looked are different things, and the
// dialog is where the difference gets said.
IconGlyphButton(LOG_GLYPH, "Show ${component.name}'s log") { showingLog = true }
// Always drawn, including for a component with a single
// build mode and nothing to strip: what a component can be
// told is part of what it is, and a control that comes and