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:
1 parent
3727c7c67b
commit
013d7116d7
11 files changed
+772
-44
No files matched your search
@@ -534,6 +534,73 @@ an override anyway. It stays this server's business rather than the build
|
||||
script's because it is a property of *this* hop to a phone: a project
|
||||
built without dev-updater has no reason to strip.
|
||||
|
||||
## An app's own log
|
||||
|
||||
A `Server` component's runtime log is what its service script reports. An
|
||||
`Apk` runs on the phone instead, where Android forbids one app reading
|
||||
another's `logcat` — so an app delivered here has no way to say what it
|
||||
did to somebody holding the phone and nothing else. The way out is for the
|
||||
app to keep a bounded copy of its own recent log and hand it over on the
|
||||
device: the two are already on the same phone, so this needs no tunnel, no
|
||||
token and no second enrolment.
|
||||
|
||||
That is a **contract any app served here can implement**, not a feature
|
||||
for one project. Implement it and that component gets a **Runtime** tab
|
||||
beside its build log, showing the same view a service's runtime log gets.
|
||||
|
||||
Expose a `ContentProvider`, exported and read-only:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| authority | `<applicationId>.devlog` |
|
||||
| read permission | `dev.updater.permission.READ_DEVLOG` (`android:readPermission`) |
|
||||
| `content://<authority>/lines?since=<seq>` | every held line with `seq >= since`, ascending: `seq INTEGER, t_ms INTEGER, level TEXT, target TEXT, message TEXT` |
|
||||
| `content://<authority>/status` | one row: `held INTEGER, dropped INTEGER, newest_seq INTEGER` |
|
||||
|
||||
`insert`, `update` and `delete` throw — there is nothing for anyone else to
|
||||
change. `t_ms` is unix milliseconds from the app's own clock, because a
|
||||
line is timestamped when it happened rather than when it was read.
|
||||
`dropped` is what the app's own bound discarded, counted rather than
|
||||
inferred, so "the log starts here" and "the log was cut off here" can be
|
||||
told apart. `newest_seq` is `-1` for a log nothing has been written to,
|
||||
and it is also what makes a restart visible: an in-memory log starts again
|
||||
at zero, and a reader whose stored cursor is now past the newest sequence
|
||||
starts again from the beginning rather than silently skipping everything
|
||||
since.
|
||||
|
||||
The authority is derived from the `applicationId` rather than written
|
||||
down, so a project's debug and release builds — installed side by side
|
||||
under different ids — each get their own and cannot read each other's.
|
||||
|
||||
The permission is declared by Dev Updater at `protectionLevel="normal"`.
|
||||
`signature` is not available: the updater and the apps it delivers are
|
||||
built on one machine but signed with *different* locally generated keys,
|
||||
so a signature permission would be held by nothing at all. The cost of
|
||||
`normal` is real and worth saying plainly — on a phone with Dev Updater
|
||||
installed, any app that requests `dev.updater.permission.READ_DEVLOG` by
|
||||
name can read another app's dev log. These are development builds on a
|
||||
development phone, and the alternative was no log at all.
|
||||
|
||||
Dev Updater's side of it: while the Runtime tab is open it asks the
|
||||
provider once a second for what it has not seen, and forwards those lines
|
||||
to this server, into that component's runtime log. It is a poll rather
|
||||
than a `ContentObserver` because the contract does not oblige a provider
|
||||
to call `notifyChange` — implementing it should be cheap. Forwarding
|
||||
rather than rendering straight from the provider is what makes the history
|
||||
outlive the phone and what lets one tab render both kinds. The lines
|
||||
arrive here already formatted, one string each; this server appends them
|
||||
and parses nothing, exactly as it does with a service's stdout, so an app
|
||||
can change its own log format without anything here being taught about it.
|
||||
|
||||
A component with no provider still gets the tab, and the tab says which of
|
||||
the several reasons there is nothing to read — the app is not installed,
|
||||
it exposes no devlog, its provider refused us, or it simply has not logged
|
||||
anything yet.
|
||||
|
||||
The reference implementation is iris's
|
||||
`iris/android-app/app/src/main/java/dev/iris/android/demo/DevLogProvider.java`
|
||||
in `~/repos/ai-app-2` (the `rustify` branch), over a ring in Rust.
|
||||
|
||||
## Testing
|
||||
|
||||
The Kotlin half has no unit tests, but it does have two checks that
|
||||
|
||||
Reference in new issue
Block a user