Reading the repo
Opening the box…
Reading the repo
Opening the box…
How to install on macOS
A community-built phone app for Clean Your TV: clean up an Android TV from your phone. No laptop, no coding agent, nothing uninstalled.
Saved setup from 1 h ago
Will it work on macOS?
173 TV lists from 47 brands, from the Clean Your TV community lists: TCL, Sony, Xiaomi, Hisense, Philips, Amazon Fire TV, Panasonic, XGIMI, BenQ, NVIDIA Shield, onn, Google and many more, on Android TV, Google TV and Fire OS. A model nobody has listed yet still gets a careful clean-up based on its brand.
<details> <summary>The details</summary>Your TV is not listed? TV lists live in Clean Your TV, so they help everyone, with or without the app. Add yours there by following its contributing guide, and the app picks it up in its next release. See also CONTRIBUTING.md.
CONTRIBUTING.mdThanks for helping. Bug reports, TV support requests, profile fixes and code are all welcome. This app changes other people's TVs, so the bar for safety is high; the rules below exist for that.
By contributing you agree that your contribution is licensed under the MIT licence and that you follow the code of conduct. Security problems go through SECURITY.md, never a public issue.
You need JDK 21 and the Android SDK (compile SDK 37, build tools 36; Gradle downloads what is missing once the licences are accepted). Linux or macOS. Gradle never downloads a JDK: the JDK 21 that runs Gradle is the toolchain of every module.
git clone --recurse-submodules https://github.com/udhaykumarbala/cleanyourtv-app.git
cd cleanyourtv-app
echo "sdk.dir=$ANDROID_HOME" > local.properties
./gradlew check assembleDebug
The submodule third_party/cleanyourtv is the upstream launcher at its pinned release; without
it the build stops and tells you to run git submodule update --init.
| Command | What it runs |
| :- | :- |
| ./gradlew check | the module graph rules, the build-logic tests, every unit test (the app's on both store flavours), Android Lint and the pin checks |
| ./gradlew verifyRoborazziDebug | the screenshot tests against the committed goldens |
| ./gradlew :app:assembleDebug | debug APKs of both flavours in app/build/outputs/apk/full/debug/ and .../play/debug/ |
| ./gradlew :app:assembleFullRelease :app:bundlePlayRelease :app:checkApkSizeRelease | the release APK (full) and AAB (play) through R8, and the 8 MB size gate on both flavours |
| python3 -m unittest discover -s scripts/tests -p 'test_*.py' | the tests of the release and redaction scripts |
CI (.github/workflows/pr.yml) runs all of these on every push and pull request; please run at
least check before opening one.
The app module has two product flavours (dimension store) built from the same code, with the
same application ID, versionCode and signing certificate:
full, for GitHub and F-Droid: "Check for updates" asks GitHub for the latest release, the
optional add-ons (Cards and modules) download from GitHub, and software updates on the TV are
a choice in Customise. The default flavour.play, for Google Play: no update check (Play updates the app), no add-on downloads (Play
allows no app downloads from outside Play), the TV's software updaters always stay on, and the
network security config names no host.The difference lives in app/src/full and app/src/play only: Store.kt (the ports AppGraph
wires: the hosts, the update checker, the add-on fetcher, the rules the flows follow) and
res/xml/network_security_config.xml. The screens read the rules from their state (StoreRules
in :core:flows) and leave out what a build does not offer. Tests that only hold for one flavour
go in app/src/testFull or app/src/testPlay. The version lives in app/build.gradle.kts.
Optional helpers: scripts/build-on-forge.sh and the other scripts/*-on-forge.sh scripts run the
same Gradle tasks, emulator sessions and release builds in throwaway Docker containers on a remote
Linux build host over ssh ("forge"); see scripts/lib/forge.sh for the settings. You never need
them for a contribution.
:testing:fixtures, and :testing:fake-adbd is a fake TV
that speaks the ADB protocol. Tests never touch a real TV; nothing in the build may need one.feature/* module and
:ui:design, in four renderings: dark, light, 200 % font and right to left, with the
accessibility checks on. Goldens live in src/test/screenshots/../gradlew recordRoborazziDebug, look at
every changed PNG, and commit them with the change. Record on Linux x86_64 (as CI does): other
platforms can render text a pixel differently.:e2e
(scripts/emulator-session.sh 28 ./gradlew :e2e:emulatorTest on a machine with KVM; nightly CI
runs them).The project is split into small modules with one direction of dependency, checked by
checkModuleGraph (rules in build-logic/convention/src/main/kotlin/tv/cleanyour/buildlogic/ModuleRules.kt):
core/* and adb/* are pure Kotlin JVM modules: no Android imports, no Android plugin.
:core:model holds the shared types and interfaces.android/*, ui/design, feature/* and app are Android modules. A feature module may depend
only on :core:flows, :core:model and :ui:design, never on another feature or on a lower
core module.gradle/libs.versions.toml, stable releases
only, and every artifact is checked by gradle/verification-metadata.xml
(scripts/update-verification-metadata.sh after a version change).:core:ops and :core:probe (@RawShell);
credential passes only by the gate implementations (@GateIssuer). Do not work around either.Changing a rule, a public interface in :core:model or a pin is fine, but say so in the pull
request and explain why; reviewers look at those first.
You don't need a computer or adb for either. The app makes the report, shows it to you first, and sends nothing itself: you copy it to GitHub, share it, or close the screen.
We read every report. For a new TV we make a list from the report, ask you to try it, and send it upstream once it works (below). How maintainers sort reports is in docs/TRIAGE.md.
The profiles come from the upstream launcher's community lists, so a new TV goes upstream first:
examples/<brand>-<model>/debloat.txt to
Gathin23/cleanyourtv following its
CONTRIBUTING.md: only
packages you have disabled and lived with, the model, Android version and firmware in the
header, and what broke. That list is useful to everyone, with or without this app.third_party/cleanyourtv submodule to the upstream commit with your list, and adds the TV's
entry to core/spec/src/main/resources/tv/cleanyour/core/spec/profile-meta.json: how to
recognise it (match: model or device names, brand, SDK range), its platform, stock launcher,
Live TV app and setup wizard, neverDisable for anything that must stay on, a category for
each group of the list, and tested (who, which firmware, how many days, whether a reboot was
tried). The build converts the list at build time and fails on anything it does not
understand. Bundled launcher changes need the maintainer (the APK is re-signed).No upstream list yet? Open a "Help add my TV" issue with the app's report (above); someone with the same TV may pick it up.
feature/*/src/main/res/values/).
Explain what happens to the TV, not how the code works.pm disable-user), never uninstalled.
Every write to the TV is recorded so it can be undone, and a restore point is saved before the
first one.:core:ops and :core:probe.Denylist in :core:ops (Play services, the Play
Store, Wi-Fi, Bluetooth for the remote, the TV's inputs, the shell and adbd) is never relaxed
for convenience.DownloadHosts (the full flavour's network security config lists the same; the
play flavour reaches no host at all). No analytics, no crash reporting, no new hosts or
permissions without an explicit review.:core:trust) before it is installed. The bundled launcher and TV curtain are pinned in
Pins.kt and checked at build time.scripts/import-mitv-capture.py and scripts/check-redaction.py before they become fixtures.One tag, three channels: every vX.Y.Z ships to GitHub and Google Play the same day and to
F-Droid on its next build cycle, from the same commit, with the same versionCode and the same
signing certificate (docs/release-signer.sha256), so an install from any channel updates from
any other. GitHub and F-Droid get the full APK, Google Play the play AAB.
versionCode and versionName in app/build.gradle.kts, add the CHANGELOG entry and the
short store notes (fastlane/metadata/android/en-US/changelogs/<versionCode>.txt, at most 500
characters; F-Droid and Google Play both read them), commit, and tag vX.Y.Z.scripts/release-on-forge.sh builds the release set: two clean builds of
:app:assembleFullRelease and :app:bundlePlayRelease in the pinned container
(docker/repro, scripts/repro-build.sh), each pair byte identical. On the maintainer's
machine only, the APK is signed with the release key (apksigner, v2 and v3) and the AAB with
the Play upload key (jarsigner). scripts/verify-release.sh then checks the APK (signature,
signer, apksigcopier compare against the unsigned build, scripts/check-apk.py --flavour full) and the AAB (signature, upload signer, the unsigned build plus that signature,
check-apk.py --flavour play: the same permissions, no GitHub host, no update check). Output:
dist/cleanyourtv-X.Y.Z.apk and .aab with their checksums, evidence in
dist/release-X.Y.Z/. --apk-only signs the APK alone (no upload key yet); --test-key
proves the pipeline with throwaway keys (evidence in dist/release-X.Y.Z-TESTKEY/, so a real
run's evidence stays).dist/release-X.Y.Z/RELEASE-NOTES.md or --notes),
then scripts/publish-release.sh vX.Y.Z publishes: the GitHub release with the APK and its
checksum, the AAB and the store notes to Google Play through the Play Developer API
(scripts/play-upload.py; the internal track by default, --track closed for the closed test,
--track production --rollout 0.2 for a staged rollout, --draft until the first release is
published from the Play Console), then the release workflow's verify job. --dry-run checks
every input and sends nothing.CI never holds a key or a credential: on a v* tag, .github/workflows/release.yml builds the
unsigned APK and AAB twice each, compares them and publishes them as artifacts, and its verify job
checks the signed APK attached to the GitHub Release against that build.
Keys and credentials live outside the repository, under $CYT_SECRETS (see scripts/lib/forge.sh),
and only the scripts read them:
| Path under $CYT_SECRETS | What | Used by |
| :- | :- | :- |
| cleanyourtv-release/release.p12, keystore-password.txt | the release key (PKCS#12) and its password | release-on-forge.sh (the APK) |
| cleanyourtv-play-upload/upload.p12, keystore-password.txt | the Play upload key (PKCS#12, RSA 2048 or more, alias upload) and its password | release-on-forge.sh (the AAB) |
| cleanyourtv-play/service-account.json | a Google Cloud service account key with release rights on the app in the Play Console | play-upload.py (read only, never printed) |
The Play upload key: Play App Signing holds the release key (uploaded once from the existing key, so Play installs share the certificate of the other channels), and a separate upload key signs what is uploaded. Make it once, keep it with the release key, and register its certificate in the Play Console (Setup, App signing):
keytool -genkeypair -keystore "$CYT_SECRETS/cleanyourtv-play-upload/upload.p12" -storetype PKCS12 \
-alias upload -keyalg RSA -keysize 4096 -validity 10000 -dname "CN=Clean Your TV upload key" \
-storepass:file "$CYT_SECRETS/cleanyourtv-play-upload/keystore-password.txt" \
-keypass:file "$CYT_SECRETS/cleanyourtv-play-upload/keystore-password.txt"
keytool -exportcert -rfc -keystore "$CYT_SECRETS/cleanyourtv-play-upload/upload.p12" -alias upload \
-storepass:file "$CYT_SECRETS/cleanyourtv-play-upload/keystore-password.txt" > upload-cert.pem
scripts/play-upload.py uses only the Python standard library (it signs the OAuth request itself),
so no extra package is needed.
Once, before the first Google Play release (decision D2, one certificate for every channel): give Play App Signing the existing release key instead of letting Google make a new one. In the Play Console (Test and release, App integrity, App signing) choose "Export and upload a key from Java keystore", download Google's PEPK tool and its encryption public key, and run the tool on the maintainer's machine only, where the release key lives; it asks for the keystore and key passwords itself, so they never appear on the command line:
java -jar pepk.jar --keystore="$CYT_SECRETS/cleanyourtv-release/release.p12" --alias=cleanyourtv \
--output=cleanyourtv-release-for-play.zip --include-cert --rsa-aes-encryption \
--encryption-key-path=encryption_public_key.pem
Upload the zip (it holds the key encrypted for Google only), then delete it. Check afterwards that
the Console shows the app signing certificate SHA-256 of docs/release-signer.sha256, and register
the upload key's certificate (upload-cert.pem above) on the same page.
Google Play's first steps are by hand: the first bundle of a new app is uploaded in the Play
Console (the API takes uploads only once the app has one), and until the app's first release is
published there, scripts/publish-release.sh needs --draft. The first production release cannot
be a staged rollout, so it takes --track production --rollout 1; staged rollouts (the default
0.2) work from the second production release on.
F-Droid builds the app from source with the recipe in docs/fdroid/tv.cleanyour.app.yml and
publishes our own signed APK when its build is that APK minus the signature (Binaries and
AllowedAPKSigningKeys in the recipe). After the one-time merge request to
fdroiddata (text in docs/fdroid/MERGE-REQUEST.md), a
release reaches F-Droid by itself:
vX.Y.Z tag and reads versionName and versionCode from the
app's build file.scripts/fdroid-build-tv-apks.sh, which builds the launcher (from third_party/cleanyourtv) and
:tv:curtain unsigned, puts the committed signature blocks from android/platform/signatures/
back on them with apksigcopier patch and checks the result against Pins.kt. Then it builds
the app's full release.cleanyourtv-X.Y.Z.apk from the GitHub Release and publishes it if
apksigcopier compare passes and the signer is the release certificate.So per release: keep the tag format vX.Y.Z and the asset name cleanyourtv-X.Y.Z.apk, and check
the F-Droid build before F-Droid does. The fdroid-check job (.github/workflows/fdroid.yml) runs
fdroidserver's fdroid build on the recipe in F-Droid's buildserver image on every tag and nightly,
and compares the result with the Release's APK when there is one. Once the Release is published,
run it on the tag: gh workflow run fdroid.yml --ref vX.Y.Z; it must say "match".
When a bundled TV APK is rebuilt and re-signed (scripts/build-launcher-on-forge.sh,
scripts/build-curtain-on-forge.sh), those scripts also refresh its signature block in
android/platform/signatures/; commit the APK, the signature block and the Pins.kt change
together. A stale signature block makes scripts/fdroid-build-tv-apks.sh fail.
Run the check locally:
# Only the bundled TV APKs: rebuild both from source and check them against their pins
# (JDK 21, the Android SDK and apksigcopier; the assets are rewritten with the same bytes).
scripts/fdroid-build-tv-apks.sh
# The whole F-Droid build, on a Linux x86_64 machine with Docker, from a normal clone (not a git
# worktree). Mount a signed APK at /in/signed.apk to compare it; outputs land in dist/fdroid/.
IMAGE=$(sed -n 's/^ FDROID_IMAGE: //p' .github/workflows/fdroid.yml)
docker run --rm -v "$PWD":/seed:ro -v "$PWD/dist/fdroid":/out \
-v "$PWD/cleanyourtv-X.Y.Z.apk":/in/signed.apk:ro "$IMAGE" bash /seed/scripts/fdroid-check.sh vX.Y.Z
Then run bash setup.sh. It stops at the first error and is safe to run again. Servers and tests aren't started; it lists them at the end. Read it first.
Compare yours with the README's “Supported TVs” list above before installing anything.
Needs Git: xcode-select --install. The repo uses git submodules, so the clone fetches them too.
git clone --recurse-submodules https://github.com/udhaykumarbala/cleanyourtv-app.git cd cleanyourtv-app
JDK
brew install openjdk@21
./gradlew build -x test
./gradlew run
No reports for macOS yet.
GitManual AI Agent reads the 9 key files and writes a step-by-step guide for macOS. Once written, it's saved for everyone.