Syncthing Live Update

Android 16 introduced Live Updates: promoted ongoing notifications that surface in the status bar chip, on the lock screen and on Always-on Display. Syncthing already knows how far along a sync is — it simply has no way to say so in that format. This module supplies the missing part, without adding a notification of its own.

Scope

The module hooks three processes:

system
android
com.github.catfriend1.syncthingfork
com.android.systemui
Scope Required Without it
system / android yes The notification is rewritten but never promoted, and no chip appears
com.github.catfriend1.syncthingfork yes Nothing happens at all
com.android.systemui optional The bar is flat and jumps between values, and the chip keeps the system grey

Grant all of them. Do not add any other application.

A flat progress bar and a grey chip mean the SystemUI scope is missing — not that the module is working as intended.

The system server scope generally has to be confirmed by hand in the manager.

What it does

The Live Update

  • Promotes Syncthing's sync progress to a Live Update, visible in the status bar chip, on the lock screen and on Always-on Display.
  • Rewrites the notification Syncthing already posts instead of adding a second one. Its title, icon, Exit action and tap target are kept, and it stays the foreground-service notification the app depends on.
  • Draws a Notification.ProgressStyle bar for overall sync completion, and a percentage in the status bar chip through setShortCriticalText.
  • Follows Material 3 Expressive: the bar runs from where the data comes from to where it goes, each end marked by a tonal circle showing this phone or the remote computer, led by a scalloped cookie-shaped tracker carrying the Syncthing glyph. Colours are Material 3 roles derived from Syncthing's blue, resolved for the light or dark theme.
  • Names the folders currently transferring on a second line, using the labels configured in Syncthing rather than folder ids. Past two, the remainder is shown as a count.
  • Badges the status icon with the transfer direction: down while this device pulls from a remote, up while a remote pulls from this device, and both when each is happening at once.
  • Reads every figure from the host's own callbacks. No REST polling, no API key, no timers, no background service.
  • Passes the notification through untouched when nothing is transferring, so idle behaviour is exactly what Syncthing ships with.

SystemUI polish

ProgressStyle cannot express everything, so the rest is added where SystemUI draws it. Each hook first checks that it is looking at Syncthing-Fork's notification; every other app's bar and chip are drawn by the platform exactly as before.

  • The filled part of the bar is drawn as a Material 3 Expressive wavy progress indicator — 4dp stroke, 3dp amplitude, 40dp wavelength — flowing one wavelength per second and flattening near either end, followed by a flat track and a stop dot. It holds still when animations are turned off, asks for a new frame at most every 16ms so a 120Hz panel costs no more than a 60Hz one, and gives up after three failed draws rather than retrying on every frame.
  • Progress no longer jumps. Each update is applied at the value on screen, and the bar then travels to the new value on a critically damped spring, so it never overshoots. Progress is reported in tenths of a percent so the movement is smooth rather than stepped.
  • The status bar chip is filled with Syncthing's primary colour and its content drawn in on-primary, matching the progress tracker. Android 17 otherwise always paints a notification chip in the system surface colour.

Requirements

  • Android 16 (API 36) or newer. Live Updates do not exist before that. Developed and validated on Android 17.
  • Root. Vector with Magisk or KernelSU and Zygisk enabled, implementing the modern Xposed API version 101 or newer. LSPatch cannot run this module: a rootless patch injects into the target app only, and the promotion flag has to be restored inside system_server.
  • Syncthing-Fork (com.github.catfriend1.syncthingfork).

Installation

  1. Install the APK from the release attached to this repository.
  2. Enable Syncthing Live Update in the framework manager.
  3. Grant all scope entries: the system server, Syncthing-Fork and SystemUI.
  4. Reboot. The system_server hook is installed while the system server starts, so it cannot take effect until the next boot.
  5. Framework log and logcat entries are tagged:
SyncthingLiveUpdate

With the Vector CLI, steps 2 and 3 are:

su -c '/data/adb/lspd/cli modules enable io.github.mrxsin.syncthingliveupdate'
su -c '/data/adb/lspd/cli scope set io.github.mrxsin.syncthingliveupdate system/0 com.github.catfriend1.syncthingfork/0 com.android.systemui/0'

A healthy start logs the framework it attached to, the Android release it recognised, the host method it is observing and the channel it will promote on.

Later updates need no reboot: the module declares autoHotReload, so a new build is loaded into running processes. system_server is the slower half — until it holds the new build, the module says so in the log rather than leaving the Live Update silently unpromoted, and su -c 'stop; start' forces it in about a boot animation's time.

Compatibility

Component Status
Syncthing-Fork 2.1.5.0 (latest stable) Every hooked class, method and field verified present; exercised on a device
Syncthing-Fork 2.1.6.0-rc.1 (latest pre-release) Every hooked class, method and field verified present
Android 17 (SDK 37) Validated on a Pixel 8 Pro, builds CP2A.260805.005 and CP3A.260905.009
Android 16 (SDK 36) Supported, but not yet tested on a device

The rule for what may be promoted changed between releases, and the two forms are mutually exclusive:

Release hasPromotableCharacteristics() requires
Android 16 setColorized(true)
Android 17 setRequestPromotedOngoing(true), and not colorized

The module holds each release's assumptions in its own compatibility contract and picks the right one at runtime. An Android release it has not been read against keeps the base Live Update and deliberately leaves the SystemUI polish off, rather than guessing at private names.

How it works

In the Syncthing process

NotificationHandler.updatePersistentNotification(SyncthingService, Boolean, int, int) is where the host hands over the figures it has just recalculated in RestApi.onTotalSyncCompletionChange(). Observing that one method yields the overall completion percentage without a single REST call. The values are read before the call proceeds, because the host posts its notification inside it.

Direction comes from the two figures that percentage is derived from: LocalCompletion.getTotalFolderCompletion() falls below 100 while this device pulls, RemoteCompletion.getTotalDeviceCompletion() falls below 100 while remotes pull from it, and both below 100 means both directions at once.

Folder names come from LocalCompletion.setFolderStatus(String, Boolean, FolderStatus), paired with the labels the host caches in updateFromConfig(List<Folder>). A folder counts as transferring in the syncing and sync-preparing states — not while it is merely scanning, scan-waiting or starting, which is why a plain rescan never puts a folder name on the notification.

The rewrite itself happens on Service.startForeground(...), filtered to the host's persistent channel. Notification.Builder.recoverBuilder(...) reopens the notification the host just built, so nothing is reconstructed from scratch and the module only adds what promotion requires.

These methods are located by their shape — the class, the signature and the string constants the body uses — before falling back to a plain name lookup, so an upstream rename does not silently disable a feature. That is what the module's one native library, DexKit, is for. It is opened while the hooks are installed and closed immediately afterwards; nothing holds it past startup.

In system_server

Android sets FLAG_PROMOTED_ONGOING only for a package holding android.permission.POST_PROMOTED_NOTIFICATIONS. Syncthing-Fork does not declare it, and an install-time permission cannot be granted after the fact — pm grant will not do it. The module therefore restores that single flag inside NotificationManagerService.fixNotificationWithChannel(...), and only when the package is Syncthing-Fork and the channel is the module's own. Every other notification on the device, including Syncthing's own, keeps the platform's verdict. Nothing else about the platform's promotion policy is touched.

In SystemUI

The platform's own layout of the progress bar is kept and only its painting is replaced, for Syncthing-Fork's notification alone. The chip's colours are swapped for Syncthing's pair as the chip model is built. Every other app's notification is drawn exactly as before.

The channel

The host posts its persistent notification on an IMPORTANCE_MIN channel, and the platform refuses to promote anything posted there. During a transfer the notification is moved to 05_syncthing_live_update at IMPORTANCE_LOW; when the sync completes it returns to 01_syncthing_persistent and behaves exactly as before.

Failing open

Every hook installs through a single boundary that catches failure, so a hook that cannot install costs its own feature and nothing else — Syncthing is left exactly as it was. A member the module cannot reach is reported once, with the Android release, the build fingerprint, what was being looked for and whether it was missing, refused or threw.

Troubleshooting

Problem Suggested action
Progress bar appears but no chip The system server scope is missing, or the device has not been rebooted since it was granted. That hook is only installed at boot.
Bar is flat, jumps between values, or the chip is grey The SystemUI scope is missing. If it is granted, look for SystemUI polish left off, Wavy progress unavailable, Progress bar animation unavailable or Chip colours unavailable in the log, which mean a system update moved the SystemUI internals.
Bar starts wavy and goes flat mid-sync Wavy progress disabled after 3 failed draws. The custom drawing hit a mismatch with this SystemUI build and switched itself off for the rest of the process; the platform's own bar is used from then on.
Nothing changes at all during a sync Check logcat for SyncthingLiveUpdate. Host notification method not found means a Syncthing update renamed the method the module observes, and that neither the shape search nor a name lookup could find it.
Styled bar but never promoted, right after installing or updating The log says the module is newer than the running system_server. Wait for the framework to hot reload it, or force it with su -c 'stop; start'.
Chip shows the icon but no percentage Expected while Syncthing itself is the visible app, or while the notification is pinned as a heads-up. SystemUI collapses the chip to an icon in both cases; the percentage returns once another app is in front.
Chip and the status bar clock overlap or flicker Another Xposed module is redrawing the status bar and colliding with the chip animation. The notification itself is unaffected.
Progress shows but no folder name Either only a remote is pulling from this device, so no local folder enters a transferring state, or the log reports Folder names unavailable, meaning a Syncthing update moved the folder model.
Direction badge never appears Look for Transfer direction unavailable in the log. Progress still works without it.

Credits

Project Contribution
Syncthing-Fork The host application whose sync figures and notification this module builds on. MPL-2.0.
Vector by JingMatrix The ART hooking framework used in the app process, the system server and SystemUI. GPL-3.0.
libxposed API The modern Xposed module API this module compiles against. Apache-2.0.
DexKit by LuckyPray Finds the hooked host methods by shape, so an upstream rename does not silently disable a feature. Apache-2.0.
Material Symbols The smartphone and computer glyphs at either end of the progress bar. Apache-2.0.

Disclaimer

Not affiliated with, endorsed by, or sponsored by the Syncthing project or the maintainers of Syncthing-Fork. Provided for educational and personal use. Host app updates or Android platform changes may break the hooks without notice.

Releases

1.1.1

Stable

9/22/2026, 2:31:05 PM

Syncthing Live Update v1.1.1

Compatibility and correctness release. Tested on a Pixel 8 Pro on Android 17, build
CP3A.260905.009, with Vector 2.2 and Syncthing-Fork 2.1.5.0.

What's in it

  • Works with the latest Syncthing-Fork. Every class, method and field the module hooks is
    verified present in 2.1.5.0 and 2.1.6.0-rc.1, and 2.1.5.0 is confirmed on a device.
  • Survives upstream renames. Hooked host methods are found by their shape — class,
    signature and the string constants they use — before falling back to a name lookup.
  • No reboot to update. The module declares autoHotReload, so a new build is loaded into
    running processes.
  • Android 16 is promoted again, through the setColorized request that release expects.
  • An unknown Android release degrades predictably: the base Live Update stays, the
    SystemUI polish switches itself off.
  • The wavy bar costs less, asking for a frame at most every 16ms, and stops trying after
    three failed draws.

Install

  1. Install SyncthingLiveUpdate-v1.1.1.apk
  2. Enable Syncthing Live Update in Vector
  3. Grant all three scopes: the system server, Syncthing-Fork and SystemUI
  4. Reboot once

Notes

  • Upgrading from an earlier version needs one framework restart — su -c 'stop; start' —
    because the version being replaced could not hot reload. Updates after this one do not.
  • A flat progress bar and a grey chip mean the SystemUI scope is missing.
  • Android 16 is supported but has not been tested on a device.
  • Needs Android 16 or newer, root with a Zygisk-based Xposed framework exposing libxposed
    API 102, and Syncthing-Fork (com.github.catfriend1.syncthingfork).

Assets

1