diff --git a/firetv/.gitignore b/firetv/.gitignore new file mode 100644 index 0000000..d544a51 --- /dev/null +++ b/firetv/.gitignore @@ -0,0 +1,5 @@ +.gradle/ +app/build/ +build/ +dist/ +local.properties diff --git a/firetv/README.md b/firetv/README.md new file mode 100644 index 0000000..31be55e --- /dev/null +++ b/firetv/README.md @@ -0,0 +1,51 @@ +# podcasters (p0dcasters.com) for Fire TV + +A small native Android app (`com.p0dcasters.tv`) that puts https://p0dcasters.com on a Fire TV +and makes it work with a remote. No dependencies beyond the Android SDK. + +- **WebView** on p0dcasters.com. Remote play/pause, fast-forward 30 s and rewind 15 s drive the page's player (`pitMedia`). Results open in the app; `mailto:`/`intent:` links go to the system. +- **Remote navigation** (`app/src/main/assets/tv.js`): Chromium's WebView scrolls on arrow + keys instead of moving between links, so every D-pad press runs `pitNav(dir)` in the page: + spatial focus to the nearest link/button, scrolling across long gaps, and a large focus + ring. The ring is set through `element.style` because strict CSPs (`style-src 'self'`) + blocks an injected ` + + +
+

podcasters is out of reach

+

Check this TV's network connection, then press OK to try again.

+ Try again +
+ + diff --git a/firetv/app/src/main/assets/tv.js b/firetv/app/src/main/assets/tv.js new file mode 100644 index 0000000..e0770f0 --- /dev/null +++ b/firetv/app/src/main/assets/tv.js @@ -0,0 +1,174 @@ +/* Podcasters (p0dcasters.com) for Fire TV: remote-control navigation for any page in the WebView. + * + * Chromium's WebView scrolls on arrow keys instead of moving between links, so + * the activity hands every D-pad press to pitNav() and OK to pitEnter(). Both + * return a short string the activity acts on. Written in ES5 on purpose: Fire + * OS 5 devices still ship WebView 70-something. + */ +(function () { + if (window.__pitTV) return; + window.__pitTV = true; + + var SEL = 'a[href],button:not([disabled]),input:not([type=hidden]):not([disabled]),' + + 'select:not([disabled]),textarea:not([disabled]),summary,[tabindex]:not([tabindex="-1"])'; + var TEXT = /^(search|text|email|url|tel|number|password|)$/; + var field = null; // the text field OK was pressed on, while the keyboard is up + + // The focus ring is set through CSSOM on the element itself: p0dcasters.com (and + // many sites) ship style-src 'self', which blocks an injected + + + diff --git a/firetv/build.gradle b/firetv/build.gradle new file mode 100644 index 0000000..bb97a9a --- /dev/null +++ b/firetv/build.gradle @@ -0,0 +1 @@ +plugins { id 'com.android.application' version '8.5.2' apply false } diff --git a/firetv/build.sh b/firetv/build.sh new file mode 100644 index 0000000..39f6c80 --- /dev/null +++ b/firetv/build.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Builds the Fire TV APK. Runs inside gradle:8.7-jdk17; see README.md for the docker line. +# /sdk Android SDK cache (installed on first run) +# /keys optional: release.jks + release.env (PIT_TV_STORE_PASSWORD=...) to sign +set -eu +SDK=/sdk +TOOLS=commandlinetools-linux-11076708_latest.zip +if [ ! -x "$SDK/cmdline-tools/latest/bin/sdkmanager" ]; then + mkdir -p "$SDK/cmdline-tools" + curl -fsSL "https://dl.google.com/android/repository/$TOOLS" -o /tmp/tools.zip + unzip -q /tmp/tools.zip -d "$SDK/cmdline-tools" + mv "$SDK/cmdline-tools/cmdline-tools" "$SDK/cmdline-tools/latest" +fi +yes | "$SDK/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$SDK" --licenses >/dev/null +"$SDK/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$SDK" "platforms;android-34" "build-tools;34.0.0" >/dev/null +export ANDROID_HOME="$SDK" + +if [ -f /keys/release.jks ]; then + set -a; . /keys/release.env; set +a + export PIT_TV_KEYSTORE=/keys/release.jks +fi + +gradle --no-daemon -q lint assembleRelease +OUT=app/build/outputs/apk/release +APK=$(ls "$OUT"/*.apk | head -1) +"$SDK/build-tools/34.0.0/apksigner" verify --print-certs "$APK" | head -3 || true +"$SDK/build-tools/34.0.0/aapt2" dump badging "$APK" | grep -E "^(package|sdkVersion|targetSdkVersion|leanback|application-label|uses-feature)" || true +mkdir -p dist +cp "$APK" dist/podcasters-firetv.apk +[ -n "${OWNER:-}" ] && chown -R "$OWNER" dist app/build .gradle 2>/dev/null +ls -l dist diff --git a/firetv/emulator-test.sh b/firetv/emulator-test.sh new file mode 100644 index 0000000..50ee5c1 --- /dev/null +++ b/firetv/emulator-test.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Boots an Android TV emulator (API 30, the base of Fire OS 8), installs the APK, +# drives it with remote-control keys and saves 1920x1080 screenshots to store/shots. +# Runs inside gradle:8.7-jdk17 with --device /dev/kvm and the SDK cache at /sdk: +# docker run --rm --device /dev/kvm -v ~/scratch/android-sdk:/sdk -v "$PWD/firetv:/w" -w /w \ +# gradle:8.7-jdk17 bash emulator-test.sh +set -euo pipefail +SDK=/sdk +export ANDROID_HOME=$SDK ANDROID_SDK_ROOT=$SDK ANDROID_AVD_HOME=/root/.android/avd +IMG="system-images;android-30;android-tv;x86" + +apt-get update -qq >/dev/null +apt-get install -y -qq libpulse0 libnss3 libxcomposite1 libxcursor1 libxi6 libxtst6 \ + libasound2 libgl1 libxkbfile1 libxdamage1 libxrandr2 libdrm2 libgbm1 >/dev/null 2>&1 + +SM="$SDK/cmdline-tools/latest/bin/sdkmanager --sdk_root=$SDK" +(yes || true) | $SM --licenses >/dev/null || true # yes dies of SIGPIPE: not a failure +$SM "emulator" "platform-tools" "$IMG" >/dev/null + +echo no | "$SDK/cmdline-tools/latest/bin/avdmanager" create avd -n tv -k "$IMG" -d tv_1080p --force >/dev/null +ADB="$SDK/platform-tools/adb" +"$SDK/emulator/emulator" -avd tv -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect \ + -no-snapshot -memory 2048 >/tmp/emu.log 2>&1 & + +"$ADB" wait-for-device +for i in $(seq 1 120); do + [ "$("$ADB" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = 1 ] && break + sleep 2 +done +echo "booted" +"$ADB" shell wm size + +# The package manager comes up a little after boot_completed; retry until it takes. +for i in $(seq 1 20); do "$ADB" install -r dist/podcasters-firetv.apk && break; sleep 5; done +mkdir -p store/shots +shot() { "$ADB" exec-out screencap -p > "store/shots/$1.png"; echo "shot $1"; } +key() { "$ADB" shell input keyevent "$@"; sleep 1.2; } +focus() { "$ADB" shell dumpsys window 2>/dev/null | grep -m1 mCurrentFocus || true; } + +# 1. Leanback launch, the way the Fire TV home screen opens it. +"$ADB" shell monkey -p com.p0dcasters.tv -c android.intent.category.LEANBACK_LAUNCHER 1 >/dev/null +sleep 12 +focus +shot 01-home + +# 2. The home page autofocuses the search box: OK raises the Fire TV keyboard. +# (Arrows moving the focus ring are checked on the results page below.) +key KEYCODE_DPAD_CENTER +sleep 2 +focus +shot 03-keyboard +"$ADB" shell input text "true%scrime" +key KEYCODE_ENTER +sleep 10 +shot 04-results +key KEYCODE_DPAD_DOWN KEYCODE_DPAD_DOWN KEYCODE_DPAD_DOWN KEYCODE_DPAD_DOWN +shot 05-results-nav +key KEYCODE_DPAD_RIGHT +shot 05b-results-right + +# 3. Back returns to the home page, Back again leaves the app. +key KEYCODE_BACK +sleep 4 +shot 06-back +key KEYCODE_BACK +sleep 2 +focus + +# 4. Menu key opens the standalone search box. +"$ADB" shell monkey -p com.p0dcasters.tv -c android.intent.category.LEANBACK_LAUNCHER 1 >/dev/null +sleep 8 +key KEYCODE_MENU +sleep 2 +shot 07-menu-search +"$ADB" shell input text "technology" +key KEYCODE_ENTER +sleep 10 +shot 08-menu-search + +"$ADB" logcat -d | grep -E "AndroidRuntime|FATAL|com.p0dcasters.tv|chromium.*Uncaught" | tail -20 || true +[ -n "${OWNER:-}" ] && chown -R "$OWNER" store/shots +ls -l store/shots diff --git a/firetv/gradle.properties b/firetv/gradle.properties new file mode 100644 index 0000000..ac73bc4 --- /dev/null +++ b/firetv/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2g +android.nonTransitiveRClass=true diff --git a/firetv/new-key.sh b/firetv/new-key.sh new file mode 100644 index 0000000..1b21858 --- /dev/null +++ b/firetv/new-key.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Mints the release keystore once. Runs inside eclipse-temurin:17-jdk with the +# output dir mounted at /keys. Store both files in the vault (p0dcasters-firetv--signing as +# PIT_TV_KEYSTORE_B64 + PIT_TV_STORE_PASSWORD), never in the repo. Losing this key +# means Fire TV users cannot update in place, so do not run it twice. +set -eu +[ -f /keys/release.jks ] && { echo "refusing: /keys/release.jks exists"; exit 1; } +umask 077 +PW=$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n') +keytool -genkeypair -keystore /keys/release.jks -storetype PKCS12 -alias podcasters \ + -keyalg RSA -keysize 4096 -validity 36500 -storepass "$PW" \ + -dname "CN=Podcasters (p0dcasters.com), O=Profullstack, C=US" >/dev/null 2>&1 +printf 'PIT_TV_STORE_PASSWORD=%s\nPIT_TV_KEY_ALIAS=podcasters\n' "$PW" > /keys/release.env +[ -n "${OWNER:-}" ] && chown "$OWNER" /keys/release.jks /keys/release.env +keytool -list -keystore /keys/release.jks -storepass "$PW" | tail -2 diff --git a/firetv/settings.gradle b/firetv/settings.gradle new file mode 100644 index 0000000..991dc48 --- /dev/null +++ b/firetv/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { google(); mavenCentral(); gradlePluginPortal() } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { google(); mavenCentral() } +} +rootProject.name = "podcasters-tv" +include ":app" diff --git a/firetv/store/listing.md b/firetv/store/listing.md new file mode 100644 index 0000000..b6cce52 --- /dev/null +++ b/firetv/store/listing.md @@ -0,0 +1,50 @@ +# Amazon Appstore listing: podcasters (Fire TV) + +- **Title:** podcasters (keyword p0dcasters, so the brand still matches) +- **Package:** com.p0dcasters.tv +- **Category:** Music & Audio > Podcasts (fallback: Entertainment) +- **Price:** Free. No in-app purchases. +- **Devices:** Fire TV only. Audience: 13+ (open podcast directory, unfiltered content). +- **Collects data:** no. No account needed to listen. +- **Privacy policy URL:** https://p0dcasters.com/privacy + +## Short description + +Every podcast with a feed, plus live radio and TV. Free, no account. + +## Long description + +podcasters puts p0dcasters.com on your TV: every podcast with a public feed, on any host, plus live radio and TV channels. + +Over 390,000 live shows read straight from their RSS feeds: shows on their own domain, shows on Anchor, Buzzsprout, Libsyn and every other host, and the big libraries like TWiT, NPR and the BBC. Free, no account, nothing to sign up for. + +Made for the remote: +- Press Menu to search, and type with the Fire TV keyboard or your voice. +- Arrow keys move between shows and episodes with a big, easy-to-see highlight. +- Play/pause, fast-forward 30 s and rewind 15 s from the remote. +- Back steps back through your pages, and leaves the app from the home page. + +Browse by category, language and network, find self-hosted shows, or tune in to radio and TV streams. + +## Feature bullets + +Every podcast with a public feed, on any host +Over 390,000 live shows, updated from their RSS feeds +Live radio stations and TV channels +Free, no account needed +Search with the Fire TV keyboard or by voice +Remote play/pause, fast-forward and rewind +Browse by category, language and network + +## Keywords + +podcasts, podcast player, radio, live tv, p0dcasters, podcasters, rss, free podcasts + +## Art (store/out and store/shots) + +| Slot | File | +| --- | --- | +| Icon 512x512 / 114x114 | out/icon-512.png, out/icon-114.png | +| Fire TV icon 1280x720 | out/firetv-icon-1280x720.png | +| Fire TV background 1920x1080 | out/firetv-background-1920x1080.png | +| Screenshots 1920x1080 | shots/01-home.png, 05-results-nav.png, 04-results.png, 08-menu-search.png | diff --git a/firetv/store/render.sh b/firetv/store/render.sh new file mode 100644 index 0000000..6e5fbb7 --- /dev/null +++ b/firetv/store/render.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Renders the launcher icons, TV banner and Amazon Appstore art. +# Icons come from the site's own logo (store/src/logo-512.png = public/icons/icon-512x512.png). +# Runs inside alpine (see firetv/README.md): +# docker run --rm -e OWNER=$(id -u):$(id -g) -v "$PWD/firetv:/w" -w /w alpine:3.20 sh store/render.sh +set -eu +apk add --no-cache rsvg-convert imagemagick font-dejavu >/dev/null +R=app/src/main/res +O=store/out +L=store/src/logo-512.png +mkdir -p "$O" "$R/mipmap-xhdpi" "$R/mipmap-xxhdpi" "$R/drawable-xhdpi" + +magick "$L" -resize 96x96 "$R/mipmap-xhdpi/ic_launcher.png" +magick "$L" -resize 144x144 "$R/mipmap-xxhdpi/ic_launcher.png" +magick "$L" -resize 512x512 "$O/icon-512.png" +magick "$L" -resize 114x114 "$O/icon-114.png" + +cd store/src +rsvg-convert -w 320 -h 180 banner.svg -o "../../$R/drawable-xhdpi/banner.png" +rsvg-convert -w 1280 -h 720 banner.svg -o ../out/firetv-icon-1280x720.png +rsvg-convert -w 1920 -h 1080 background.svg -o ../out/firetv-background-1920x1080.png +cd ../.. +# Fire TV art may not carry transparency. +magick "$O/firetv-icon-1280x720.png" -background "#14140f" -alpha remove -alpha off "$O/firetv-icon-1280x720.png" +magick "$O/firetv-background-1920x1080.png" -background "#14140f" -alpha remove -alpha off "$O/firetv-background-1920x1080.png" + +[ -n "${OWNER:-}" ] && chown -R "$OWNER" "$R" "$O" +ls -l "$O" diff --git a/firetv/store/shots/01-home.png b/firetv/store/shots/01-home.png new file mode 100644 index 0000000..2387b39 Binary files /dev/null and b/firetv/store/shots/01-home.png differ diff --git a/firetv/store/shots/03-keyboard.png b/firetv/store/shots/03-keyboard.png new file mode 100644 index 0000000..2387b39 Binary files /dev/null and b/firetv/store/shots/03-keyboard.png differ diff --git a/firetv/store/shots/04-results.png b/firetv/store/shots/04-results.png new file mode 100644 index 0000000..ecc1e4b Binary files /dev/null and b/firetv/store/shots/04-results.png differ diff --git a/firetv/store/shots/05-results-nav.png b/firetv/store/shots/05-results-nav.png new file mode 100644 index 0000000..3cd286d Binary files /dev/null and b/firetv/store/shots/05-results-nav.png differ diff --git a/firetv/store/shots/05b-results-right.png b/firetv/store/shots/05b-results-right.png new file mode 100644 index 0000000..5453af9 Binary files /dev/null and b/firetv/store/shots/05b-results-right.png differ diff --git a/firetv/store/shots/06-back.png b/firetv/store/shots/06-back.png new file mode 100644 index 0000000..4b8ea4f Binary files /dev/null and b/firetv/store/shots/06-back.png differ diff --git a/firetv/store/shots/07-menu-search.png b/firetv/store/shots/07-menu-search.png new file mode 100644 index 0000000..86ac169 Binary files /dev/null and b/firetv/store/shots/07-menu-search.png differ diff --git a/firetv/store/shots/08-menu-search.png b/firetv/store/shots/08-menu-search.png new file mode 100644 index 0000000..5dc70b8 Binary files /dev/null and b/firetv/store/shots/08-menu-search.png differ diff --git a/firetv/store/src/background.svg b/firetv/store/src/background.svg new file mode 100644 index 0000000..8f8369e --- /dev/null +++ b/firetv/store/src/background.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/firetv/store/src/banner.svg b/firetv/store/src/banner.svg new file mode 100644 index 0000000..826c067 --- /dev/null +++ b/firetv/store/src/banner.svg @@ -0,0 +1,10 @@ + + + + + + + + podcasters + podcasts · radio · TV + diff --git a/firetv/store/src/logo-512.png b/firetv/store/src/logo-512.png new file mode 100644 index 0000000..4ab72f4 Binary files /dev/null and b/firetv/store/src/logo-512.png differ