OTA client for LumiROM series. Checks for updates, downloads the ROM from the configured server and triggers installation in recovery. It also updates itself from a JSON manifest and can notify you about new app and ROM releases.
Current integration status: the app installs as a system priv-app inside the ROM, with its privileged permissions and its entry in Samsung's allowlist. The whole pipeline (build, signing, release, ROM integration) lives in this repo.
- Update checks against a JSON manifest (configurable URL in Settings).
- Multi-release selector: list of published versions, pick which one to view/download.
- Maintainer card with avatar, contact handle and donation link.
- Per-release tech sheet: build date, Android version, One UI version, security patch level, build fingerprint, device model, kernel version, partition layout and changelog.
- Device One UI version (read from
ro.build.version.oneui). - Robust download with progress bar, resume support and SHA-256 verification.
- Installation: downloads to
/data/media/0/cloudy/, stages the OTA and reboots to recovery (requiresREBOOT+RECOVERY, available as a priv-app). - Flash ROM from storage: pick a local zip/raw image and stage it via recovery (TWRP).
- Self-update: Cloudy updates itself from
updater/app.jsonusing the same download + SHA-256 pipeline as ROMs, handed to the system installer — no root needed. - Background update notifications: a silent check for new Cloudy and LumiROM releases runs on every app open plus a periodic background check (configurable interval), alerting with a heads-up notification.
- Root service via libsu (
RootService) for privileged operations. - One UI 8 Settings screen: app info header (icon, name, version) with an in-place self-update check pill, credits and advanced settings, rendered with the SESL OneUI preference fork.
- Target ROM must be A-only (
a-onlypartition layout). - For install/reboot features: app installed as a priv-app with
privapp-permissionsand, on One UI 8.x, an entry in Samsung's allowlist (see ROM integration). - Optional: Magisk module for the SELinux context of OTA staging.
- Self-update and update notifications need no root or priv-app privileges — they only use
REQUEST_INSTALL_PACKAGES(system installer, user-approved) andPOST_NOTIFICATIONS(runtime, Android 13+).
Requirements: JDK 17+ and Android SDK (build-tools 36).
export ANDROID_HOME=$HOME/Android/Sdk
./gradlew assembleDebug # debug APK
./gradlew assembleRelease # signed release APKThe release build uses your own keystore. Create keystore.properties in the repo root (it is in .gitignore):
storeFile=/path/to/release.jks
storePassword=...
keyAlias=cloudy
keyPassword=...If keystore.properties is missing, the release build falls back to debug signing (useful for CI/development).
The app can be pointed at a locally served manifest from adb, without unlocking developer
options or typing a URL on the device. tools/adb-local-ota.sh
automates the whole loop: it reads the ZIP's build_info.txt, writes a manifest, serves the
manifest + ZIP on 127.0.0.1, maps that port back to the dev machine with adb reverse, and
broadcasts the URL to Cloudy.
tools/adb-local-ota.sh /path/to/LumiROM_8.7.1-...-INCREMENTAL_from_8.7.0.zip "changelog;entries"
# then open Cloudy → OTA / ROM tab and pull the update
tools/adb-local-ota.sh --stop # stop the local server + adb reverse
tools/adb-local-ota.sh --clear # restore the built-in sourcesUnder the hood it is a single am broadcast to a permission-guarded receiver; the same can
be done by hand:
adb reverse tcp:8000 tcp:8000
adb shell am broadcast -n dev.cloudy.ota/.ota.DevConfigReceiver --es url "http://127.0.0.1:8000/a32.json"
adb shell am broadcast -n dev.cloudy.ota/.ota.DevConfigReceiver --ez clear true--es url sets both tabs; --es ota_url / --es rom_url target one tab each. The receiver
is exported but gated by android.permission.WRITE_SECURE_SETTINGS, so only the adb shell /
system (which hold it) can repoint the updater. Cleartext traffic is only permitted to
127.0.0.1 / localhost (see network_security_config.xml), so the local server must be
reached through adb reverse.
If the device refuses a normal install because a system copy of Cloudy is signed with a different key (dirty flash across signing keys), build a test copy with a suffix that can coexist and pass it to the script:
./gradlew assembleRelease -PappIdSuffix=.dev # -> dev.cloudy.ota.dev
tools/adb-local-ota.sh -k dev.cloudy.ota.dev /path/to/ROM.zipThe app reads a manifest by default from:
https://raw.githubusercontent.com/Luminous418/cloudy/refs/heads/main/updater/<codename>.json
Notes:
oneui_versionis the raw value ofro.build.version.oneui(80500); the app formats it as8.5(the fifth digit is dropped when0).releasesmay be ordered newest to oldest; the app marks the installed version.
Cloudy's own update metadata lives in updater/app.json (the URL is fixed, not configurable). It mirrors the ROM manifest's download object so the same pipeline applies:
{
"app_name": "Cloudy",
"version": "2.22",
"version_code": 6,
"changelog": ["..."],
"download": { "url": "...", "filename": "Cloudy_2.22.apk", "size_bytes": 0, "sha256": "...", "install_type": "apk" }
}The check button is the pill in the Settings app info header. When a newer version exists, Cloudy downloads the APK, verifies its SHA-256, and hands it to the system installer (ACTION_VIEW via a FileProvider URI — requires REQUEST_INSTALL_PACKAGES).
A global Update notifications switch in Settings (default on) enables silent checks for new Cloudy and LumiROM releases. Anything newer than what's installed triggers a notification; an up-to-date app/ROM stays quiet.
- When: on every app open, plus a periodic background check via a Doze-friendly
AlarmManageralarm. The interval is configurable in Advanced settings → Check interval: every hour, 8 hours, 12 hours, day (default), week or month. - Reboots: the alarm is re-armed on boot (
RECEIVE_BOOT_COMPLETED). - Tap target: an app update opens the Settings tab; a ROM update opens the Update tab.
- Pop-ups: notifications use a dedicated
update_notificationschannel at alert importance, so they appear as heads-up pop-ups while the screen is on. On Android 13+ the runtimePOST_NOTIFICATIONSpermission is requested when the toggle is enabled. - Works fully without root; when running as a priv-app it needs no special allowlist entry for these features.
Workflow release.yml (manual, via workflow_dispatch):
- Input
name-> sanitized into a tag (e.g.Cloudy-2.22). - Builds
assembleReleasewith the keystore decoded from secrets. - Generates a changelog from commits since the last tag.
- Creates the GitHub release and uploads
app-release.apkto a rollinglatestrelease. - Regenerates
updater/app.json: readsversionCode/versionNamefrombuild.gradle.kts, computes the APK's SHA-256 and size, fills the changelog fromgit log, and commits + pushes it — so the self-update and the update notifications always point at the freshly released APK.
Required secrets:
| Secret | Description |
|---|---|
RELEASE_KEYSTORE |
release.jks as base64 |
KEYSTORE_PASSWORD |
keystore password |
KEY_PASSWORD |
key password (same as keystore on JDK 17+ builds) |
The key alias is
cloudyand is hardcoded in the workflow (validated withkeytool -list -alias cloudy).
For Cloudy to install as a system app on a Samsung ROM (One UI 8.x / Android 16), three pieces are required:
-
APK in priv-app
/system/priv-app/Cloudy/Cloudy.apk -
Privileged permissions whitelist ->
privapp-permissions/privapp-permissions-cloudy.xml/system/etc/permissions/privapp-permissions-cloudy.xmlGrants
android.permission.REBOOTandandroid.permission.RECOVERY. Without this, since Android 9 the app is silently dropped. -
Samsung system allowlist ->
allowed-preload/cloudy-allowed-preload.xml/system/etc/sysconfig/cloudy-allowed-preload.xmlImportant (classic One UI 8.x pitfall): Samsung will not install any package that is not in its
allowed-system-preload-apps.xmllist. PM drops it withPackage is not in allowed list : <pkg>and leaves no trace inpackages.xml. This extra file is what lets the app install. Without it, the app looks correctly placed and signed but never shows up as installed.
Report issues in this repo by using the Issues tab or clicking this link