기기 관리 도구
12c21f7 기준이며, 파일 경로는 모두 저장소 루트 기준입니다.
dumbtv는 ADB로 Fire TV의 불필요한 서비스를 비활성화하고 DNS 필터와 자체 홈 화면을 연결하는, 루트 권한 없는 기기 정리 도구입니다. Fire OS를 지우거나 다른 운영체제를 설치하지 않습니다. 앱 실행과 HDMI 입력은 기존 Android 기반 시스템의 기능을 이용합니다.
GitHub의 주 언어는 Java이지만 저장소 전체를 이해하려면 두 부분을 함께 봐야 합니다.
컴퓨터에서 실행되는 scripts/는 TV의 상태를 바꾸고, TV 안에서 실행되는 launcher/는 사용자가 보는 홈 화면을 그립니다.
Java 서버나 웹 프론트엔드가 있는 프로젝트는 아닙니다.
Fire OS가 집이라면 Bash 스크립트는 어떤 설비를 켜 둘지 정하는 관리인입니다. DNS 필터는 특정 주소로 나가는 연락을 막는 교환원이고, Java 런처는 자주 쓰는 방으로 연결되는 간결한 현관입니다.
집의 기초인 부트로더와 /system은 그대로 둡니다.
그래서 이 프로젝트의 재미는 화면을 예쁘게 만드는 코드뿐 아니라, 남의 기기를 다룰 때 경계를 지키는 코드에도 있습니다.
| 수단 | 실제 구현 | 배울 질문 |
|---|---|---|
| 패키지·설정 조정 | scripts/debloat.sh, lockdown.sh | 삭제하지 않고 기능을 끌 수 있을까? |
| 통신 제한 | scripts/dnsfilter.sh가 외부 필터 앱을 설치·설정 | 꺼지지 않는 서비스는 어떻게 다룰까? |
| 홈 화면 교체 | launcher/src/tv/dumbtv/home/ | 시스템이 HOME을 쥐고 있으면 어떻게 연동할까? |
이 저장소의 핵심은 광고 제거 명령 하나가 아니라, 변경·검증·복구를 한 흐름으로 묶는 방법입니다.
소스와 설정 파일을 읽으며 명령의 흐름을 종이 위에 그려 보세요. TV 없이도 사용자 선택에 따른 분기와 변경·검증·복구의 연결을 공부할 수 있습니다.
README에서 저자는 느려진 Fire TV Edition TV를 ADB와 코딩 에이전트로 정리한 경험이 출발점이라고 설명합니다. 남은 메모리가 129 MB였다는 일화와 언론 보도 링크도 README에 있습니다. 이는 저자의 경험과 README에 기록된 배경이며, 다른 TV에서도 같은 성능 개선이 나타난다는 뜻은 아닙니다.
2026-10-23 TrendShift Daily #27에 선정되었으며, 2026-10-23 GitHub 기준 스타 1,312개, 포크 113개입니다. 생성일은 2026-10-18입니다. 이 수치만으로 스타의 증가 속도나 특정 보도가 인기를 끌어올렸는지는 알 수 없습니다.
코드 관점에서 눈에 띄는 조합은 세 가지입니다. 루트 없이 동작하는 도구, 원문을 읽을 수 있는 작은 Java 런처, 그리고 기기별 실패 사례를 YAML로 남기는 카탈로그입니다. 초보자는 기능과 운영 지식을 같은 저장소에 담는 방법을 볼 수 있습니다.
| 접근·예시 | 주로 해결하는 문제 | dumbtv에서 더 볼 부분 |
|---|---|---|
| 홈 화면 교체 예: Projectivy |
TV의 첫 화면과 앱 탐색을 바꿉니다. | 자체 런처와 함께 패키지 정리·DNS 설정·결과 검증을 연결합니다. 다른 런처의 전체 기능 우열을 뜻하지 않습니다. |
| 네트워크 DNS 필터 | 도메인 단위로 광고·추적 주소의 이름 해석을 막습니다. | 필터를 TV 안에 둡니다. 라우터 설정을 바꾸지 않고 해당 TV에 적용하는 구성입니다. |
| 수동 ADB 정리 | 사용자가 패키지와 설정을 하나씩 조정합니다. | 기종별 목록, 유지 목록, 금지 목록, 복구 절차와 테스트를 함께 읽을 수 있습니다. |
| Android TV용 도구 예: cleanyourtv |
Android TV·Google TV 계열을 정리하는 별도 프로젝트입니다. | dumbtv README가 대안으로 안내합니다. dumbtv의 Fire OS 자동화와 지원 범위를 구별해야 합니다. |
위 표는 각 접근이 해결하는 문제의 범위를 비교합니다.
Projectivy 연동의 흔적은 launcher/install.sh의 OLD_GUARD에서도 확인됩니다.
설치 과정은 기존 Projectivy 접근성 서비스를 교체 대상으로 다루며, 일반적인 모든 런처와의 공존을 보장하지는 않습니다.
launcher/README.md는 APK 54 KB, 홈 화면 표시 중 메모리 약 60~70 MB, 영상 재생 중 약 37 MB를 제시합니다.
특정 1.4 GB RAM 기기에서의 저자 측정값입니다. APK 크기가 작다고 실행 중 메모리도 같은 크기가 되는 것은 아닙니다.
dumbtv의 교육적 장점은 작은 코드에서도 권한, 호환성, 복구, 테스트가 서로 연결되는 모습을 볼 수 있다는 점입니다.
런처에 INTERNET 권한이 없다는 사실은 Manifest에서 확인됩니다.
다른 시스템 앱과 스트리밍 앱의 통신까지 사라진다는 뜻은 아닙니다.
DNS 차단은 도메인 단위이며, 실제 앱 정상 동작과 차단 효과는 기기에서 확인해야 합니다.
package.json, Cargo.toml, pyproject.toml, go.mod,
pom.xml, Gradle 설정, Dockerfile은 커밋 12c21f7의 파일 목록에 없습니다.
서버 프레임워크·DB·컨테이너 스택을 억지로 대응시키면 구조를 잘못 이해하게 됩니다.
| 구성 요소 | 소스에서 확인한 값 | 역할·주의점 |
|---|---|---|
| Bash | scripts/lib.sh: 3.2 호환 명시 | macOS 기본 Bash까지 고려합니다. 실제 실행 버전을 고정한 잠금 파일은 없습니다. |
| ADB | ADB=${ADB:-adb} | 실기기 제어 경계이자 테스트 대체 지점입니다. Platform-Tools 버전은 고정하지 않습니다. |
| 텍스트 도구 | awk, sed, grep, curl | 패키지 목록과 명령 출력을 해석합니다. 각 도구의 버전 잠금은 없습니다. |
| curl 옵션 | fetch(): --retry-all-errors | 소스 주석은 curl 7.71+ 옵션임을 밝힙니다. Bash 호환성만으로 모든 환경이 충족되지는 않습니다. |
Home은 Activity, Guard는 AccessibilityService,
Keeper는 Service를 확장합니다. React·웹뷰로 화면을 그리는 구성이 아닙니다.
화면은 Home.build() 등의 Java 코드에서 Android View를 조합합니다.
| 항목 | 실제 설정 | 초보자가 구별할 것 |
|---|---|---|
| 빌드 JDK | CI: Temurin 17 | 컴파일 도구를 실행하는 Java 버전입니다. |
| Java 컴파일 | javac --release 8 | Java 8 언어·라이브러리 기준을 사용합니다. Android API 호환성 검사는 별도로 필요합니다. |
| Android 플랫폼 | MIN_SDK=25, SDK의 platforms/android-25/android.jar | 기본 빌드는 API 25를 기준으로 컴파일하고 설치 최소 API도 25로 지정합니다. |
| 대상 API | --target-sdk-version 28 | 최소 지원 API와 구별해야 합니다. 최신 API를 겨냥한 앱이라는 뜻은 아닙니다. |
| 외부 Java 라이브러리 | 별도 의존성 선언 없음 | AndroidX·Compose·Play Services 없이 Android 표준 API를 사용합니다. |
| 런처 버전 | VERSION=1.2, VERSION_CODE=3 | launcher/version.env가 빌드·설치의 공통 기준입니다. |
--release 8 하나로 모든 Android 기종의 API 호환성이 보장되는 것은 아닙니다.
이 레포는 오래된 android.jar와 실제 기기 보고도 함께 사용합니다.launcher/build.sh의 호출 순서는 아래와 같습니다. CI는 build-tools;34.0.0을 설치하지만,
로컬 스크립트는 설치된 build-tools 디렉토리를 정렬해 마지막 버전을 고릅니다.
따라서 로컬 빌드 도구가 CI와 항상 같다고 단정할 수 없습니다.
launcher/res/ + launcher/AndroidManifest.xml
-> aapt2 compile / link # 리소스 묶기, R.java 생성
Java 소스 + Android SDK의 platforms/android-25/android.jar
-> javac --release 8 # .class 생성, 경고를 오류로 처리
-> d8 --min-api 25 # Android 실행 형식 classes.dex
-> zip + zipalign # APK 조립과 정렬
-> apksigner sign / verify # 서명하고 서명 유효성 확인
-> launcher/build/dumbtv-home.apk
서명 키는 이후 업데이트에도 같아야 합니다. install.sh는 서명이 다르면
INSTALL_FAILED_UPDATE_INCOMPATIBLE을 별도로 안내합니다.
파일 해시와 APK 서명은 각각 파일 일치 여부와 설치·업데이트의 서명 관계를 다루는 서로 다른 장치입니다.
| 의존 대상 | 실제 고정값·정책 | 확인 파일 |
|---|---|---|
| personalDNSfilter | F-Droid APK versionCode 1506103과 SHA-256 고정 | scripts/dnsfilter.sh |
| 런처 릴리스 APK | launcher-v1.2 경로와 SHA-256 고정 | launcher/version.env, install.sh |
| DNS 기본 설정 | 상류 master의 설정 파일을 다운로드 | CONF_URL: 커밋 미고정 |
| HaGeZi 목록 | jsDelivr의 @latest | HAGEZI_URL: 내용이 변할 수 있음 |
| Python | 카탈로그 CI는 3.12, 테스트는 표준 unittest | .github/workflows/validate.yml |
| PyYAML | pip install pyyaml, 버전 미고정 | 카탈로그 검증용이며 TV 런타임 의존성이 아님 |
| GitHub Actions | checkout@v4, setup-java@v4, setup-python@v5 | 동일 워크플로에서 확인 |
실행 파일의 해시를 고정해도, 함께 가져오는 설정과 차단 목록까지 고정되는 것은 아닙니다.
dnsfilter.sh는 설정 치환 후 필수 문자열을 검사해 상류 형식 변화 일부를 감지합니다.
이는 완전한 의존성 잠금과는 다릅니다.
[사용자의 컴퓨터]
scripts/configure.sh ----> dumbtv.conf
data/ ------------------> scripts/lib.sh
|
scripts/all.sh | ADB 명령·파일 전송
preflight v
debloat [Fire TV / Fire OS]
lockdown PackageManager / Settings / AppOps
dnsfilter NetworkPolicy / Accessibility
launcher |
youtube (선택) +-- tv.dumbtv.home
verify | Home / Catalog / Config
| | Guard (Fire OS 7·8)
v | Keeper (Fire OS 6)
report/ | Input -> TvView -> HDMI
설정 스냅샷·변경 기록 |
+-- personalDNSfilter (외부 앱)
로컬 VPN -> DNS 필터링
[외부 배포처: 설치 시 컴퓨터가 접근]
GitHub Releases -> 런처 APK / 선택한 SmartTube APK
F-Droid --------> personalDNSfilter APK
HaGeZi·상류 설정 -> 필터 목록·설정 -> 조합 후 TV로 전송
런처가 외부에서 목록을 받아 오는 구조가 아닙니다. APK와 필터 자료를 내려받는 일은 컴퓨터의 스크립트가 맡습니다.
report/와 dumbtv.conf도 컴퓨터 쪽 파일입니다.
TV의 런처 설정인 home.conf와 이름이 비슷하니 구별하세요.
어떤 패키지를 끌지는 data/fireos/packages.txt, 절대 끄지 않을 대상은
never-disable.txt, 비활성화가 거부되는 대상은 protected.txt가 설명합니다.
스크립트에 패키지 이름을 길게 박아 넣는 대신 데이터와 실행 절차를 나눈 것입니다.
lib.sh의 fos()는 API 28 미만에서 Fire OS 6 전용 파일이 있으면 그 파일을 선택하고,
없으면 data/fireos/로 돌아갑니다. cleanup_list()는 사용자가 Alexa를 끄겠다고 했을 때만 추가 목록을 합칩니다.
# scripts/debloat.sh의 실제 루프 일부
for p in $(cleanup_list); do
if protected_by_policy "$p"; then
printf ' %-46s SKIP (never-disable list)\n' "$p"; continue
fi
if keep_pkg "$p"; then
printf ' %-46s SKIP (keep list)\n' "$p"; continue
fi
if ! present "$p"; then
printf ' %-46s absent\n' "$p"; absent=$((absent+1)); continue
fi
disable_pkg "$p"
done
이 루프에서 정책 금지, 사용자 유지, 실제 설치 여부를 차례로 검사합니다.
protected_by_policy()는 첫 필드인 패키지 식별자만 비교합니다.
앞서 list_of()가 주석과 행 끝 공백, 빈 줄을 제거합니다.
never-disable은 프로젝트 정책상 건드리지 않을 핵심 기능,
protected는 운영체제가 끄기를 거부하는 패키지,
keep은 사용자가 유지하겠다고 선택한 대상입니다. 세 집합이 같은 뜻은 아닙니다.disable_pkg()는 명령 출력에 따라 disabled, PROTECTED,
system-disabled, SUSPECT를 나눕니다.
lockdown.sh는 UID를 읽지 못하거나 UID가 10000 미만이면 패키지별 제한을 건너뜁니다.
낮은 시스템 UID를 제한하면 같은 UID를 공유하는 다른 구성 요소에도 영향을 줄 수 있기 때문입니다.
작업 목록에 문 이름이 적혀 있어도, 공사 직전에 “비상구인가, 주인이 쓰는 방인가, 실제 존재하는 문인가”를 다시 보는 방식입니다. 목록을 신뢰하는 것과 실행 시 검사를 생략하는 것은 별개의 결정입니다.
all.sh는 preflight → configure(필요 시) → debloat → lockdown → dnsfilter → launcher → youtube(선택) → verify를 호출합니다.
각 주요 단계가 실패하면 || exit 1로 다음 단계를 멈춥니다.
하지만 앞에서 바꾼 내용이 자동으로 원상 복귀되는 DB 트랜잭션은 아닙니다.
| 변경 | 복구 코드 | 보존되는 정보의 범위 |
|---|---|---|
| 설정 값 조정 | preflight.sh 스냅샷 + restore_value() | secure·global을 기록하되, 복구는 목록에 있는 키를 대상으로 합니다. |
| 패키지 비활성화 | restore_list(), pm enable | 모든 패키지의 변경 전 enabled 상태를 저장하는 방식은 아닙니다. |
| HOME 연동 | launcher/install.sh --undo | Guard·Keeper 경로를 해제하고 기본 홈을 다시 엽니다. |
| DNS 연동 | always-on 설정 제거, 필터 비활성화 | 앱 자체를 삭제하지는 않습니다. |
preflight.sh는 첫 스냅샷이 있으면 덮어쓰지 않습니다.
변경 뒤 값을 다시 “원래 값”으로 기록하면 복구 기준을 잃기 때문입니다.
restore_value()는 스냅샷의 값이 비어 있으면 번들 기본값을 사용하므로, 원래 없던 키와 빈 값까지 완벽히 재현하는 복구는 아닙니다.
복구 순서도 중요합니다. restore.sh는 설정과 HOME 복구를 먼저 하고 DNS 필터 비활성화를 뒤로 미룹니다.
VPN을 끄면 네트워크 ADB 연결이 끊어질 수 있기 때문입니다.
이후 adb_reconnect()로 연결을 회복하고 재부팅합니다.
reboot_wait()는 sys.boot_completed=1만 보지 않고 이전과 이후의
/proc/sys/kernel/random/boot_id를 비교합니다.
명령이 무시되어 TV가 계속 켜져 있는 상태를 새 부팅으로 오인하지 않도록 한 것입니다.
외부 기기를 바꾸는 자동화에서는 명령의 성공 문구보다, 다음에 읽은 실제 상태가 더 중요한 증거입니다.
목표는 HOME을 눌렀을 때 dumbtv 화면이 나타나게 하는 것입니다. 소스 주석과 기기 보고에 따르면 Fire OS는 Amazon 홈을 보호하고 HOME 키도 먼저 처리합니다. 따라서 런처가 단순히 HOME 키 이벤트를 가로채는 구조로 설명하면 틀립니다.
| 경로 | 감지 방법 | 실제 전환 |
|---|---|---|
| Fire OS 7·8 | Guard.onAccessibilityEvent()가 특정 Amazon 홈 창 확인 | launchWhenCovered() → home() → 알람의 PendingIntent |
| Fire OS 6 | Keeper.check()·top()가 UsageStats 조회 | launch()가 기존 작업을 moveToFront(), 옮길 작업이 없거나 예외가 나면 알람 |
Fire OS 7·8
창 변경 이벤트
-> 패키지 + HomeActivity_vNext 확인
-> 가림 화면 표시
-> 첫 그리기 직전 콜백 또는 150 ms 대체 경로
-> AlarmManager -> Home
-> Home의 표시 콜백 -> 가림 화면 제거
Fire OS 6
250 ms 간격 조회
-> 최근 4초 UsageEvents에서 전면 앱·클래스 확인
-> 런처의 기존 작업 앞으로 이동 시도
-> 옮길 작업이 없거나 예외가 나면 알람으로 실행
Guard에는 반복 실행 제한 REPEAT_MS=1000, 가림 화면 상한 COVER_MAX_MS=1500,
그리기 콜백의 대체 지연 LAUNCH_FALLBACK_MS=150이 있습니다.
install.sh의 set_futurity()는 알람 최소 대기 설정에 min_futurity=0을 넣습니다.
이는 Android 전체에 통하는 일반 권장값이 아니라 이 구현이 택한 시스템 연동입니다.
Keeper는 HandlerThread에서 POLL_MS=250 간격으로 조회하고,
DEBOUNCE_MS=1500으로 잦은 재전환을 막습니다.
패키지뿐 아니라 홈 Activity 클래스까지 확인해 같은 패키지의 설정 화면을 빼앗지 않습니다.
Keeper.start() 자체는 API 26 이상이면 시작하지 않는 방어 조건도 갖습니다.
guard.xml은 canRetrieveWindowContent="false"입니다.
접근성 서비스를 쓰지만 화면 본문을 읽는 기능은 요청하지 않고 창 변경 이벤트를 이용합니다.
“권한이 없다”는 소개 문구와 “시스템이 접근성 서비스를 활성화한다”는 동작은 다른 층위의 이야기입니다.
Home은 io와 colorIo라는 단일 작업자 Executor를 두고,
UI 수정은 메인 Looper의 Handler로 보냅니다.
앱 목록을 갱신하는 loadApps()에는 요청 세대 번호가 있습니다.
// Home.loadApps(): 이미지 캐시 재사용 부분을 생략한 발췌
int generation = ++appsGeneration;
io.execute(() -> {
List<Item> list = Catalog.apps(this);
ui.post(() -> {
if (generation != appsGeneration || isDestroyed()) return;
// 기존 이미지 캐시를 옮긴 뒤 목록을 반영합니다.
apps.all = list;
render(apps);
});
});
요청 A 뒤에 B가 생겼다면 A 결과는 이미 낡은 결과입니다. 화면이 파괴된 뒤에도 반영하지 않습니다.
현재 작업자는 순차 실행되지만, UI에 쌓인 이전 결과를 무시하는 데에도 이 검사가 의미가 있습니다.
onDestroy()는 작업자를 종료하고 등록한 수신기도 해제합니다.
예외도 읽어야 합니다. 첫 화면의 firstApps()는 Future.get()으로 기다리고,
화면에 들어올 만큼의 배너를 즉시 읽습니다. 입력 행만 먼저 나타나는 깜빡임을 피하려는 선택입니다.
“이미지는 전부 비동기”라고 요약하면 이 시작 지연과 완성된 첫 화면 사이의 절충을 놓칩니다.
앱이 삭제되는 동안 목록 조회가 끝나거나, 배경 이미지 A를 읽는 중 사용자가 B를 선택하면 어떻게 될까요?
appsGeneration, wallKey, isDestroyed()가 각각 무엇을 막는지 설명할 수 있으면 비동기 코드의 큰 줄기를 이해한 것입니다.
dumbtv/
|-- AGENTS.md 에이전트의 실기기 작업 규칙
|-- README.md 사용자 안내와 프로젝트 주장
|-- scripts/ 컴퓨터에서 실행하는 Bash 14개
| |-- lib.sh ADB·설정·목록·재연결 공통 함수
| |-- all.sh 단계 순서와 실패 전파
| |-- configure.sh 사용자 선택 -> dumbtv.conf
| |-- preflight.sh 기기 조회와 최초 설정 스냅샷
| |-- debloat.sh 패키지 비활성화·선택 앱 제거
| |-- lockdown.sh 보호 패키지의 배경 실행·데이터 제한
| |-- dnsfilter.sh 외부 필터 설치와 목록 구성
| |-- launcher.sh 런처 설치기 연결
| |-- verify.sh 실제 상태 확인
| `-- restore.sh 복구 절차
|-- data/
| |-- fireos/ Fire OS 7·8 정책과 공통 설정
| |-- fireos6/ Fire OS 6 전용 목록
| |-- androidtv/ 수동 작업용 참고 목록
| |-- blocklists/ 추가·검증용 도메인 목록
| `-- apps.txt 앱 이름과 패키지 식별자 대응
|-- launcher/
| |-- src/tv/dumbtv/home/ Java 구현 13개
| |-- res/ 아이콘·문자열·테마·접근성 XML
| |-- tests/ FocusColorTest.java
| |-- AndroidManifest.xml 앱 구성·기능·권한 선언
| |-- version.env 버전과 릴리스 APK 해시
| |-- build.sh Android SDK 도구 직접 호출
| |-- install.sh APK 검증·설치·HOME 연동
| `-- release-check.sh 버전·해시 일관성 확인
|-- tests/ Python 테스트와 가짜 ADB
|-- catalog/
| |-- devices/ 실제 보고 22개 + 템플릿
| |-- schema.json 기기 보고 형식
| `-- validate.py YAML 해석과 일부 스키마 규칙 검사
|-- docs/ 원리·문제 해결·다른 TV 안내
`-- .github/workflows/
`-- validate.yml 카탈로그·APK·스크립트 CI
트리는 핵심 경로를 추린 것입니다. discover.sh는 모르는 기기의 패키지를 조사하고,
contribute.sh는 기기 보고 작성을 돕습니다. youtube.sh는 사용자 동의에 따른 별도 선택 경로입니다.
| 파일 | 책임 | 처음 볼 함수 |
|---|---|---|
Home.java | 화면 구성, 갱신, 포커스, 수명 관리 | onCreate(), render(), open() |
Catalog.java | 앱·입력 조회, 노출 여부, 순서 | apps(), visible(), saveOrder() |
Config.java | 파일 설정·기본값·배경 이미지 선택 | refresh(), save(), wallpaper() |
Tile.java·Grid.java | 타일 그리기·배치 | Android의 그리기·측정 콜백 |
Guard.java·Keeper.java | 세대별 HOME 전환 | onAccessibilityEvent(), check() |
Input.java | 입력 앱 선택·내장 HDMI 화면 | intent(), onStart(), onStop() |
FocusColor.java | 아이콘에서 배경용 대표색 추출 | representative(), tint() |
Options.java·Ui.java | 설정 화면·공통 시각 요소 | Config.Choice와 연결해서 읽기 |
| 대상 | 파일 수 | 물리 줄 수 |
|---|---|---|
| Java 구현 | 13 | 2,529 |
| Java 테스트 | 1 | 32 |
| Bash 전체 | 17 | 2,106 = scripts 1,744 + launcher 362 |
| Python 전체 | 11 | 1,052 = 테스트·가짜 장치 805 + 카탈로그 검증 247 |
| XML | 6 | 144 |
| YAML·YML | 27 | 1,550 |
계수는 .git을 제외하고 해당 확장자의 파일을 읽어 빈 줄·주석을 포함해 센 값입니다.
커밋 12c21f7의 소스 파일 기준이며, 실행 코드만의 줄 수나 GitHub 언어 비율과는 다릅니다.
Java 합계는 2,561줄이고, Git 추적 파일은 134개입니다.
기기 보고 22개 중 최상위 status 값은 tested 6개, partial 16개입니다.
README에 이름이 나왔다고 모두 전체 자동화 검증을 통과한 기기는 아닙니다.
기기별 보고 상태가 다른 펌웨어의 호환성까지 보장하지는 않습니다.
configure.sh의 yn()은 질문을 표준 오류에, 답인 0 또는 1을 표준 출력에 씁니다.
ALEXA=$(answer ...)가 설명 문장까지 값으로 받아 버리지 않도록 한 것입니다.
터미널에서 보기 좋은 출력과 프로그램이 읽기 좋은 출력을 나누는 간단한 사례입니다.
lib.sh의 dev()는 장치 명령을, acmd()는 dry-run을 고려한 변경 명령을 감쌉니다.
dev_ok()는 명령이 성공 종료했더라도 출력에 특정 연결 오류 문자열이 있으면 중단합니다.
all.sh의 run()은 Bash 3.2에서 빈 배열과 set -u가 만나는 예외까지 처리합니다.
# scripts/lib.sh의 변경 명령 경계
acmd() {
if [[ $DRY_RUN == 1 ]]; then note "[dry-run] $*"; return 0; fi
dev "$@"
}
실습 아이디어: “계획 출력”과 “기기 명령”을 서로 다른 색의 펜으로 표시하세요.
모든 변경이 이 함수 하나를 거치는지 찾아보면, dnsfilter.sh의 실제 설치·파일 전송처럼 별도 분기에서 dry-run을 검사하는 경로도 보입니다.
Catalog.apps()는 TV용 CATEGORY_LEANBACK_LAUNCHER를 먼저 모으고 일반
CATEGORY_LAUNCHER를 추가합니다. LinkedHashMap의 패키지 키로 중복을 줄이고,
Collator로 이름을 정렬합니다. shown()은 일반 앱의 기본 노출도 별도로 판단합니다.
Input.intent()는 입력 URI를 받을 앱과 설정 모드를 보고 TV의 앱에 넘길지, 내부 TvView를 사용할지 결정합니다.
내부 화면의 onStart()는 입력을 연결하고 오디오 포커스를 요청하며,
onStop()는 view.reset()과 오디오 포커스 반납으로 자원을 놓습니다.
실습 아이디어: HDMI 화면에서 HOME 또는 BACK을 누른 순간을 그려 보세요. 화면을 숨기는 것만으로 충분한지, 입력 세션과 오디오를 함께 해제해야 하는지 설명해 보세요. 화면 수명과 하드웨어 자원 수명이 만나는 지점입니다.
Config는 home.conf의 key=value를 읽고 알 수 없는 키도 맵에 보존합니다.
get(Choice)는 허용된 선택지인지 검사해 기본값으로 돌아갑니다.
refresh()는 수정 시각과 파일 길이로 변경을 감지하므로 내용 전체 해시 비교는 아닙니다.
# TV 런처의 home.conf를 설명하기 위한 예시 데이터
accent=amber
layout=grid
clock=24h
background=black
inputs=on
save()는 home.conf.tmp에 먼저 쓰고 renameTo()로 교체합니다.
실제 설정 파일을 직접 덮어쓰다가 중간에 끊기는 문제를 줄이려는 방식입니다.
다만 디스크 동기화까지 보장하는 저널이나 완전한 충돌 복구 체계를 구현한 것은 아닙니다.
실습 아이디어: layout=unknown, 값이 빈 줄, 주석, 알 수 없는 키를 각각 넣었다고 가정하고
read()와 get()의 결과를 표로 적어 보세요.
컴퓨터의 dumbtv.conf는 Bash로 source하는 파일이라 이 파서와 다르다는 점도 확인하세요.
FocusColor.from()은 Drawable 복사본을 48×48 픽셀로 그린 뒤 representative()에 넘깁니다.
알파가 96 미만인 픽셀은 제외하고, 12개 그룹 중 채도가 있는 색에 가중치를 더 줍니다.
밝기가 높으면 RGB 크기를 줄여 흰 글씨를 올릴 어두운 배경색을 만듭니다.
FocusColorTest.java는 흰 바탕 75개와 붉은색 25개로 구성한 아이콘에서도 붉은색이 선택되는지 확인합니다.
단순 전체 평균이면 바탕의 흰색에 끌려가기 쉬운 문제를 테스트 데이터로 표현한 것입니다.
실습 아이디어: “평균색”과 “대표색”의 차이를 네모 100개로 그려 보세요.
tint()가 RGB는 유지하고 알파를 61로 바꾸는 이유도 계산해 보세요.
61/255는 약 24%이며, 채도와 투명도는 서로 다른 개념입니다.
compose_blocklist()는 내려받은 목록, 추가 목록, 선택한 Alexa 목록을 합칩니다.
PRIME=1이면 aiv-delivery, aiv-cdn, 특정 메시징 호스트를 제외합니다.
expected_blocked()도 같은 사용자 선택에 맞춰 검증 대상을 바꿉니다.
verify.sh는 차단 대상의 ping 첫 줄에서 0.0.0.0 또는 127.0.0.1을 찾고,
허용 대상에는 잘못된 차단·이름 해석 실패가 없는지 검사합니다.
ping의 응답 주소를 관찰하는 휴리스틱이며, 영상 재생·로그인·모든 통신 경로를 시험하는 것은 아닙니다.
차단 도구의 좋은 검증은 막혀야 할 주소뿐 아니라, 계속 열려 있어야 할 주소도 확인합니다.
실습 아이디어: Prime·Alexa 두 선택의 네 조합을 만들고 차단 목록과 검증 기대값이 함께 바뀌는지 추적하세요. 네트워크 VPN 필터는 이 구성에서 DNS를 걸러 주는 수단입니다. 외부 VPN 사업자가 익명성을 제공한다는 의미로 읽으면 안 됩니다.
tests/test_debloat.py는 환경변수 ADB에 tests/fake_tv.py 경로를 넣습니다.
가짜 장치는 JSON 상태를 읽고 받은 명령을 로그에 남깁니다.
스크립트 본체를 대대적으로 바꾸지 않고도 “무엇을 비활성화했는가”를 확인하는 구조입니다.
test_restore.py는 필터 종료 때 연결이 끊기는 상황과 재부팅 요청이 무시되는 상황을 다룹니다.
test_verify.py는 접근성 서비스가 설정상 켜졌어도 실제 바인딩되지 않은 경우를 구분합니다.
Python 테스트 파일 8개의 test_ 메서드는 정적으로 세어 51개입니다.
CI는 APK를 만든 뒤 권한 선언도 검사합니다. 모든 권한 항목에 maxSdkVersion=27이 있어야 하며,
항목이 3개를 넘거나 이 상한이 없는 항목이 있으면 실패합니다.
이 검사는 APK의 권한 선언을 검사하며, 실제 기기에서의 동작 검증은 별도로 필요합니다.
| 목적 | 필요한 것 | 근거·범위 |
|---|---|---|
| 소스 학습 | 텍스트 편집기, Git 체크아웃 | TV·GPU·API 키가 없어도 이 글의 코드 경로를 읽을 수 있습니다. |
| 일반 사용 | 컴퓨터, ADB, TV에 도달하는 네트워크 | README: macOS·Linux·Windows의 WSL 또는 Git Bash |
| 자동 정리 대상 | Fire OS 6·7·8 기기 | need_supported_fireos(): 제조사 Amazon*, API 25 이상 검사 |
| 기기 승인 | ADB 디버깅 활성화와 TV 리모컨 | 첫 연결 허용 창은 사람이 승인해야 합니다. |
| APK 직접 빌드 | JDK 17 권장, Android SDK API 25, build-tools | CI 기준 build-tools 34.0.0. 단순 릴리스 설치에는 빌드 환경이 보통 필요 없습니다. |
| 카탈로그 개발 | Python, 전체 YAML 파싱에는 PyYAML | CI Python 3.12. PyYAML이 없으면 한 단계의 중첩까지만 읽는 제한된 파서 사용 |
| 추가 다운로드 | 컴퓨터의 인터넷 연결 | 선택한 APK, DNS 목록과 설정을 외부 배포처에서 받습니다. |
need_supported_fireos()는 API 상한을 강제로 막지 않습니다.
fireos_name()은 API 30 초과를 “Fire OS 8보다 새로움·미시험”으로 표시합니다.
코드가 실행을 허용하는 범위와 검증했다고 말할 수 있는 범위는 구별해야 합니다.
| 플랫폼 | 이 저장소가 안내하는 범위 |
|---|---|
| Fire OS 6 | 별도 패키지 목록, UsageStats Keeper 경로. API 25이며 세 가지 구형 권한 선언이 적용됩니다. |
| Fire OS 7·8 | 접근성 Guard 경로. 구형 권한 세 개는 API 27 상한 때문에 적용되지 않습니다. |
| Fire OS 5 이하 | 자동 정리 스크립트가 거부합니다. 루트 우회 절차를 제공하는 프로젝트가 아닙니다. |
| Android TV·Google TV | 참고 목록과 수동 보고가 있습니다. Fire OS 정리 스크립트의 자동 지원으로 해석하면 안 됩니다. |
| Samsung·LG·Roku 등 | docs/OTHER-TVS.md에서 별도로 다룹니다. Android ADB 절차를 그대로 적용할 대상이 아닙니다. |
README는 최소 RAM·CPU·저장 공간의 수치 요구사항을 제시하지 않습니다. README의 메모리 실측은 성능 사례이지 최소 사양표가 아닙니다. HDMI 입력의 실제 제공 여부도 TV와 스트리밍 스틱이 다르므로 앱 목록처럼 모두에게 있다고 가정하면 안 됩니다.
dnsfilter.sh는 API 30 이상에서 앱 전용 데이터 폴더에 접근하지 못하면
Android/media로 목록을 보내고, 필터 앱에서 그 목록을 선택하라고 안내한 뒤 종료합니다.
이 분기에서는 일반 경로의 DNS 라우팅 설정을 모두 적용할 수 없다고 소스가 밝힙니다.
“실행 한 번으로 모든 기기가 자동 설정된다”는 설명은 맞지 않습니다.
기기별 메뉴와 대화상자는 catalog/devices/를 먼저 읽으세요.
기본 목록은 OTA 업데이트 서버도 막으므로 업데이트 정책까지 함께 바뀐다는 점을 이해해야 합니다.
아래 과제는 읽기·계산·별도 모형 작성으로 진행할 수 있습니다. 원본 코드를 보존하고 실습 파일은 별도 연습 폴더에 두세요. 실제 ADB 대신 명령을 기록하는 모형을 사용하면 TV 없이도 실패 상황을 다룰 수 있습니다.
scripts/all.sh, configure.sh, lib.sh를 읽고
설정 파일 없음·기존 설정 있음·YOUTUBE=1의 세 경우를 그리세요.
제출물: 실행 단계 순서표와 기본 선택 여섯 가지. 완료 기준: Prime·Alexa·런처·DNS는 기본 1, YouTube는 0, 제거 앱은 없음으로 설명하고, 기존 설정이 있으면 질문을 다시 하지 않는 이유를 말할 수 있어야 합니다.
list_of()와 protected_by_policy()를 읽고 아래 세 줄에서 비교할 식별자를 직접 적으세요.
이어서 별도 모형 함수로 “주석 제거 → 빈 줄 제거 → 첫 필드 선택”을 작성해 보세요.
com.example.home # 홈 화면
# 설명만 있는 줄
com.example.input # 입력 서비스
제출물: 결과 com.example.home, com.example.input과 모형 함수.
완료 기준: 행 전체 문자열 비교가 왜 실패할 수 있는지 설명하고, 빈 줄은 패키지로 취급하지 않아야 합니다.
compose_blocklist(), expected_blocked(), tests/test_blocklist.py를 읽으세요.
PRIME과 ALEXA의 네 조합에서 CDN 예시 호스트·Alexa 호스트·업데이트 호스트가 어떻게 달라지는지 정리하세요.
제출물: 4행 표와 허용 호스트 두 개를 잘못 막았을 때의 예상 실패 메시지. 완료 기준: Prime 예외가 모든 Amazon 주소를 허용하는 것은 아니며, 정책 변경과 검증 기대값 변경이 한 쌍임을 설명해야 합니다.
tests/fake_tv.py와 test_restore.py의 상태와 명령 로그를 읽으세요.
종이 또는 별도 프로그램으로 연결됨 → 필터 종료 → 연결 끊김 → 재연결의 상태 전이를 표현하세요.
제출물: 상태표와 올바른 복구 명령 순서에 대한 검증 조건 세 개. 완료 기준: 설정 쓰기가 필터 종료보다 먼저이며, 끊긴 연결에서 다음 명령을 성공 처리하지 않아야 합니다. 실제 ADB 대신 받은 명령을 기록하는 인터페이스만 사용하세요.
preflight.sh와 restore.sh의 차이를 바탕으로, 패키지 활성 상태와 설정의 존재 여부까지 저장하는
JSON 기록 형식을 설계하세요. 예를 들어 값 없음과 빈 문자열을 다른 상태로 표현해야 합니다.
{
"package": "com.example.metrics",
"enabled_before": false,
"setting_before": {"exists": false, "value": null},
"change_applied": true
}
제출물: 기록 형식, 적용·복구 의사코드, 중간 실패 사례 두 개. 완료 기준: 원래 꺼져 있던 패키지를 복구 과정에서 켜지 않고, 쓰기 성공이 확인된 작업만 보상하며, 복구를 두 번 요청해도 결과가 안정적이어야 합니다.
실습의 완료 기준을 “명령을 실행했다”가 아니라 “실패해도 지켜야 할 상태를 설명했다”로 잡아 보세요.
| 주차 | 주제·읽을 코드 | 학습 결과물 |
|---|---|---|
| 1주차 | Bash의 인자·출력·종료 코드. lib.sh의 dev(), acmd(), confirm() |
조회 명령과 변경 명령을 분류한 표, dry-run의 동작 설명 |
| 2주차 | 정책 데이터와 외부 기기 상태. debloat.sh, data/fireos/, test_debloat.py |
금지·유지·부재·보호·성공의 분기표, 각 분기의 입력 예시 |
| 3주차 | Android Activity·Intent·View. Home, Catalog, Input |
앱 실행과 입력 전환의 호출도, 시작·중지 시 자원 목록 |
| 4주차 | 스레드·콜백·포커스. loadApps(), loadArt(), focusBackground() |
작업자와 UI 스레드의 시간축, 오래된 결과를 버리는 세 조건 |
| 5주차 | DNS·VPN·관찰 가능한 상태. compose_blocklist(), dnsfilter.sh, verify.sh |
정책 진리표, 차단 성공과 앱 정상 동작을 구별하는 검증 계획 |
| 6주차 | 복구·테스트 대역·배포 검증. restore.sh, fake_tv.py, CI, build.sh |
실패 복구 설계서, APK 제작 단계 그림, 해시와 서명의 차이 설명 |
1~2주차를 마치면 CLI의 입력·출력 계약과 정책 검사로 확장하세요. 3~4주차는 Android 공식 Activity 수명 문서를 옆에 두고, 콜백마다 언제 등록하고 해제하는지 표시하면 좋습니다. 5~6주차에는 “설정이 존재한다”와 “기능이 실제 동작한다” 사이에 어떤 관찰이 필요한지 적어 보세요.
레포를 읽는 일은 완성된 기계를 분해하는 것과 비슷합니다. 부품 이름만 외우면 다시 조립할 수 없습니다. 각 선이 왜 연결되어 있고, 연결이 끊어졌을 때 무엇으로 알아차리는지까지 알아야 합니다.
source하는 것은 어떤 차이가 있나요?파일을 많이 읽는 것보다, 한 기능의 입력·변경·증거·복구 경로를 끝까지 연결하는 편이 더 깊은 학습이 됩니다.
| 용어 | 쉬운 설명 |
|---|---|
| ADB | 컴퓨터와 Android 기기 사이의 명령 통로입니다. lib.sh의 dev()가 감쌉니다. |
| Fire OS | 이 레포가 주로 다루는 Amazon의 Android 기반 TV 시스템입니다. 세대별 API와 제한이 다릅니다. |
| 패키지 | 설치된 앱·서비스를 식별하는 단위입니다. com.amazon.tv.launcher 같은 이름으로 다룹니다. |
| disable-user | 지정 사용자에서 패키지를 꺼 두는 동작입니다. APK 파일을 시스템에서 삭제하는 것과 다릅니다. |
| 루트 권한 | 시스템 전체에 강한 변경 권한을 가진 사용자 권한입니다. dumbtv의 정상 경로는 이를 요구하지 않습니다. |
| dm-verity | 시스템 영역의 무결성을 확인하는 장치입니다. 레포는 /system을 수정하지 않는 경계를 둡니다. |
| AppOps | 앱의 특정 동작을 허용하거나 제한하는 Android 제어 장치입니다. 여기서는 배경 실행·VPN 활성화 등에 사용합니다. |
| UID | 앱이 실행되는 사용자 식별값입니다. 여러 구성 요소가 공유할 수 있어 네트워크 제한의 영향 범위를 살펴야 합니다. |
| DNS | 도메인 이름을 주소로 바꾸는 과정입니다. 이 레포는 목록에 있는 이름이 차단 주소를 받는지 확인합니다. |
| 로컬 VPN | 기기 안의 가상 네트워크 경로입니다. 이 구성에서는 personalDNSfilter가 DNS를 다루는 데 사용합니다. |
| OTA | 네트워크를 통한 기기 업데이트입니다. 기본 DNS 정책에는 업데이트 서버 차단도 들어 있습니다. |
| ACR | 화면 내용의 특징을 이용해 시청 콘텐츠를 식별하는 기술입니다. README가 줄이려는 추적 기능 중 하나입니다. |
| 런처 | 앱을 선택하고 실행하는 홈 화면입니다. Home은 이 레포가 직접 만든 런처입니다. |
| Intent | 앱이나 시스템에 특정 화면·작업을 요청하는 메시지입니다. 앱 실행과 TV 입력 URI 전달에 쓰입니다. |
| Activity | Android에서 사용자 화면을 맡는 구성 요소입니다. 시작·일시 중지·종료 등의 수명이 있습니다. |
| 접근성 서비스 | 사용자 지원을 위한 Android 서비스입니다. Guard는 창 변경 이벤트를 HOME 연동에 이용합니다. |
| PendingIntent | 나중에 시스템이 실행할 수 있도록 준비한 요청입니다. Guard.home()은 알람에 이를 등록합니다. |
| UsageStats | 앱 사용 이벤트를 조회하는 Android 기능입니다. Keeper.top()이 전면 앱을 알아내는 데 씁니다. |
| TvView | TV 입력 서비스가 제공하는 영상을 표시하는 Android View입니다. Input의 내부 입력 화면에 사용합니다. |
| 메인 스레드 | 화면을 갱신하고 입력을 처리하는 실행 흐름입니다. 오래 걸리는 일을 맡기면 화면이 멈출 수 있습니다. |
| 테스트 대역 | 실제 외부 구성 요소를 대신하는 모형입니다. fake_tv.py는 ADB 대신 상태와 명령을 기록합니다. |
| 스냅샷 | 변경 전 상태의 기록입니다. 이 레포에서는 일부 설정 복구의 기준이며 TV 전체 백업은 아닙니다. |
| 보상 작업 | 이미 적용한 변경을 반대 동작으로 되돌리는 절차입니다. restore.sh에서 구체적으로 볼 수 있습니다. |
| 멱등성 | 같은 작업을 반복해도 결과가 더 달라지지 않는 성질입니다. 모든 스크립트가 완전히 이를 보장한다는 뜻은 아닙니다. |
| SHA-256 | 파일 내용의 일치 여부를 비교하는 해시입니다. 신뢰하는 예상 해시와 비교해야 의미가 있습니다. |
| APK 서명 | Android가 앱 설치·업데이트의 서명 관계를 확인하는 장치입니다. 빌드 키가 달라지면 기존 앱 업데이트가 거부될 수 있습니다. |
| shallow clone | 최근 일부 이력만 가져온 Git 복제본입니다. 현재 파일은 읽을 수 있지만 전체 커밋 통계를 낼 수는 없습니다. |
저장소 메타데이터는 2026-10-23 GitHub API 기준입니다. 파일·설정·줄 수는 커밋 12c21f7 기준이며, 성능 수치는 README에 기록된 측정 사례입니다.
| 항목 | 상태 | 근거 |
|---|---|---|
| 스타 / 포크 | 1,312 / 113 | 2026-10-23 GitHub API 기준 |
| 생성일 | 2026-10-18 | 2026-10-23 GitHub API 기준 |
| 주 언어 | Java | 2026-10-23 GitHub API 기준 |
| 최신 릴리스 | launcher-v1.2 · 2026-10-22 | 2026-10-23 GitHub API 기준. VERSION=1.2와 일치 |
| 최근 push | 2026-10-22 | 2026-10-23 GitHub API 기준 |
| 소스 기준 커밋 | 12c21f7 · 2026-10-23 | Git 기록. dumbtv.org 링크를 문서·템플릿에 연결한 변경 |
| 코드 규모 | Java 2,561 / Bash 2,106 / Python 1,052줄 | 빈 줄·주석·테스트 포함 |
| 파일·테스트 | 추적 파일 134개 / Python 테스트 메서드 51개 | 정적 계수. 실행 결과·커버리지 수치가 아님 |
| 기기 카탈로그 | 22개 보고: tested 6 / partial 16 | 템플릿 제외, YAML 최상위 상태 값 계수 |
| API 라이선스 | NOASSERTION | 2026-10-23 GitHub API 기준 |
| 소스 라이선스 | 코드·문서·런처 MIT / 카탈로그 CC BY-SA 4.0 | LICENSE, catalog/LICENSE의 명시 내용 |
| 수치 해석 | 물리 줄 수·정적 테스트 개수 | 성능·테스트 통과율·커버리지 수치와 구별 |
NOASSERTION은 라이선스 파일의 부재를 뜻하지 않습니다.
반대로 외부에서 받는 필터·목록까지 모두 MIT라고 하면 틀립니다.
README는 personalDNSfilter GPL-2.0, HaGeZi 목록 GPL-3.0, SmartTube MIT를 별도로 안내합니다.
최근 push 날짜와 커밋에 기록된 날짜는 서로 다른 항목입니다. 소스 설명과 코드 규모는 명시한 커밋을 기준으로 읽으세요.
복구는 완전한 기기 백업이 아닙니다. restore.sh는 대상 목록을 다시 켜며,
기존 패키지 활성 상태 전체를 복원하지 않습니다. 설정 스냅샷이 없거나 빈 값이면 기본값을 사용합니다.
삭제한 스토어 앱은 재설치해야 하고, 추가한 런처·필터·SmartTube는 설치된 채 남습니다.
README의 “필터를 제거한다”는 요약과 달리 실제 코드는 필터를 비활성화합니다.
DNS는 모든 광고를 구분하지 못합니다. README는 YouTube 광고가 영상과 같은 서버를 써 DNS로 분리 차단할 수 없다고 설명합니다. 선택한 SmartTube 설치는 별도 접근입니다. 도메인 테스트 성공도 모든 앱 기능이나 모든 추적 통신이 사라졌다는 증거는 아닙니다.
설정과 실제 동작은 다릅니다. Fire OS 8의 수동 DNS 분기, Fire OS 6의 시각 동기화 예외,
접근성 서비스의 실제 바인딩을 기기별로 봐야 합니다. SYSTEM_DNS=1은 Fire OS 6에만 적용되며, 시스템 사용자로 실행되는 앱도 필터를 우회하게 합니다.
업데이트 서버 차단은 보안 업데이트 수신에도 영향을 줍니다.
실행 계획은 단계별로 확인해야 합니다. 사용자 선택은 설정 파일과 각 실행 단계에서 반영됩니다.
preflight.sh의 계획 출력도 사용자 설정을 모두 반영하는 최종 실행 계획은 아닙니다.
실제 변경 대상은 설정을 읽은 debloat.sh의 분기까지 확인해야 합니다.
목록을 코드에서 분리하고, 기기 명령을 한 경계로 감싸고, 실제 상태를 다시 읽고, 실패한 작업의 복구 순서를 설계하세요. dumbtv는 이런 결정을 Java와 Bash 몇 천 줄 안에서 구체적으로 살펴볼 수 있는 공부 자료입니다.