An LSPosed module that shows live network up/down speed inline in the status bar, immediately left of the battery — on stock Android, verified on Pixel 8 Pro / Android 16.
Two-line download/upload (or combined), with drawn ▼/▲ icons, adaptive or fixed units, per-direction colors, and optional placement in the notification shade and on the lock screen.
Stock Android has no built-in status-bar net-speed indicator. To place a widget among the system
icons, left of the battery, code has to run inside the SystemUI process and insert a view into
its layout. A plain Magisk overlay (RRO) can only swap resources — it cannot add a view or logic. So
the robust, OTA-survivable way is an Xposed/LSPosed hook of com.android.systemui. LSPosed is
itself a Zygisk/Magisk module, so you stay within the Magisk ecosystem:
Magisk ──> Zygisk ──> LSPosed (or Vector) ──> THIS module (hooks SystemUI)
- A rooted device with Magisk + Zygisk enabled.
- LSPosed active. On Android 16 use the maintained fork JingMatrix/LSPosed ≥ v1.11.0 or its successor Vector.
- Built and tested on Pixel 8 Pro / Android 16. Other Pixels should work; OEM skins (One UI, HyperOS) are out of scope.
- Download the latest
app-release.apkfrom the Releases page and install it (adb install -r app-release.apkor via a file manager). - Open the LSPosed / Vector manager → Modules, enable Net Speed Indicator.
- Open its Scope and tick System UI (auto-suggested via
xposedscope). - Restart SystemUI:
adb shell su -c 'killall com.android.systemui'— or reboot. - Open the Net Speed Indicator app to configure. Changes apply live (no restart needed).
- Display mode: Down + Up (two lines) · Combined · Download only
- Refresh interval: 0.5 / 1 / 2 / 3 s
- Traffic source: Total · Wi-Fi only · Cellular only
- Units: Auto (B/s, KB/s, MB/s) · Auto (bit/s, Kbit/s, Mbit/s) · or a fixed magnitude (KB/s, MB/s, Kbit/s, Mbit/s). With a fixed unit the label can be hidden and the bare number is still unambiguous; with an Auto unit the label is always shown.
- Show upload on top (swap UL/DL order) · Dim arrow when idle (semi-transparent ▲/▼ at 0)
- Show unit label · Hide when idle + threshold · Text size
- Color: auto (match status-bar tint) or custom per-direction hex (separate download / upload)
- Where to show: collapsed status bar (always) · notification shade · lock screen
Requirements: JDK 17–21 (AGP 8.x doesn't support JDK 24+) and the Android SDK (platform 36,
build-tools 36). Point the build at a suitable JDK via JAVA_HOME.
# Debug build (unsigned-by-debug-key)
JAVA_HOME=/path/to/jdk-21 ./gradlew :app:assembleDebug
# -> app/build/outputs/apk/debug/app-debug.apkFor a signed release, create keystore.properties in the project root (gitignored):
storeFile=release.jks
storePassword=********
keyAlias=netspeed
keyPassword=********…with a keystore generated by:
keytool -genkeypair -v -keystore release.jks -alias netspeed \
-keyalg RSA -keysize 2048 -validity 10000then:
JAVA_HOME=/path/to/jdk-21 ./gradlew :app:assembleRelease
# -> app/build/outputs/apk/release/app-release.apk./build-and-install.sh builds and installs to a connected device. Keep a backup of your keystore
— without it you can't ship in-place updates to a signed install.
- Unified anchor —
StatusIconContainer(idstatusIcons). Every status-bar surface (collapsed bar, notification-shade / Quick-Settings header, lock screen) contains astatusIconscluster with the battery sitting right after it in the same parent. The module hooksStatusIconContainer(onFinishInflate+onMeasure) and, for each instance, inserts its view into that instance's parent atindexOf(statusIcons) + 1→ directly left of the battery. Resource ids are resolved by name, never numeric. Every hook/insert is wrapped in try/catch +XposedBridge.log, so it can never crash SystemUI. - Multi-surface. The surface is detected from ancestor ids (
status_bar→ collapsed,split_shade_status_bar/shade_header→ shade,keyguard_header→ lock screen). Each surface gets its own view instance; the shade and lock-screen ones self-hide unless enabled. Insertion is idempotent (per-parent tag guard + deferredpost), surviving re-attach / config changes. - Android 16 note: with the
newStatusBarIconsflag the battery is no longer aBatteryMeterView(R.id.batteryis gone) — it's a Jetpack Compose view afterstatusIcons. Anchoring tostatusIconsis what makes "left of the battery" robust. The collapsedPhoneStatusBarViewis itself hosted in aComposeView, butstatusIconsand its siblings are still classic Views. - Direction markers are drawn programmatically (a triangle
Drawable), tinted per direction and dimmed when idle — no dependency on the system font's arrow glyphs. - Speed is sampled once per interval via
elapsedRealtime; counter resets / source changes emit 0 instead of a spike. Total usesTrafficStats(de-dupes VPN/stacked ifaces); Wi-Fi sumswlan*and Cellular sumsrmnet*/ccmni*from/proc/net/dev. The loop pauses when the status-bar window is hidden (AOD / fullscreen). - Color follows the status-bar tint by mirroring the clock in the same root (or custom per-direction hex), so dark/light flips match and the wrong (keyguard) clock is never used.
On Android 16 the SystemUI process cannot read the module's prefs file at all (verified on device:
canRead=false). DAC perms and SELinux app-isolation categories block cross-app file reads, and
MODE_WORLD_READABLE no longer makes the file readable on this framework — so XSharedPreferences is
a dead end. Instead, settings travel over binder: the app hosts an exported ConfigProvider; the
hook pulls all values via ContentResolver.call("get") on a background thread, refreshing on attach
and whenever the user changes a setting (the settings screen calls notifyChange, firing the hook's
ContentObserver). Sane defaults ship in the hook, so it works before the app is ever opened.
- Nothing appears: confirm the module is enabled and System UI is in its scope, then restart
SystemUI. Check
adb logcat | grep NetSpeedfor[NetSpeed]lines. - Not on shade / lock screen: enable those toggles under Where to show.
- Cramped layout: lower the text size, or use Combined / Download only.
MIT © zazaulola

