{"content":"Command: cd /Users/nickchen/WorkBuddy/2026-10-23-15-17-57/cleanyourtv && sed -n '1,60p' tools/theme.sh; echo \"=== dir ===\"; grep -n \"static File dir\" -A 20 launcher/app/src/main/java/com/gathin/tvhome/Config.java\nStdout: #!/usr/bin/env bash\r\n# Push a theme (a folder with config.json and optionally wallpaper.jpg) to the launcher on the TV and\r\n# restart Home so it picks it up. The launcher reads the files from its external files directory and\r\n# overlays config.json on its built-in config.xml; see themes/README.md for the keys.\r\n#\r\n# Usage: TV_HOST=192.168.1.50 tools/theme.sh themes/aurora\r\n#        TV_HOST=192.168.1.50 tools/theme.sh --reset        # remove the overlay, back to config.xml\r\nset -euo pipefail\r\n# Resolve the theme folder against the caller's directory before moving into tools/.\r\n# Git Bash's pwd says /c/Users/..., which the Windows Python cannot open; pwd -W gives C:/Users/... (as in disable.sh).\r\n# An absolute argument needs the same: /c/Users/... passed through as typed reached the JSON check below, which\r\n# could not open it and called the file \"not valid JSON\". A folder that does not exist keeps the path as typed,\r\n# so the \"no config.json\" message names what the caller gave.\r\nTHEME_ARG=\"${1:-}\"\r\ncase \"$THEME_ARG\" in\r\n  --reset|\"\") ;;\r\n  /*) THEME_ARG=\"$(cd \"$THEME_ARG\" 2>/dev/null && { pwd -W 2>/dev/null || pwd; } || printf '%s' \"$THEME_ARG\")\" ;;\r\n  *) THEME_ARG=\"$(pwd -W 2>/dev/null || pwd)/$THEME_ARG\" ;;\r\nesac\r\ncd \"$(dirname \"$0\")\"\r\n: \"${TV_HOST:?set TV_HOST to the TV IP address, e.g. TV_HOST=192.168.1.50 $0 themes/aurora}\"\r\nPY=venv/bin/python\r\n[ -x \"$PY\" ] || PY=venv/Scripts/python.exe\r\n[ -x \"$PY\" ] || { echo \"run: python3 -m venv tools/venv && tools/venv/bin/pip install -r tools/requirements.txt\" >&2; exit 1; }\r\nexport MSYS_NO_PATHCONV=1\r\nPKG=com.gathin.tvhome\r\nDIR=/sdcard/Android/data/$PKG/files\r\n# A unique marker lets the launcher clear on-TV layout/motion choices after every theme push,\r\n# including a push of identical config.json bytes and --reset.\r\nREVISION=\"$(\"$PY\" -I -c 'import uuid; print(uuid.uuid4().hex)')\"\r\ntv() { \"$PY\" -I tv.py \"$@\"; }\r\n# Restart Home so it rereads the theme. Android drops a force-stopped app's accessibility services from the\r\n# enabled list, which would switch off InputKeyService/HomeKeyService, so put the list back if it had ours.\r\n# Android removes them asynchronously while it handles the restart, so wait a second before writing the\r\n# list back. Writing the same list back is not enough: the force-stop also turns accessibility_enabled off,\r\n# and the service only binds again once the list changes. So restart first, then clear the list, write it\r\n# back and switch accessibility on again as build-and-install does. Some TVs (TCL Smart TV Pro G03, Android 11)\r\n# also keep the service in their \"crashed\" list after the force-stop and leave it unbound until accessibility\r\n# itself goes off and on again, so switch it off with the list cleared and wait a second before writing back.\r\n# By then Android has already emptied the list and switched accessibility off (Xiaomi MiTV-MOOR2, Android 11), so\r\n# clearing it changed nothing and the services stayed crashed: write the list back first, so the clear is a change.\r\n# When the launcher is already Home, it comes back through the Home intent: `am start -n` from the shell brings it\r\n# back in an ordinary task, and the next Home press starts a second MainActivity next to it. Otherwise (a theme\r\n# pushed before the handover) it is started by name as before, so the new look still shows.\r\nRESTART=\"a=\\$(settings get secure enabled_accessibility_services); am force-stop $PKG; case \\\"\\$(cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.HOME | tail -n 1)\\\" in $PKG/*) am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null ;; *) am start -n $PKG/.MainActivity >/dev/null ;; esac; case \\\"\\$a\\\" in *$PKG*) sleep 1; settings put secure enabled_accessibility_services \\\"\\$a\\\"; settings put secure accessibility_enabled 1; sleep 1; settings put secure enabled_accessibility_services ''; settings put secure accessibility_enabled 0; sleep 1; settings put secure enabled_accessibility_services \\\"\\$a\\\"; settings put secure accessibility_enabled 1 ;; esac\"\r\n\r\n# Some firmware (TCL G07, Android 11) locks the shell out of Android/data. Files are staged in\r\n# /data/local/tmp and copied in, or written through the launcher's ThemeProvider when that is refused.\r\n# The provider has to be tried first: on Android 14 the shell can write into Android/data, so cp \"succeeds\",\r\n# but the file it creates belongs to the shell (uid 2000) and the launcher then gets EACCES opening it, which\r\n# silently ignores the theme whenever the file did not exist yet - a fresh install, or after a reinstall.\r\n# A provider write creates the file as the launcher, and it cannot truncate a file the shell owns either, so\r\n# remove our copy first. cp stays as the fallback for firmware without the provider.\r\nURI=content://$PKG.theme\r\nTMP=/data/local/tmp/cyt-theme\r\n# A file's staging path; wallpapers/a.jpg is staged as $TMP-wallpapers-a.jpg.\r\nstage() { echo \"$TMP-${1//\\//-}\"; }\r\n# Android 8 and 9 have no `content write`: they print the usage and \"[ERROR] Unsupported operation: write\" but\r\n# exit 0, so the exit status alone would skip cp and leave the file deleted. Treat that message as a failure.\r\nput() { echo \"rm -f $DIR/$1 2>/dev/null; { o=\\$(content write --uri $URI/$1 < $(stage \"$1\") 2>&1) && case \\\"\\$o\\\" in *'Unsupported operation'*) false ;; esac; } || cp $(stage \"$1\") $DIR/$1 2>/dev/null; rm -f $(stage \"$1\")\"; }\r\n=== dir ===\r\n310:    static File dir(Context ctx) {\r\n311-        return ctx.getExternalFilesDir(null);\r\n312-    }\r\n313-\r\n314-    static Config load(Context ctx) {\r\n315-        Resources res = ctx.getResources();\r\n316-        List<String> dock = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.dock_apps)));\r\n317-        boolean fillDock = res.getBoolean(R.bool.fill_dock);\r\n318-        List<String> hidden = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.hidden_apps)));\r\n319-        List<String> libraryApps = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.library_apps)));\r\n320-        String liveTv = res.getString(R.string.live_tv_package).trim();\r\n321-        String liveTvAction = res.getString(R.string.live_tv_action).trim();\r\n322-        String liveTvInputId = res.getString(R.string.live_tv_input_id).trim();\r\n323-        List<String> launchActions = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.launch_actions)));\r\n324-        List<String> appLabels = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.app_labels)));\r\n325-        List<String> appBanners = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.app_banners)));\r\n326-        String panelService = res.getString(R.string.source_panel_service).trim();\r\n327-        String panelAction = res.getString(R.string.source_panel_action).trim();\r\n328-        String panelExtra = res.getString(R.string.source_panel_extra).trim();\r\n329-        boolean coocaaSources = res.getBoolean(R.bool.coocaa_sources);\r\n330-        String inputAction = res.getString(R.string.input_action).trim();\r\n\nStderr: (empty)\nExit Code: 0\nSignal: (none)","renderer":{"type":"text","value":"#!/usr/bin/env bash\r\n# Push a theme (a folder with config.json and optionally wallpaper.jpg) to the launcher on the TV and\r\n# restart Home so it picks it up. The launcher reads the files from its external files directory and\r\n# overlays config.json on its built-in config.xml; see themes/README.md for the keys.\r\n#\r\n# Usage: TV_HOST=192.168.1.50 tools/theme.sh themes/aurora\r\n#        TV_HOST=192.168.1.50 tools/theme.sh --reset        # remove the overlay, back to config.xml\r\nset -euo pipefail\r\n# Resolve the theme folder against the caller's directory before moving into tools/.\r\n# Git Bash's pwd says /c/Users/..., which the Windows Python cannot open; pwd -W gives C:/Users/... (as in disable.sh).\r\n# An absolute argument needs the same: /c/Users/... passed through as typed reached the JSON check below, which\r\n# could not open it and called the file \"not valid JSON\". A folder that does not exist keeps the path as typed,\r\n# so the \"no config.json\" message names what the caller gave.\r\nTHEME_ARG=\"${1:-}\"\r\ncase \"$THEME_ARG\" in\r\n  --reset|\"\") ;;\r\n  /*) THEME_ARG=\"$(cd \"$THEME_ARG\" 2>/dev/null && { pwd -W 2>/dev/null || pwd; } || printf '%s' \"$THEME_ARG\")\" ;;\r\n  *) THEME_ARG=\"$(pwd -W 2>/dev/null || pwd)/$THEME_ARG\" ;;\r\nesac\r\ncd \"$(dirname \"$0\")\"\r\n: \"${TV_HOST:?set TV_HOST to the TV IP address, e.g. TV_HOST=192.168.1.50 $0 themes/aurora}\"\r\nPY=venv/bin/python\r\n[ -x \"$PY\" ] || PY=venv/Scripts/python.exe\r\n[ -x \"$PY\" ] || { echo \"run: python3 -m venv tools/venv && tools/venv/bin/pip install -r tools/requirements.txt\" >&2; exit 1; }\r\nexport MSYS_NO_PATHCONV=1\r\nPKG=com.gathin.tvhome\r\nDIR=/sdcard/Android/data/$PKG/files\r\n# A unique marker lets the launcher clear on-TV layout/motion choices after every theme push,\r\n# including a push of identical config.json bytes and --reset.\r\nREVISION=\"$(\"$PY\" -I -c 'import uuid; print(uuid.uuid4().hex)')\"\r\ntv() { \"$PY\" -I tv.py \"$@\"; }\r\n# Restart Home so it rereads the theme. Android drops a force-stopped app's accessibility services from the\r\n# enabled list, which would switch off InputKeyService/HomeKeyService, so put the list back if it had ours.\r\n# Android removes them asynchronously while it handles the restart, so wait a second before writing the\r\n# list back. Writing the same list back is not enough: the force-stop also turns accessibility_enabled off,\r\n# and the service only binds again once the list changes. So restart first, then clear the list, write it\r\n# back and switch accessibility on again as build-and-install does. Some TVs (TCL Smart TV Pro G03, Android 11)\r\n# also keep the service in their \"crashed\" list after the force-stop and leave it unbound until accessibility\r\n# itself goes off and on again, so switch it off with the list cleared and wait a second before writing back.\r\n# By then Android has already emptied the list and switched accessibility off (Xiaomi MiTV-MOOR2, Android 11), so\r\n# clearing it changed nothing and the services stayed crashed: write the list back first, so the clear is a change.\r\n# When the launcher is already Home, it comes back through the Home intent: `am start -n` from the shell brings it\r\n# back in an ordinary task, and the next Home press starts a second MainActivity next to it. Otherwise (a theme\r\n# pushed before the handover) it is started by name as before, so the new look still shows.\r\nRESTART=\"a=\\$(settings get secure enabled_accessibility_services); am force-stop $PKG; case \\\"\\$(cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.HOME | tail -n 1)\\\" in $PKG/*) am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null ;; *) am start -n $PKG/.MainActivity >/dev/null ;; esac; case \\\"\\$a\\\" in *$PKG*) sleep 1; settings put secure enabled_accessibility_services \\\"\\$a\\\"; settings put secure accessibility_enabled 1; sleep 1; settings put secure enabled_accessibility_services ''; settings put secure accessibility_enabled 0; sleep 1; settings put secure enabled_accessibility_services \\\"\\$a\\\"; settings put secure accessibility_enabled 1 ;; esac\"\r\n\r\n# Some firmware (TCL G07, Android 11) locks the shell out of Android/data. Files are staged in\r\n# /data/local/tmp and copied in, or written through the launcher's ThemeProvider when that is refused.\r\n# The provider has to be tried first: on Android 14 the shell can write into Android/data, so cp \"succeeds\",\r\n# but the file it creates belongs to the shell (uid 2000) and the launcher then gets EACCES opening it, which\r\n# silently ignores the theme whenever the file did not exist yet - a fresh install, or after a reinstall.\r\n# A provider write creates the file as the launcher, and it cannot truncate a file the shell owns either, so\r\n# remove our copy first. cp stays as the fallback for firmware without the provider.\r\nURI=content://$PKG.theme\r\nTMP=/data/local/tmp/cyt-theme\r\n# A file's staging path; wallpapers/a.jpg is staged as $TMP-wallpapers-a.jpg.\r\nstage() { echo \"$TMP-${1//\\//-}\"; }\r\n# Android 8 and 9 have no `content write`: they print the usage and \"[ERROR] Unsupported operation: write\" but\r\n# exit 0, so the exit status alone would skip cp and leave the file deleted. Treat that message as a failure.\r\nput() { echo \"rm -f $DIR/$1 2>/dev/null; { o=\\$(content write --uri $URI/$1 < $(stage \"$1\") 2>&1) && case \\\"\\$o\\\" in *'Unsupported operation'*) false ;; esac; } || cp $(stage \"$1\") $DIR/$1 2>/dev/null; rm -f $(stage \"$1\")\"; }\r\n=== dir ===\r\n310:    static File dir(Context ctx) {\r\n311-        return ctx.getExternalFilesDir(null);\r\n312-    }\r\n313-\r\n314-    static Config load(Context ctx) {\r\n315-        Resources res = ctx.getResources();\r\n316-        List<String> dock = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.dock_apps)));\r\n317-        boolean fillDock = res.getBoolean(R.bool.fill_dock);\r\n318-        List<String> hidden = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.hidden_apps)));\r\n319-        List<String> libraryApps = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.library_apps)));\r\n320-        String liveTv = res.getString(R.string.live_tv_package).trim();\r\n321-        String liveTvAction = res.getString(R.string.live_tv_action).trim();\r\n322-        String liveTvInputId = res.getString(R.string.live_tv_input_id).trim();\r\n323-        List<String> launchActions = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.launch_actions)));\r\n324-        List<String> appLabels = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.app_labels)));\r\n325-        List<String> appBanners = new ArrayList<>(Arrays.asList(res.getStringArray(R.array.app_banners)));\r\n326-        String panelService = res.getString(R.string.source_panel_service).trim();\r\n327-        String panelAction = res.getString(R.string.source_panel_action).trim();\r\n328-        String panelExtra = res.getString(R.string.source_panel_extra).trim();\r\n329-        boolean coocaaSources = res.getBoolean(R.bool.coocaa_sources);\r\n330-        String inputAction = res.getString(R.string.input_action).trim();"}}