diff --git a/firetv/.gitignore b/firetv/.gitignore new file mode 100644 index 00000000..d544a517 --- /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 00000000..5fd3a368 --- /dev/null +++ b/firetv/README.md @@ -0,0 +1,51 @@ +# pairux.com for Fire TV + +A small native Android app (`com.pairux.tv`) that puts https://pairux.com on a Fire TV +and makes it work with a remote. No dependencies beyond the Android SDK. + +- **WebView** on pairux.com. 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 pairux.com's CSP (`style-src 'self'`) + blocks an injected ` + + +
+

pairux.com 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 00000000..6177b661 --- /dev/null +++ b/firetv/app/src/main/assets/tv.js @@ -0,0 +1,213 @@ +/* pairux.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: pairux.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 00000000..bb97a9ac --- /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 00000000..f3ad9ebc --- /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 (PX_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 PX_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/pairux-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 00000000..cbb40a25 --- /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/pairux-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.pairux.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 "demo" +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.pairux.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 "demo" +key KEYCODE_ENTER +sleep 10 +shot 08-code-entered + +"$ADB" logcat -d | grep -E "AndroidRuntime|FATAL|com.pairux.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 00000000..ac73bc4a --- /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 00000000..a242f87c --- /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 (pairux-firetv--signing as +# PX_TV_KEYSTORE_B64 + PX_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 pairux \ + -keyalg RSA -keysize 4096 -validity 36500 -storepass "$PW" \ + -dname "CN=PairUX (pairux.com), O=Profullstack, C=US" >/dev/null 2>&1 +printf 'PX_TV_STORE_PASSWORD=%s\nPX_TV_KEY_ALIAS=pairux\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 00000000..0b54ad92 --- /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 = "pairux-tv" +include ":app" diff --git a/firetv/store/listing.md b/firetv/store/listing.md new file mode 100644 index 00000000..54bbfbf1 --- /dev/null +++ b/firetv/store/listing.md @@ -0,0 +1,52 @@ +# Store listing: PairUX (Fire TV) + +- **Title:** pairux +- **Package:** com.pairux.tv +- **Category:** Productivity +- **Price:** Free. No in-app purchases in the Fire TV app. No ads. +- **What it is on a TV:** a viewer. Open a PairUX room on the big screen and watch the + screen someone else is sharing. Sharing your own screen and taking remote control are + what the desktop app is for; a Fire TV has no screen to share and no pointer to lend. +- **Audience:** 13+ +- **Collects data:** an account (email, name) for people who sign in; a room can also be + joined anonymously by link. The microphone is used only if the viewer joins a call and + grants it, and a Fire TV has no camera. +- **Privacy policy URL:** https://pairux.com/privacy +- **Support:** anthony@profullstack.com, https://github.com/profullstack/pairux.com/issues + +## Short description + +Watch a PairUX screen share on your TV. Open a room by code or link, see what the other person is sharing on the big screen, and drive the whole thing with the remote. + +## Long description + +PairUX is collaborative screen sharing with simultaneous remote mouse and keyboard control, like Screenhero but open source. This is PairUX on a television: the viewer's half. + +- Join a room by code, or follow a room link, and watch the shared screen full size +- Made for the remote: the D-pad moves a clear focus ring, OK activates, Back walks back, and the Menu key asks for a room code from anywhere +- Rooms can be anonymous and end to end encrypted; the key lives in the link, never on a server +- Sign in to reach your own channels and scheduled sessions +- Free and open source (MIT): github.com/profullstack/pairux.com + +Sharing your own screen and remote control need a computer: install the PairUX desktop app from pairux.com. The Fire TV app is for watching. + +## Feature bullets + +Watch a PairUX screen share on the big screen +Join a room by code or by link +Anonymous end-to-end encrypted rooms +Made for the remote: focus ring, D-pad, Menu for a room code +Free, open source, no ads + +## Keywords + +screen sharing, pair programming, remote control, screenhero, collaboration, pairux + +## Art + +| 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/*.png | diff --git a/firetv/store/render.sh b/firetv/store/render.sh new file mode 100644 index 00000000..8681249b --- /dev/null +++ b/firetv/store/render.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Renders the launcher icons, TV banner and Amazon Appstore art. +# icon.svg and logo-light.svg are PairUX's own artwork, copied from apps/web/public. +# Runs inside alpine: docker run --rm -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 +S=store/src +mkdir -p "$O" "$R/mipmap-xhdpi" "$R/mipmap-xxhdpi" "$R/drawable-xhdpi" + +# Launcher + Appstore icons: the mark on its own, transparency kept. +rsvg-convert -w 96 -h 96 "$S/icon.svg" -o "$R/mipmap-xhdpi/ic_launcher.png" +rsvg-convert -w 144 -h 144 "$S/icon.svg" -o "$R/mipmap-xxhdpi/ic_launcher.png" +rsvg-convert -w 512 -h 512 "$S/icon.svg" -o "$O/icon-512.png" +rsvg-convert -w 114 -h 114 "$S/icon.svg" -o "$O/icon-114.png" + +# Fire TV banner and background: the real logo lockup on the PairUX night blue. +# Fire TV art may not carry transparency, so these are flattened. +rsvg-convert -w 820 -h 290 "$S/logo-light.svg" -o /tmp/lockup.png +magick -size 1280x720 xc:'#0f172a' /tmp/lockup.png -geometry +120+200 -composite \ + -font DejaVu-Sans -pointsize 44 -fill '#3b82f6' -annotate +126+560 'watch a shared screen on your TV' \ + -alpha remove -alpha off "$O/firetv-icon-1280x720.png" +rsvg-convert -w 320 -h 180 "$S/banner.svg" -o "$R/drawable-xhdpi/banner.png" +rsvg-convert -w 1920 -h 1080 "$S/background.svg" -o /tmp/bg.png +magick /tmp/bg.png -background '#0f172a' -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 00000000..f818a378 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 00000000..f818a378 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 00000000..f818a378 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 00000000..b431369e 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 00000000..bc8c22dc 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 00000000..0d2552b1 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 00000000..cba2367f Binary files /dev/null and b/firetv/store/shots/07-menu-search.png differ diff --git a/firetv/store/shots/08-code-entered.png b/firetv/store/shots/08-code-entered.png new file mode 100644 index 00000000..d088568e Binary files /dev/null and b/firetv/store/shots/08-code-entered.png differ diff --git a/firetv/store/src/background.svg b/firetv/store/src/background.svg new file mode 100644 index 00000000..9db4b697 --- /dev/null +++ b/firetv/store/src/background.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/firetv/store/src/banner.svg b/firetv/store/src/banner.svg new file mode 100644 index 00000000..da3af864 --- /dev/null +++ b/firetv/store/src/banner.svg @@ -0,0 +1,7 @@ + + + + + PairUX + watch a shared screen on your TV + \ No newline at end of file diff --git a/firetv/store/src/icon.svg b/firetv/store/src/icon.svg new file mode 100644 index 00000000..d5e8ef28 --- /dev/null +++ b/firetv/store/src/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firetv/store/src/logo-light.svg b/firetv/store/src/logo-light.svg new file mode 100644 index 00000000..cbad525d --- /dev/null +++ b/firetv/store/src/logo-light.svg @@ -0,0 +1 @@ + \ No newline at end of file