Skip to content

Upstream - #18

Open
superturtlee wants to merge 41 commits into
superturtlee:upstreamfrom
JingMatrix:master
Open

Upstream #18
superturtlee wants to merge 41 commits into
superturtlee:upstreamfrom
JingMatrix:master

Conversation

@superturtlee

Copy link
Copy Markdown
Owner

No description provided.

JingMatrix and others added 27 commits July 25, 2026 06:32
Gradle 9.6 removed the internal API that AGP 8.13 relied on, forcing the
AGP 9 upgrade and the surrounding build-script changes.

Toolchain:
- Gradle wrapper 9.3.1 -> 9.6.1; AGP 8.13.1 -> 9.3.1; apksig follows AGP.
- Adopt AGP 9 built-in Kotlin: remove org.jetbrains.kotlin.android from all
  Android modules; Kotlin stdlib 2.3.10 -> 2.4.10.
- compileSdk/targetSdk 36 -> 37, build-tools 37.0.0.

Build-script migration:
- Root build configures the shared CommonExtension through its getters, since
  AGP 9 dropped the action-DSL methods on that type.
- daemon generates SignInfo through androidComponents.onVariants and a typed
  task; android.applicationVariants was removed.

Resource generators (replace the rikka autoResConfig / materialthemebuilder
plugins, whose entry points use removed AGP variant APIs):
- buildSrc/GenerateLangListTask scans the translated locales.
- buildSrc/GenerateMaterialThemeTask computes the accent-color theme overlays,
  reusing the materialthemebuilder color library without applying its plugin.

Drop android.nonFinalResIds=false:
AGP 9 enables optimized resource shrinking by default, and that shrinker
requires non-final resource IDs. With isShrinkResources=true the manager's
release build now fails :app:minifyReleaseWithR8 with "Optimized resource
shrinking requires non-final IDs". AGP offers two remedies: make the IDs
non-final, or opt out of optimized shrinking (r8.optimizedResourceShrinking=
false). We take the former, because the false setting turns out to be dead
weight:
- It was added in 348f049 (Aug 2023), an unrelated "show packagename" feature
  commit, as a one-line drop-in beside the now-removed experimental flags
  enableAppCompileTimeRClass / enableNewResourceShrinker.preciseShrinking. Those
  siblings were cleaned up later; this line was simply missed.
- Final IDs are only actually required to use R.* as Java switch/case labels.
  There are zero `case R.*` occurrences in the tree at 348f049 and at every
  commit since, so the flag never protected anything here.
- Non-final IDs are the modern AGP default, so removing the line (rather than
  writing =true) expresses the intent with no config at all.
It also builds smaller: the optimized shrinker trims the release APK from
~3.45 MB to ~3.13 MB (~9%). Verified end to end -- assembleRelease plus a
zygisk installKsuAndReboot run that loads the module and starts lspd on device.

Formatting task:
- Add buildSrc/src/main/kotlin to the format task so the generator sources are
  formatted with the rest of the Kotlin build logic.
- Exclude daemon/**, which is intentionally kept on ktfmt's default (Meta)
  style; formatting it here fought :daemon:ktfmtFormat and flipped the style
  back and forth.

Dependencies:
- AGP/apksig 9.3.1, Kotlin 2.4.10, androidx.core 1.19.0 (dependabot maven group).
- coroutines 1.11.0, okhttp 5.4.0, gson 2.14.0, nav 2.9.8, glide 5.0.9,
  androidx activity/browser/annotation, ktfmt 0.26.0.
- Material kept at 1.12.0; 1.13+ removes the colorPrimary/colorError attrs the
  manager references.
- Submodules fmt and commons-lang bumped; CI action versions bumped
  (actions/checkout 6 -> 7, actions/cache 5 -> 6).
Android 17 (API 37) reshaped the IServiceConnection callback and dropped the
old overload instead of keeping both:

    void connected(in ComponentName name, IBinder service,
                   in @nullable IBinderSession session, boolean dead);

ManagerGuard overrode only the three-argument form, so as soon as system_server
dispatched the new transaction the Stub landed on an abstract method and the
daemon died with AbstractMethodError, taking the manager session down with it.
Only the Xiaomi XSpace workaround binds this connection, which is why the crash
was reported on HyperOS first; the interface change itself ships in stock
Android 17 and is not vendor specific.

Declare both overloads in the IServiceConnection stub and override both in
ManagerGuard, so every supported release finds the method system_server
dispatches. IBinderSession is stubbed as an empty interface, since the type is
only referenced by the descriptor of the new overload.
Brings the framework in line with the API version master vendors, checked
throughout with test modules that assert the documented behaviour and log
pass/fail, on a Pixel 6 running Android 17.

hookClassInitializer never worked: it aborted the process, and once that was
fixed the hook still could not fire, because resolving <clinit> through JNI
runs the initialiser during the lookup. It finds the method from ART's layout
now — the gap the reflected members leave behind — and the hooker runs ahead of
the class's own initialiser.

Also fixed: the interceptor chain could resurrect an exception it had already
suppressed; ExceptionMode.DEFAULT ignored module.prop, so passthrough was
unreachable; getInvoker() threw NPE on an unhooked method; Constructor
.newInstance was hookable; an unhooked constructor dispatched Method.invoke's
id against itself; getArgs() was mutable; a late-injected system server
dispatched onSystemServerStarting into an empty module set; android:ui was
reported to modules as the system package; edit().clear() did nothing, and
preference updates reached every Android user's hooked processes; empty scope
requests never called back; openRemoteFile threw RemoteException where
FileNotFoundException is documented; getScope() repeated a package once per
user; module.prop was not parsed as Properties.

In the manager, one malformed module.prop could blank the entire module list.
The list now shows which API each module targets, and staticScope is enforced
rather than parsed and ignored — in the picker, in the daemon, and by dropping
stale rows at startup.

Two changes modules will notice:

- Invoker.invoke reports the target's exception wrapped in
  InvocationTargetException on every path, as Method#invoke does. A module
  catching the raw exception will stop catching it.
- A module declaring staticScope loses scope entries outside its scope.list.

The <clinit> lookup is measured on one device, one Android version and one
architecture. Its assumptions are re-checked at runtime and it declines rather
than guesses, so an unfamiliar layout degrades to "no static initializer"
rather than misbehaving.
Since #648 the `ServiceManager.addService` call that claims the `serial` name
sat inside the `SDK_INT >= R` branch that exists only for
`registerForNotifications`, so on pre-R nothing claimed it and the Zygisk module
aborted the injection. Keep only `registerForNotifications` behind the check.

`startActivityAsUserWithFeature` is also R-only, and both call sites used it
unconditionally; the `NoSuchMethodError` escaped `onTransact` and killed the
daemon. Route them through `startActivityAsUserCompat`, like
`registerReceiverCompat`.

Closes #773.
An Actions artifact cannot be downloaded without a GitHub account: measured
against this repository, `GET /actions/artifacts/<id>/zip` answers 401 to an
anonymous caller while a release asset answers 206. Testing a canary is the
lowest-friction way for an ordinary user to help, and asking each of them to grant
an OAuth app something first — to work around where the zips happen to live — is a
real cost for a project whose users are careful about what they install. It also
excludes the users who cannot reach GitHub's login page at all, who are exactly
the ones a canary programme loses first.

Each push to master, and each manual run, now attaches the same two zips to a
`canary-<versionCode>` prerelease. Prerelease, so `releases/latest` — which is what
update checks read — keeps pointing at the last stable tag. Artifacts stay: they
also carry mappings and symbols, which are for us rather than for testers.

Five are kept. They are pruned by version code rather than by date, because the
version code is the commit count and therefore monotonic, while dates can be
disordered by a rerun or a revert. The release is deleted and recreated rather
than edited, so re-running the workflow for a commit replaces that build instead
of appending a second copy of every asset to it.

The job gains `contents: write`, which it did not have; everything else in it only
reads.
The parasitic manager lives inside com.android.shell, which has no INTERNET
permission before Android 12, so preAppSpecialize appends the INET group to its
gid array. That satisfies setgroups() and nothing else: once
nativeForkAndSpecialize returns, Zygote#forkAndSpecialize runs
setAllowNetworkingForProcess(containsInetGid(gids)) against the array it passed
in, not the one we substituted, and turns networking off in libnetd_client.
socket() and dns_open_proxy() then return EPERM whatever groups we belong to,
which libcore reports as "Permission denied (missing INTERNET permission?)" —
an unchecked SecurityException that killed the OkHttp dispatcher and the manager
with it, and left the Repository tab empty.

So overwrite the first entry of the caller's array too; it has no further use
once specialization is done. LSPosed did this from the commit that introduced
the parasitic manager, and the line was dropped in the rewrite for the new
Zygisk architecture. Android 12 and later were never affected: com.android.shell
declares INTERNET there, so the array already carries the group.

Fixes #636
`:app` is deleted, not deprecated — 218 files, and no longer in
settings.gradle.kts. `:manager` takes its place: 86 Kotlin files, Material 3,
one activity, because parasitically every activity has to be tracked by hand by
the zygisk hooker. Launching it has not changed: still injected into
com.android.shell, still reached through Constants.setBinder.

Most of the branch is screens the old manager did not have. The framework
updates from inside the app, canaries included. Home shows the project's commit
history rather than a status light. The log screen indexes byte offsets and
pages a window instead of loading four megabytes of strings into a process
whose heap belongs to com.android.shell, and reaches the daemon's rotated
parts. The Store runs off the mirrors that still answer. The scope editor
collects edits into a draft and writes them once, where the old one rewrote a
module's whole scope on every checkbox tap. System status states what is
running and copies itself in English whatever the phone's language.

Daemon-side, less: ModuleDatabase owns every configuration read and ConfigCache
holds no SQL, so "enable this module" and "what is enabled" agree rather than
race a rebuild. The AIDL gained the transactions the new screens need, and
ROOT_UNKNOWN takes 0 — a binder proxy answers an unimplemented transaction with
a default, so ROOT_NONE at 0 meant an old daemon told a rooted user to install
the root manager they were already running. The two ship in one zip.

Eighteen languages, crowdin.yml repointed from app/ to manager/, right-to-left
included.

No automated tests, because the repository has none, and CI runs `zipAll`. It
was verified on a device, screen by screen, checking each claim against what
the daemon had actually stored. That found real bugs late — a comment asserting
the daemon force-stops apps on a scope write, which it has never done; a filter
that hid the rows a reader had chosen. Expect a few more.
OkHttp 5's Android artifact keeps the public suffix list in assets/, reached
through a process-static Context set from androidx.startup. Parasitically the
manager's manifest is never installed, so that provider never runs and
DnsOverHttps -- which asks the list whether a host is private before opening
any socket -- died with "Unable to load PublicSuffixDatabase.list" on the first
lookup. Coil was already hand-initialised for this reason; OkHttp was not.

Two things made that fatal rather than degrading: VectorDns caught only
UnknownHostException, so an IllegalStateException escaped it and the fallback
never engaged; and canaryBuilds and frameworkReleases called get() unguarded,
with no CoroutineExceptionHandler anywhere in the manager, so the throw landed
on the main thread.

Coil's own initialisation then moves out of MainActivity to
ServiceLocator.attach, for the same reason OkHttp's did: the debug demo host
never opens MainActivity, so it had no image loader at all.

Separately, for diagnosis: off, bypassed by a proxy, and latched onto the
system resolver all looked identical from the sheet, so VectorDns now records
what each lookup did and the sheet renders it, with a way to clear the latch.
Two fixes fell out:

- `direct` was a lazy val, so the proxy check ran once per process and joining
  a VPN mid-session was invisible. Read per lookup now.

- The failure line named the host being resolved, which UnknownHostException
  already carries as its whole message -- so it printed twice while which way
  it failed went missing.

Finally, a crash record is discarded when another build wrote it. A record
outlives the build that made it, so the status card kept showing a crash the
running build had already fixed -- which is what #799 was answered with: five
traces from the build before the one that fixed them.

Fixes #799
Every row on PackageActionSheet dismissed the sheet and then launched its
work on rememberCoroutineScope(), so the composition left on the next frame
and took the scope with it — the coroutine died at the first withContext hop
inside DaemonClient, usually before the binder transaction was made. Nothing
logged, onResult never ran, and whether the work beat the frame was a race,
which is what made it look flaky. The actions now run on
ServiceLocator.appScope with Dispatchers.Main. App info, force stop,
re-optimize, uninstall and soft reboot were all affected; the Scope screen's
companion button was not, since it goes through viewModelScope.

The sheet also did not read as one list. A Material list item paints its
container surface while a sheet is drawn on surfaceContainerLow, so every
list item on a sheet now takes the transparent sheetRowColors — the mute
switch here, and the log settings, batch update, asset picker and framework
versions sheets. The mute switch was the only row built from the generic
ToggleRow, so it gains the sheet's own shape through ActionRowLayout and
keeps announcing itself as a switch; uninstall drops DeleteOutline for
Delete to match the filled glyphs around it; and "not in the store" no
longer ripples under a thumb.

Fixes #810
Four unrelated fixes.

The mapping and symbols artifacts have pointed at paths that stopped existing
in #796, so both uploaded empty.

check_translations.py had no caller; it runs in CI now and catches bare %s and
%d, which fixes three defects it found.

zygisk/update.json named a zip the release does not attach, so the v2.0 update
has always failed to install. Old repository paths go with it.

And the daemon translations: 18 machine-translated product names, a handful of
real defects, and 39 locales saying "Xposed module" where we say "module".
On a Huawei MatePad 11 the manager never opened. A LoadedApk for the host package
is already cached by the time bindApplication arrives, so
ActivityThread#getPackageInfo returns that instance and never looks at the
ApplicationInfo we just swapped in. Its one repair path, updateApplicationInfo, is
gated by isLoadedApkResourceDirsUpToDate, which compares nothing but the resource
and overlay directories -- and getManagerPkgInfo copies both from the host, so
sourceDir is never picked up. The process then runs with mResDir on the stock
Shell.apk, and because its mApplicationInfo is a different object than ours, the
identity check in the getClassLoader hook skips the DEX injection and MainActivity
cannot be found.

Dropping the entry from mPackages and mResourcePackages forces a fresh LoadedApk,
built from our own ApplicationInfo, which the identity check then accepts. That
check is left as it is: matching on the package name instead, as this patch first
did, would also match the stale LoadedApk and the resource-only one out of
mResourcePackages. A freshly forked process has an empty cache, so the eviction is
a no-op everywhere else, and the warning it logs when it does remove something is
the only evidence that the pre-warming is real.

Verified by the reporter on the affected tablet, and on a device without it.
A legacy module reports being active by hooking a method in its own app, so it
has to be in its own scope before it can say anything at all. The View-era
manager added that row on every save and hid it again on read; #796 dropped
both halves, and every legacy module has reported itself inactive since.

ConfigCache derives that scope during its rebuild rather than storing it, so
configurations written by those builds need no repair and nothing that replaces
the scope table can drop it again. Legacy is the loader's own verdict, so a
module built against API 101 keeps its own process to itself.

The scope screen shows the derived row: ticked, exempt from every filter,
grouped with what is in force, and closed to the toggle and to both bulk
actions. It never enters the draft, so an apply can neither write nor delete a
row the scope table does not own.

Fixes #816.
Android 17 refuses every reflective write to a static final field: `Field_set`
calls `ThrowIAEIfFieldIsNotOverwritable` before it looks at the accessible flag,
and `ArtField::IsUnmodifiable` lets one through only for a process targeting SDK
36 or lower. Clearing the reflective copy's ACC_FINAL does not help, an
unreflected VarHandle is read-only, and Android's `Unsafe` has no static field
accessors -- which left `XposedHelpers.setStatic*Field` dead for every legacy
module on 17, `android.os.Build` spoofing included.

`HookBridge.makeFieldWritable` clears ACC_FINAL where the check reads it and the
setters retry through reflection, so the value, the conversions and the
exceptions stay reflection's. ART's own JNI `SetStatic*Field` is deliberately not
taken: `EnsureModifiable` is `LOG(FATAL)` for a field it holds unmodifiable, so a
write outside the `android.os.Build` carve-out aborts the process. The ArtField's
access flags are checked against `Field.getModifiers()` before anything is
written, so a runtime that lays them out differently is left alone and the caller
keeps the `IllegalAccessError` it already had.
…om (#809)

Flash Vector, open it from the root manager, close it, and there is no way back
in. The dialer code registered a filter with no action, which matches nothing, so
that branch has been dead since #597; it works again and is rebound to 832867,
VECTOR on the keypad. Parasitically the manager is not installed, so the launcher
has nothing to show either: the pinned shortcut and the standalone install that
#796 dropped return, in an "Opening Vector" section and a first-launch prompt,
with `getManagerApk()` handing over the APK the host cannot read. Sixteen new
strings, translated into all eighteen languages.

The version string now names where a build came from rather than calling every
canary "dirty", which it did only because the workflow writes signing credentials
into the tracked `gradle.properties`. Repository and commit both come from the
pull request's head, since GitHub's defaults describe the run and not the code.

Closes #815.
#809 gave the stamp a second half and left `divergesFrom` comparing the whole
string. A canary reports `JingMatrix-Vector-93d66473`, no release SHA starts
with that, so every canary was called "same number, other build" against the
release it had just been flashed from, and no row was ever marked installed.
The `-dirty` test it still carried had become unreachable in the same commit.

The stamp now leads with the commit, `93d66473-JingMatrix-Vector`, so reading it
back is a prefix and nothing more. A modified tree is marked `+` rather than
`-`, in semver's sense of build metadata: after a `-` is a repository holding
this exact commit, after a `+` are changes no repository holds. `buildStamp`
takes one apart and `isCommit` compares as a prefix in either direction, since a
stamp carries git's short form and a release the full SHA. Where a build was
made is not compared — a fork at the same commit builds the same code. A stamp
naming no commit, including the shape published between #809 and here, reads as
"I cannot tell" rather than as divergence.

On the status page the commit keeps the size it is read at and the rest is set
smaller and muted; copying the page still yields the whole stamp. In the
versions sheet the status has a width of its own, so the clause on a divergent
row no longer takes the room the build's name needs and wraps that row alone.
Refuse to hook Object.getClass

R8 compiles Kotlin's parameter null checks into `obj.getClass()`, and one of them is
the first instruction of `VectorNativeHooker.callback`. The dispatch therefore calls
`Object.getClass` entering every hooked method, and a module that hooks it re-enters
the dispatch from its own prologue until the stack is gone, before any hooker runs.
There is no recovery: lsplant marks a hooked method non-compilable, the framework dex
never gets an oat file, and `Throwable.toString` calls `getClass`, so reporting the
StackOverflowError raises another one.

It is refused at both registration paths, alongside `Method.invoke` and
`Constructor.newInstance`, which are refused for the same reason. The
`IllegalArgumentException` costs the module that one hook and nothing else.

AGP 9 made this certain rather than possible: from the same source the Release
framework dex holds 44 `Object.getClass` call sites at 3043 and 246 at 3048, and the
one in `callback` moved from a cold branch to instruction zero. The hook was already
fatal -- KiminonawaResa/HyperLight#193 is the same launcher loop on 3044. What
changed on the module side is libxposed API 101, which offers no `hookAll*` helper;
the module that hit this walks `Class#getMethods()`, which always lists the
`getClass` inherited from `Object`. The legacy helpers walk `getDeclaredMethods()`
and cannot reach it.

Verified on a Pixel 7a with a module transcribed from the decompiled one: on master
the target dies and restarts with `failed to complete startup`; here the refusal is
logged and the process stays up.

The messages for the other blocked hooks -- abstract methods, framework-internal
methods, `Method.invoke`, `Constructor.newInstance` -- now say why rather than only
that it failed.

Fixes #798.
Three of these are what borrowing `com.android.shell`'s uid costs the manager.
Below API 33 `ContextCompat.registerReceiver` demands
`<package>.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION`, which under the host is
nobody's, so every store install threw before `commit()`; both installers
register by hand now, under a UUID-named action. `createSessionInternal` skips
`INSTALL_REPLACE_EXISTING` for `SHELL_UID`, so a module already on the device
failed with `ALREADY_EXISTS` -- that branch is unchanged from API 27 to AOSP
main, so a store update has never worked parasitically. And `AwSettings` reads
`checkSelfPermission(INTERNET)` in its constructor, which AOSP's Shell did not
request until android-12, so every in-app page failed as `ERR_CACHE_MISS` on a
device whose networking is fine; the context a WebView is built with now answers
that one question, and only where the platform says no.

The fourth is that an offer is decided by numbers a git tag states, which nothing
obliges to be the ones in the APK's manifest, and no rule over `(code, name)` can
bridge that: a module that never changes its tag code is only ever seen to update
through the name clause, and one that reuses a versionName across several codes
only through the code clause. So the Store stops inferring and records instead --
the release it installed, and the version the device reported once it was on --
read in `StoreEntry.upgradable` so every count, filter and badge agrees. Where an
offer names the version already installed it is worded as a reinstall rather than
as an update from a version to itself, which is true whether the tag disagrees
with the manifest or the release is a genuine rebuild. All eighteen locales are
filled in.

Fixes #823.
)

A module loaded into system_server cannot dlopen a library out of its own
APK. Everything under /data/app is apk_data_file, and while system_server
may read and map such a file it may not execute it, which AOSP states
outright and forbids granting: "Executable files in /data are a
persistence vector". Every app domain does hold that permission, so the
same module loads the same library without trouble in an ordinary process
and fails only here, with the linker refusing the first PROT_EXEC mapping:

  dlopen failed: couldn't map ".../base.apk!/lib/arm64-v8a/libhmahook.so"
  segment 2: Permission denied

The way past it is not a new rule but the one this module already ships.
xposed_data is a type we declare ourselves, outside the data_file_type
attribute that neverallow is written against, and the existing
`allow * xposed_data {file dir} *` already reaches every domain,
system_server included. So the daemon now copies a system_server-bound
module's libraries into the misc directory it already owns and labels,
and hands the loader that directory to search first. Nothing is staged
for any other process: they can execute straight out of /data/app, and
staging for them would buy nothing but disk.

The copy is keyed to the APK's size, mtime and the framework version, so
an updated module is re-extracted rather than left running superseded
native code, and directories belonging to modules that are no longer
bound for system_server are dropped.

Staging deliberately ignores moduleLibraryNames: that list only names the
libraries whose native_init we are asked to call, and a module may load
its own libraries without declaring any -- the module that prompted this
does exactly that.

While here, register the declared native entrypoints before the entry
classes run instead of after. A module may load its libraries from its
constructor or from onModuleLoaded, and an entrypoint recorded afterwards
is one the dlopen hook has already missed. The legacy loader has always
done it in this order.
…ped (#832)

Defects across the daemon and the manager sharing one cause: the daemon writes or
announces something, and the manager either never hears it or overwrites it.

Scope
- An empty static scope no longer fixes the scope at "no apps at all". readStaticScope
  answered an empty set, so setModuleScope refused every write and pruneScopeToClaimed
  deleted the module's rows on each cache rebuild.
- A static scope fixes which apps may be listed, not which of them are chosen. The rows
  are editable and any subset is stored, which the daemon already accepted.
- The editor sends the user's diff applied to a fresh read rather than a whole-set
  replace, and re-reads on resume, so a scope approved elsewhere is no longer deleted.
- A row unticked in the list stays in the list.

Notifications
- One scope prompt per requested package, each independently answerable, bounded at
  sixteen unanswered per module. Previously they shared a tag and replaced one another,
  so only the last was ever answerable and the rest were never answered at all.
- The delete intent and one-hour timeout dropped in 714e6c8 are restored, so a
  dismissed prompt answers the module instead of leaving it waiting indefinitely.
- "Never ask again" withdraws the module's other prompts, and uninstalling undoes it.
- The "not activated yet" notice is cancelled once the module is activated.

Manager
- module:// launch intents open that module's scope editor again, lost in #796, and a
  rebuilt activity no longer replays them.
- Package events in other users reach the manager, via the daemon's own broadcast.
- The "no way back in" prompt waits until the answer is known.
- A framework flash survives leaving the screen; a download can still be abandoned.
Source patterns. Within a path node the CLI compiles `*` to `.+` and applies the
result as an anchored match, so a `*` must consume at least one character. A
pattern of the form `strings*.xml` therefore selects only names that carry
something between the stem and the extension, never the bare `strings.xml`. A
file that no pattern selects never enters the candidate set, so the upload
neither includes it nor reports it as missing: the failure is silent by
construction. Neither `**` nor `?` closes the gap — `**` is special only as a
whole path node, and `?` adds a further required character. Naming each source
file is the only formulation without such a hole.

Locale placeholders. %android_code% is region-qualified for every language,
whereas an Android resource folder carries a region only where one is needed to
disambiguate a variant. Aligning folders with it would require one mapping entry
per language to strip the region back off — an obligation that grows with every
language added and degrades silently when forgotten, since translation upload
derives one expected path per target language and skips the ones it cannot find.
%two_letters_code% inverts the default: it already agrees with the unqualified
folders, so only the genuinely region-qualified locales, plus the codes Android
retained after the ISO renames, need an entry. Those are now written down in the
repository rather than left implicit in project state that no checkout can see.

The mapping is repeated per file because it is a file-level key; a YAML anchor
keeps the four entries identical by construction.

The `translation` value doubles as the file's server-side export pattern and is
expanded there, where the local mapping does not apply, so the same overrides
must exist in the project's language settings for downloads to resolve. Nothing
in CI observes this, as it does not download translations.

The workflow trigger now names the same files instead of globbing them. Its
matching rules and the CLI's are defined independently, and a trigger that
quietly never fires is the worse failure of the two.
Two unrelated fixes.

A process no module has in scope produced three log lines about it, and one
called the daemon's ordinary refusal a failure. The duplicate in
`ipc_bridge.cpp` goes; the two in `module.cpp` now say what happened. The
wording stops short of "out of scope" because the zygisk side cannot tell:
`BridgeService.onTransact` also returns false when the daemon binder has not
arrived yet, or when the process is already registered. The daemon logs the
reason itself.

`sendToBridge` leaves the main thread at euid 1000, and the statement after it
read the verbose-log preference out of the config database, which sits under a
directory only root can enter. That normally works because a binder thread opens
and caches the handle during specialization first, but only when the injection
succeeded. When it failed nothing had, and the daemon died on the preference
read instead of carrying on — the second half of the crash in #744 and #773.
Reading it before `sendToBridge` leaves the check where it was and does the open
while we still have root.
android.util.Log drops a throwable's trace whenever an
UnknownHostException is anywhere in its cause chain, so our DoH, store
and GitHub failures logged a message and nothing else. Traces are
formatted here now and appended to the message, on the unfiltered
Log.println, for the manager, the daemon and the legacy bridge.

The log panel then lost half of each one by guessing continuations from
indentation. It goes by the writer instead: logcat.cpp puts the prefix
in the first iovec, so an unprefixed line under an entry continues it,
bar the four the daemon writes raw. The one exception rejoins a message
printlns cut in two, on tag, process, thread, level and adjacency.

One renderer draws every trace -- our frames marked, the platform's
dimmed, Caused by as a divider, each frame tappable to copy. The crash
card states what threw, what it said, the nearest frame that is ours and
when, with the trace a tap away.

Sixteen new strings across eighteen locales; fifteen dead imports gone.
GrapheneOS ships a "Restrict dynamic code loading" exploit-protection setting
that is immutable and enabled for system apps. The parasitic manager runs
inside com.android.shell, a system app, so GrapheneOS forbids it from loading
the manager's DEX and the manager never starts.

GrapheneOS enforces DCL through two channels, both fed by the same per-app
verdict, and each has to be cleared at that shared source rather than at the
symptom:

  - The ART DexFile checks (DynCodeLoading.getAppBindFlags) reject the
    manager's transplanted /proc/self/fd DEX as "DCL via storage".
  - The kernel grapheneos_flags written at zygote specialize keep
    DENY_EXECMEM/DENY_EXECMOD set, so LSPlant/Dobby cannot make its inline-hook
    trampoline executable and the process takes a SIGSEGV
    (TSEC_FLAG_DENY_EXECMEM: op denied). The manager's isolated WebView process
    likewise keeps DENY_EXECMEM, disabling Chromium's JIT.

getAppBindFlags and SELinuxFlags.{get,getForWebViewProcess} all read
AswRestrict{Memory,Storage,WebView}DynCodeLoading.get(), which honours a
non-null getImmutableValue. So the hook is on getImmutableValue for all three
switches in system_server, returning false (allowed) for the single host
package and deferring to GrapheneOS's original verdict for every other app. A
non-null result takes precedence over both the user toggle and the default, so
this forces the setting to allowed regardless of the user's configuration.

The manager host ends up an ordinary DCL-allowed app: the DexFile checks are
off and the exec* SELinux flags cleared, while ptrace denial, hardened_malloc
and MTE stay intact. GrapheneOS is detected by the classes being present, so
the hook is a no-op on every other system; verified against the GrapheneOS 14,
16 and 17 branches.

The scope is intentionally narrow: only com.android.shell is affected, and
only in system_server, where the value is computed. Modules are excluded --
a normal app already exposes a user-configurable DCL toggle on GrapheneOS and
can be allowed without a patch.

Supersedes #711 with a smaller footprint: the GRAPHENE_SETTINGS_PACKAGE_NAME
build variable is dropped and the hook resides in its own class. The technique
was originally developed by @Enovale in #711 and in discussion #340.

Co-authored-by: Enovale <17408285+Enovale@users.noreply.github.com>
Long-press the navigation container to enter edit mode: drag panels to
reorder, tap a badge to hide or restore one. Hidden panels stay
registered as destinations, since a saved back stack may still name
them; only the container stops drawing them. One panel always remains
visible. Works on both the bottom bar and the rail.

Add an appearance-sheet option, off by default, that replaces the bar
with a draggable ball. The navigation suite type becomes None, so the
container is never laid out and the strip it costs every screen returns
as content. Long-press the ball to fan the panels into an arc and
release on one; a plain tap latches the arc open so each panel is an
ordinary target, which is the path a screen reader can use. Drawn inside
the app window, never a system overlay -- the parasitic manager runs as
com.android.shell and must not request SYSTEM_ALERT_WINDOW.

The arrangement persists as one ordered string of route keys, hidden
ones flagged, tolerant of unknown or duplicate entries. The ball's edge
and height persist likewise; ball and arc are positioned in absolute
window coordinates so both render correctly under RTL.

Sixteen strings across eighteen locales.
The GitHub sign-in came from a `githubClientId` Gradle property set nowhere
-- not in gradle.properties, not in CI -- so every build compiled
GITHUB_CLIENT_ID as "". isConfigured was therefore always false: the card
returned before drawing anything, signIn() short-circuited to Unavailable,
and tokenProvider() always returned null, so no request ever carried an
Authorization header. It bought only the rate limit, 60 requests an hour
anonymous against 5000 signed in, and nothing depends on having it: the
canary screen sources its zips from release assets precisely so that testing
a build needs no account.

Deleted rather than given a client id, because the flow also has nowhere
good to keep a token. Parasitically the manager's SharedPreferences land in
com.android.shell's data directory, whose shared_prefs is world-executable,
so a token would sit outside our own sandbox. That is worth solving before
offering sign-in again, not alongside it.

The rest is what #796 left unreferenced. buildSrc held two Gradle task
classes no build script registers and whose outputs no longer exist -- the
locale list comes from BuildConfig.TRANSLATIONS now. The version catalog
still pinned the whole View stack: 26 aliases, the safeargs plugin, and the
nav, glide and appcenter versions. hiddenapi/stubs carried 20 files nothing
compiles against, with four HiddenApiBridge wrappers and the stub members
that existed only to type them. In :manager the four @serializable models
for the Actions runs and artifacts API became unreachable when eb07955
moved canary sourcing to release prereleases, and nine symbols --
relativeTime, levelLabel, sectionHeaderStyle, SeedScheme.parseHex,
VectorLightColors, VectorDarkColors, ModulesViewModel.selectAll,
LogPaneState.atOldest and GOOD_FIRST_ISSUE_URL -- have no call site at all.
Seventeen strings go with them, translations deleted alongside their source
so the next Crowdin pull does not add them back. Daemon-side,
CliHandler.isPackageInstalled, Utils.getZoneId, Utils.isLENOVO and :xposed's
trackedApks have no caller; the live tracker is LoadedApkTracker.activeApks.

One deletion ships behaviour. Before #550 the loader baked the raw
githubusercontent URL of magisk-loader/update/zygisk.json into every
module.prop, so Magisk on a pre-v2.0 install polls it and will now get a
404. Effectively everyone is on v2.0 by now, and zygisk/update.json -- what
module.prop has pointed at since -- is untouched.
…841)

The fmt and commons-lang submodules move to the tip of their upstream
branches, core-splashscreen to 1.2.0, navigation3 to 1.1.5 and material3 to
1.5.0-alpha25, which is everything in the catalog that was behind. Both
libxposed submodules stay where they are, since their only new commit is the
API 102 RFC and that belongs with the API work. In CI, ninja moves to 1.13.2
and crowdin.yml's floating action refs are pinned to majors like everywhere
else.

The NDK pin moves from 29.0.13113456, which is r29 beta 1, to r29 stable at
29.0.14206865. Its clang 21 rejects the lsplant x86 build over duplicate
<emmintrin.h> definitions arriving through two module fragments, so phmap's
SSE2 group scan is switched off -- for every native module, because the flag
changes phmap's layout and hook_bridge.cpp instantiates the same templates.

The rest is the seventy or so lines a build printed when nothing was wrong.
:hiddenapi:stubs no longer overrides its Java version to 8. Thirty-three
Kotlin deprecations in the manager are migrated: ListItem's headline becomes
a trailing content lambda, TabRow becomes PrimaryTabRow,
rememberModalBottomSheetState becomes rememberBottomSheetState, and eight
icons move to their auto-mirrored versions; the two reads with no
replacement are suppressed in place. Vendored and generated Java, along with
:legacy, compile with javac's notes off, and lsplant's string literal
operator template warning is silenced at its target. The `by extra` delegate
and AGP's srcDirs are rewritten, and the apksign plugin is replaced by the
signing configuration it was applying, since its last release still calls
the deprecated Project.getProperties.

Underneath that, lint was reporting calls the minimum cannot make. The
manager and the daemon both set API 27 and both reached for later methods,
which on 8.1 is a NoSuchMethodError rather than an opinion:
getLongVersionCode is API 28, FileObserver's multi-file constructor is 29,
and LocalServerSocket only implements Closeable from 28. Each is now gated
on the release that introduced it, checked against the platform's own
api-versions.xml, and two constants newer than the minimum are spelled out
rather than referenced. NewApi and InlinedApi findings go from twelve to
none, on both variants.

Signing deserved a second look with the plugin gone, and was verified both
ways: with a keystore the manager APK and the certificate compiled into
SignInfo.kt are the same one, and without, both fall back to the debug key
as before. A clean zipAll over debug and release now prints only its task
list, and the release zip runs on a Pixel 6 on Android 17.
Modules loaded but none of their hooks landed in release builds, while debug
builds were fine (#847, #848).

Some framework types extend super classes that no dex contains: they are
generated on the device, so they can inherit from whichever platform classes it
provides. Such a type cannot be resolved until that has happened, and a failed
resolution is permanent, so the fragility is transitive — every class naming one
acquires it.

R8 spread it beyond what the source shows. Each lambda becomes a class, and
classes of the same shape are merged afterwards, so `XResources`' two lambdas
put it inside the shared `Function` synthetic — the one commons-lang's
`ClassUtilsX` instantiates from its static initialiser, which
`XposedHelpers.findClass` calls. Every `findClass` in system_server failed for
the rest of the boot.

Both lambdas are now written out long-hand. Keep rules cannot state this
invariant, since they govern the classes you write rather than the ones an
optimiser invents, so `checkXResourcesIsolationRelease` reads the optimised dex,
resolves names through the mapping file, and fails the build if any class
outside resource hooking comes to name one of these types.

Saved bug reports also get one name, built in one place instead of three. Two
archives attached to the same report used to be indistinguishable until opened,
so the name now says the build type, and each format records the commit where it
can — the log zip in its comment, the module backup in a field of its own. The
version code is the commit count on master, so branch builds wear numbers they
were never built from.
JNI has a rule that is easy to forget: when a Java method throws, the exception
does not become a C++ exception. It stays pending on the thread, and the runtime
is entitled to abort at the next transition rather than let you carry on. So
every call into Java, and every lookup that can fail, owes an answer to "did
that throw" — and six files never asked.

The worst of them is `FindAndCall`, which hands a whole process to the
framework's Java entry and then inspected nothing. A throwing entry left that
process without Xposed, while the line printed immediately afterwards said the
framework had been injected. It now reports the failure and returns whether the
call arrived, and both callers say which happened. The rest are smaller versions
of the same thing: two lookups in `resources_hook` returned `JNI_FALSE` to Java
with `NoSuchMethodError` still pending, so a caller that asked for a boolean got
a throw instead; `RegisterNatives`, `LogcatMonitor`'s `refreshFd` lookup and
`dex2oat`'s string read did the same on their failure paths; and the obfuscation
map builder returned null on a failed `FindClass` without clearing, then fed two
unchecked method ids to `NewObject`.

Most of this is not new code but the lsplant wrappers we already have. They
clear the exception, log the Java stack behind it, and hand back scoped
references — which incidentally disposes of a local reference the obfuscation
map leaked per entry. An explicit check survives only where the caller has to
know what happened, because a wrapper clears the exception before anyone can
ask.

Two decisions worth recording. The stack is rendered with
`Log.getStackTraceString` rather than `ExceptionDescribe`, because the latter
writes to stderr and a process forked from the zygote has nowhere for stderr to
go — the trace would simply vanish. And `SetAllowUnload(false)` stays
unconditional: the ART and JNI hooks are installed before the entry runs and
their trampolines point into this library, so a failed entry is no reason to let
it be unloaded.

`hook_bridge` is deliberately untouched. It implements `Method.invoke` semantics
and has to leave a target's exception pending so it can wrap it in
`InvocationTargetException`.
JingMatrix and others added 10 commits August 3, 2026 09:56
Four changes, three of them cosmetic.

The monochrome icon was a flat blob traced round the winged victory, so
neither the themed launcher icon nor the status notification said what the
statue is. It now carries the statue's own line work, punched out of the
silhouette with evenOdd so the feathers and the drapery read at launcher
size while the shape stays solid at 24dp.

The circuit ambience routes its board on a lattice rather than scribbling
it: lanes are claimed, so no two traces share a line; a trace that meets
copper already laid steps aside and carries on to the edge rather than
stopping mid-header; corners are mitred into the two 45° elbows a router
leaves behind, with the vias on them. The signal is a swell of current with
a proper head — a tapering ribbon drawn down to nothing behind it — and a
vertical drag sets its speed, the gesture the rain and the snow already use.

The code rain takes its glyph size from how many columns fit across the
header — thirty at the largest, a hundred and twenty at the smallest, eighty
at rest — rather than a fraction of its height against a fixed column count,
which drew fifty-two streams at a size that fit thirty. Lanes are spread by
golden ratio so the field stops clumping, and columns are added and removed
between frames so a pinch never blanks a stretch of the header.

Last, parasitically the "Install as an app" row went green as soon as the
package existed, even when that copy was a different build. Version codes
cannot tell those apart, since they count commits on master, so the two APKs
are compared by digest instead and the row offers a reinstall. A comparison
that could not be made — a dead daemon, an unreadable install — leaves the
card as it was, and only a completed one is remembered.
…stent (#757)

API 102 is built around hot reload: replacing a module's code inside a process that is already running it. Three smaller capabilities make that coherent. A single entry class must be able to withdraw from lifecycle callbacks while its siblings carry on, or a module cannot retire part of itself. A hooker must be replaceable atomically, or every migration leaves a window in which the method is unhooked. And a module targeting this level may no longer reach the legacy API, which is global static state with no notion of generations, so anything reaching it leaves behind what a reload cannot clean up.

The motivation is the cost of iteration. Changing a module means killing every process it is injected into, and for one that hooks the system, a reboot — taking with it whatever state was being reproduced. Reloading in place is effectively free, and deliberate state can be carried across the swap. For users the same mechanism means an update can reach processes already running the old code, offered rather than imposed, since a module is asked first and may refuse.

The implementation follows from one decision: the framework already knows which module it handed to which process, so a reloadable target is derived from that record rather than registered by the process. This matters because the system server loads its modules before the framework's own bookkeeping exists, and any lookup there would fail silently; for the same reason the channel it calls back through carries no module identity. Reloads are asynchronous under a timeout the framework owns, since the callee runs arbitrary module code and the IPC layer imposes no deadline. A replacement generation is built fully before the previous one is disturbed, and the swap happens under the lock a call already takes, so a call in flight is answered by the generation it began with.

Most of this is IPC, so the interfaces now live in this project's own namespace and are documented. That is not tidying: several of their constraints cannot be inferred from the signatures, and violating one fails in a way whose symptom points elsewhere.

Hot reload also exposed how loosely the framework held the idea of a user. A module is one package and one binary for the whole device, so its configuration belongs to the package; what varies per user is only whether it is installed and what identity it is given there. Nothing enforced that, so a module present in one user could execute inside another user's applications. The invariant is now explicit — a module runs only in the users that installed it — with the system server the one principled exception, since it belongs to no user and every holder has equal claim. Granting and revoking scope became symmetric, module identity is stored at the granularity it is compared at, and queries answer with what the caller can act on.

Co-Authored-By: JingMatrix <jingmatrix@gmail.com>
On a release build, pressing install on a canary opened the newest release. The channel rule that keeps nightlies out of the update card also filtered the list an explicit request was resolved against, so the version code the canary list passed matched nothing and the selection fell back to the channel default. That rule now filters what is offered, not what can be asked for by name.

The canary page is rebuilt around the builds themselves. Each row is the build's head commit — subject wrapped, author credited, pull request in a fixed corner that opens the discussion — matched by SHA, falling back to the subject CI writes into the release notes. The header names the issues closed since the running build was cut, read from the issues endpoint filtered to `completed`: the Development panel closes issues without writing to any commit message, and the link itself exists only in GraphQL, which needs an account. The commit rail's marker shows where the running build sits, and a build wearing a canary's number without being it is told so.
Finishes the namespace move #757 started, doing the three things it deferred: the manager's interface, the Utils logger, and the daemon's implementation class names. /data/adb/lspd stays as it is — on-disk state wants a migration, not a rename.

ILSPManagerService and its parcelables become IManagerService, IFrameworkInstallReceiver, ScopeEntry and DeviceUser under org.matrix.vector.ipc. org.lsposed.lspd.util.Utils becomes org.matrix.vector.util, with its inner Log promoted to top level. ApplicationService and ModuleService take the names of the interfaces they implement.

45 methods become 39: three were dead, and getUnloadableModules plus getModuleLoadState collapse into one call returning a map, which drops the placeholder reason a lost transaction used to report as a missing APK. The hand-written transaction ids go too — they kept a number stable rather than a meaning, and getProtocolVersion, declared first and so transaction zero in every revision, guards that at the handshake instead.

The interface name is the binder descriptor, so a separately installed manager stops working until it is updated. It now says so rather than drawing blank screens.

Seven unrelated defects come with it, each its own commit: a discarded insert() result, a health flag latched before the work it reports, an unbounded wait on a binder thread, and four smaller ones.
Restoring the manager after its process had been reaped crashed, which is #871
-- and #834 before it, on Android 13. Two rules are missing, and the second one
only became visible once the first was in place.

R8 shrank values() out of 105 of the 106 enums in the released manager, because
nothing calls it any more: Kotlin compiles `entries` to a separate synthetic
field, so the generated method is left without a call site. Enum.valueOf looks
that method up by name, so an enum written into a Bundle -- Parcel has no enum
case and java.lang.Enum is Serializable, so it goes out as VAL_SERIALIZABLE --
cannot be read back. The navigation suite scaffold state is one such enum, and
it sits at the root of every screen.

CREATOR is found the same way, by a reflective field lookup R8 cannot see, and
it was gone from every Parcelable the manager did not already keep by name. A
`mutableStateOf` that survives process death is a ParcelableSnapshotMutableState,
so with the enums fixed the same restore threw BadParcelableException instead.

Both stanzas are AGP's, from proguard-android-optimize.txt. That file stopped
being passed to proguardFiles in #263, five years ago; the legacy manager had
copied CREATOR back by hand, the rewrite in #796 did not, and it declared no
enums at all, so neither rule was missed until the Compose manager needed them.

Verified on a Pixel 6 (Android 17) and a Galaxy A52s (Android 14): open the
manager, background it, kill the host process so the icicle comes back through
a Parcel, reopen. Before, that crashed every time; after, state restores across
repeated cycles, including the Logs tab and an open bottom sheet.
…owhere (#876)

The system server is offered as a synthetic row rather than an installed
package, so no filter can lead a reader back to it, yet it was subject to the
system-apps filter and a module whose declared scope is the framework showed an
empty list. Exempt the framework alone, and only when the module asks for it.

The empty-scope dialog's dismiss action closed it and returned the reader to the
page they were trying to leave, where back raised it again; the only exit was to
disable the module. Make that button leave, and offer the module's own
recommendation as the primary action where it declares one.

Legacy modules name the framework "android" and the android package "system";
modern modules and the daemon use the reverse. The store normalised the
installed scope but not the catalogue's, so one module could name one target two
ways in adjacent lines. Move the transform into one function both readings pass
through.

Module-update notifications are always enqueued for user 0 but carried the user
whose PACKAGE_REPLACED raised them, so for a module installed in a private space
as well as the main user the surviving notice pointed into a locked profile.
Prefer user 0 when the module is installed there.
#882)

Constructing a Notification on Android 16 reads an aconfig flag of the systemui
container, and the daemon cannot serve that read: it holds an ActivityThread but
no application record, so the settings provider is refused it and the
constructor throws SecurityException. Setting systemui_is_cached before the
first read leaves the flags at their compiled defaults and removes the read.

That bypass was gated on SDK_INT >= VANILLA_ICE_CREAM, wider than the release it
belongs to, because Xiaomi shipped the change on Android 15 without the SDK
level; #96 was such a device, four days before Android 16 DP1 was published.
#597 rewrote the test as SDK_INT == 36 and lost that case, which #880 now
reports on the same vendor and the same version. Android 17 sets those fields
unconditionally, so the test spans 35 to 36, the versions in which the field can
exist.
Scaffold places its bottom slot against the bottom of the window and reserves
nothing for it -- the documentation is explicit that topBar and bottomBar are
expected to handle insets themselves, which is why NavigationBar and
BottomAppBar carry windowInsets of their own. Three of this app's four docked
bars are plain Surfaces and reserved nothing, so their contents were drawn
under the navigation bar.

It bites on every detail screen whatever the navigation style, because the
navigation container is hidden away from a panel root and
NavigationSuiteScaffold consumes NoWindowInsets while hidden: nothing above the
screen has taken the system bars. With three-button navigation that is 48dp, and
on the scope editor it left a few pixels of Apply and Discard to aim at, which
is #884. Gesture navigation is 24dp and was covered too, the handle drawn across
the supporting line.

The padding goes inside each bar's Surface rather than on it, so the fill still
reaches the bottom edge and the bar reads as one surface. Insets already
consumed count for nothing, so the same call adds nothing in the arrangements
where a container below has taken them.

Home is the same fault from the other direction. It sets contentWindowInsets to
zero so the header can run under the status bar, which gave away the bottom as
well; with the panels floating there is no container to have taken it, and both
the last row of the feed and the scroll controls ended up behind the navigation
bar. Take the bottom edge alone, from the Scaffold's own default so a bottom
display cutout counts.
Four faults on the daemon side of a module's scope request.

The framework could never be granted. "system" names system_server and belongs
to no package, but the receiver resolved the requested package before acting on
the button, so every framework prompt was answered "Package not found", closed
and cancelled with no row written. Accept the framework name without asking the
package manager, and do the lookup only under Approve, the one answer that has
to name something real; deny and the one-hour timeout no longer report a lookup
failure for a package uninstalled while the prompt was up.

One request is now one prompt. The interface takes a list and a single
IXposedScopeCallback for it, but the daemon put one prompt per package on screen
and answered each in its own right, so a module asking for three packages made
the user answer three questions and fired that one listener three times. The
whole list goes up as one prompt whose Approve answers for all of it,
deduplicated and sorted so the same set asked twice replaces its own prompt, and
the per-module ceiling bounds calls rather than packages. The notification reuses
the string it always did with the packages joined into it, so no translation
changes.

An approval survives a dead module. A prompt sits for an hour and the app a
module runs inside can be killed in it, and the receiver returned on a dead
callback binder before claiming the answer, leaving nothing written and the
prompt on screen with buttons that did nothing. The decision is recorded whether
or not the module is still there to be told, and the only call that can fail
against it is caught where it is made.

The refusal path is logged. It had no log line at all, so a request that could
never be granted left no trace anywhere, which is why the framework case went
unnoticed for as long as it did. Name the packages that did not resolve, and the
ones that were approved.
Handing a module app its `IXposedService` means starting it: the daemon acquires the module's own `XposedService` provider, which brings the process up, and passes the binder in the reply. That reference was never released, and the platform reads an outstanding external reference as a live client — so `OomAdjuster` held every module app at `FOREGROUND_APP_ADJ` with adj type `ext-provider`, never cached, and a host dying while its provider was still launching was restarted for it. The platform bounds those restarts at three per provider record; acquiring again rebuilds the record with the count at zero, which is how #889 reached fourteen process starts in seventy-six seconds, six of them ours. The release has to be unconditional, because the two returns that matter come back null with the reference already registered.

Three consequences of the same path. The delivery no longer runs on the uid observer, where one module delayed every other module's binder by eight seconds. A failed send is no longer recorded as a delivered one, and repeated failures are throttled to one attempt a minute rather than one a second. And a delivery is recorded per process rather than per uid — a uid outlives any one of its processes, so the record could otherwise never clear.

Separately, `getContentProviderExternal` gained its `tag` argument in Q by replacing the three-argument form, so the unconditional four-argument call has meant no module received its app-side service on 8.1 or 9 since #597. And the `IUidObserver` stub declared four of the interface's eight methods; the rest stayed abstract at runtime, which `oneway` makes fatal rather than reportable.

Module apps are no longer immortal, so a scope answer arriving an hour later may find the app gone. The grant itself is written before the callback.
…895)

The status badge is the only route to the System status page, where the
settings for opening Vector live, and a tick does not read as a button
(#856). While the framework is active the tick now morphs into a gear for
ten seconds every thirty; across those ten seconds the gear tosses a coin
every two seconds and either turns once or stands still, because a wheel
that starts and stops reads as something being operated while one that
simply rotates becomes decoration. Every degree it moves was tossed for,
including the one hint in thirty-two that does not move at all. Only the
tick does this; the other states are reports, two of them urgent. The
hint retires after five badge taps in a day and returns the next.

A pinned shortcut does not follow the user to a launcher installed later,
yet getPinnedShortcuts keeps reporting it, because the pin flag belongs
to the shortcut rather than to the pair and only the active launcher may
read the per-launcher sets (#883). The launchers that have pinned it are
now recorded on this side, and a device running a launcher that is not
among them is offered the shortcut again. Where nothing is recorded the
current launcher is adopted, so no existing shortcut is declared missing,
and a home screen resolving to the chooser or to nothing counts as
unknown rather than as a mismatch.

Opening Home tossed a coin, and four times in five it showed whatever was
on disk — right for returning to Home, wrong for the first Home of a
process, when the archive has had longest to go stale. The first now
always revalidates; the toss governs only the visits after it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants