Skip to content

Repository files navigation

Net Speed Indicator

Release Downloads License: MIT Android

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.

Status bar — default

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.

Status bar — custom colors

Why an LSPosed module (and not a "pure" Magisk module)

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)

Requirements

  • 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.

Install

  1. Download the latest app-release.apk from the Releases page and install it (adb install -r app-release.apk or via a file manager).
  2. Open the LSPosed / Vector manager → Modules, enable Net Speed Indicator.
  3. Open its Scope and tick System UI (auto-suggested via xposedscope).
  4. Restart SystemUI: adb shell su -c 'killall com.android.systemui' — or reboot.
  5. Open the Net Speed Indicator app to configure. Changes apply live (no restart needed).

Settings

  • 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

Build from source

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.apk

For 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 10000

then:

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.

How it works

  • Unified anchor — StatusIconContainer (id statusIcons). Every status-bar surface (collapsed bar, notification-shade / Quick-Settings header, lock screen) contains a statusIcons cluster with the battery sitting right after it in the same parent. The module hooks StatusIconContainer (onFinishInflate + onMeasure) and, for each instance, inserts its view into that instance's parent at indexOf(statusIcons) + 1directly 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 + deferred post), surviving re-attach / config changes.
  • Android 16 note: with the newStatusBarIcons flag the battery is no longer a BatteryMeterView (R.id.battery is gone) — it's a Jetpack Compose view after statusIcons. Anchoring to statusIcons is what makes "left of the battery" robust. The collapsed PhoneStatusBarView is itself hosted in a ComposeView, but statusIcons and 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 uses TrafficStats (de-dupes VPN/stacked ifaces); Wi-Fi sums wlan* and Cellular sums rmnet*/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.

Settings transport — a ContentProvider, not XSharedPreferences

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.

Troubleshooting

  • Nothing appears: confirm the module is enabled and System UI is in its scope, then restart SystemUI. Check adb logcat | grep NetSpeed for [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.

License

MIT © zazaulola

About

LSPosed module: live network speed in the status bar, left of the battery (stock Android 16 / Pixel 8 Pro)

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages