From 0a3ec27774a9e2ad93fb2bcaa37e3ca82b424dbc Mon Sep 17 00:00:00 2001 From: jeremy Date: Wed, 4 Nov 2026 19:28:12 -0600 Subject: [PATCH] feat(hobbs-organizer): Shield photo app (com.dayline.photos) + monitoring, links, .100 boot fix - shield/photos-source: launcher tile + DreamService screensaver showing /photo (locked-down WebView, default URL baked in, files/base_url override) - build-photos-apk.sh: containerized build on hf-pdocker-02 (JDK17, SDK 35); signing key kept off-repo on .100 + NAS - Hobbs-Family-Photos-1.0.apk installed on .186 (canary) and set as its screensaver - Prometheus probes for /api/health and /api/health/photos; Farm Manager links + group - CLAUDE.md: .100 left the stale Swarm and its NFS mount waits for network (11-03 outage) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3 --- CLAUDE.md | 4 +- .../hf-pdocker-02/hobbs-organizer/README.md | 6 + .../hobbs-organizer/build-photos-apk.sh | 64 +++++++++ .../shield/Hobbs-Family-Photos-1.0.apk | Bin 0 -> 15370 bytes .../shield/photos-source/.gitignore | 7 + .../shield/photos-source/BUILDING.md | 14 ++ .../shield/photos-source/app/build.gradle | 58 ++++++++ .../app/src/main/AndroidManifest.xml | 53 +++++++ .../com/dayline/photos/PhotoActivity.java | 45 ++++++ .../com/dayline/photos/PhotoDreamService.java | 37 +++++ .../com/dayline/photos/PhotoFrameView.java | 135 ++++++++++++++++++ .../app/src/main/res/drawable/ic_photos.xml | 12 ++ .../app/src/main/res/drawable/tv_banner.xml | 18 +++ .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/styles.xml | 10 ++ .../app/src/main/res/xml/dream_info.xml | 2 + .../main/res/xml/network_security_config.xml | 9 ++ .../shield/photos-source/build.gradle | 3 + .../shield/photos-source/gradle.properties | 3 + .../shield/photos-source/settings.gradle | 18 +++ 20 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 compose/hf-pdocker-02/hobbs-organizer/build-photos-apk.sh create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/Hobbs-Family-Photos-1.0.apk create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/.gitignore create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/BUILDING.md create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/build.gradle create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/AndroidManifest.xml create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoActivity.java create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoDreamService.java create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoFrameView.java create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/ic_photos.xml create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/tv_banner.xml create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/strings.xml create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/styles.xml create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/dream_info.xml create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/network_security_config.xml create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/build.gradle create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/gradle.properties create mode 100644 tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/settings.gradle diff --git a/CLAUDE.md b/CLAUDE.md index c298eca..12edce2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ hf-pdocker-02 (7.6GB RAM, i7-6700T, HDD) ├── [REMOVED] Actual Budget -- decommissioned 2026-07-13 (data kept at /mnt/docker-data/configs/actual-budget) ├── [COMPOSE] n8n + n8n-db (PostgreSQL) -- workflow automation ├── [COMPOSE] CouchDB (:5984) -- Obsidian LiveSync backend for the Second Brain vault. LAN-only. LOCAL disk (NOT NFS). -├── [COMPOSE] **Hobbs Family Organizer (:8340)** -- Shield TV family dashboard (calendar/weather/headlines/markets), deployed 2026-11-04, replaces the ChatGPT-Sites-hosted `dayline-tv` page. Next.js 16 / Node 22, image **built locally** (`hobbs-family-organizer:1.2.0` since 2026-11-04 evening; `1.0.0`/`1.1.0` kept for rollback; Watchtower disabled). **v1.2.0 adds the photo frame `/photo`** (slideshow of Google Photos shared albums, scraped from the share page because Google's Library API lost album access 2025-03-31, + admin uploads; synced every 6h into LOCAL `data/photos/`, ~0.5MB/photo; current album "TV screensaver", 46 photos). Photo health `GET /api/health/photos` = 503 when an album has not synced for 48h. **v1.1.0 adds `/admin`** (password, LAN-only, manages calendar links with masked display/test/history-revert); settings + scrypt password hash live in LOCAL `/var/lib/hobbs-organizer/data` (uid 10001, 700) — calendars now come from `data/settings.json`, the secrets file is only a first-start seed. Inside the container a small front proxy on :3000 stamps the real client IP (Next.js runs on 127.0.0.1:3001) so admin can refuse non-LAN and proxied requests. Password reset: `docker exec -u 10001 hobbs-family-organizer-organizer-1 node container/reset-admin.mjs`. Source = `tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/`; deploy dir `/mnt/docker-data/compose/hf-pdocker-02/hobbs-organizer` (bundle + `compose.override.yaml` + `.env`, repo copy `compose/hf-pdocker-02/hobbs-organizer/`). **Trusted-LAN mode** (empty token, per user). Secrets on LOCAL `/var/lib/hobbs-organizer/secrets/` (root 600 -- NFS root_squash would block start.mjs). Both Shields (.179, .186) run APK v1.4 pointed at it. See memory `hobbs-family-organizer`. +├── [COMPOSE] **Hobbs Family Organizer (:8340)** -- Shield TV family dashboard (calendar/weather/headlines/markets), deployed 2026-11-04, replaces the ChatGPT-Sites-hosted `dayline-tv` page. Next.js 16 / Node 22, image **built locally** (`hobbs-family-organizer:1.2.0` since 2026-11-04 evening; `1.0.0`/`1.1.0` kept for rollback; Watchtower disabled). **v1.2.0 adds the photo frame `/photo`** (slideshow of Google Photos shared albums, scraped from the share page because Google's Library API lost album access 2025-03-31, + admin uploads; synced every 6h into LOCAL `data/photos/`, ~0.5MB/photo; current album "TV screensaver", 46 photos). Photo health `GET /api/health/photos` = 503 when an album has not synced for 48h. **Shield photo app `com.dayline.photos` v1.0** (2026-11-04, separate APK: launcher tile + screensaver/DreamService, both load `/photo`; default URL baked in, override via `files/base_url` with `run-as`). Built on .100 by `compose/hf-pdocker-02/hobbs-organizer/build-photos-apk.sh` (JDK17 container, SDK cached in volume `hobbs-android-sdk`). **Signing key** `/var/lib/hobbs-organizer/apk-signing/` (root 600) + NAS copy `/mnt/docker-data/configs/hobbs-organizer/apk-signing/` -- losing it means uninstall/reinstall for any update. Installed on .186 only (canary), set as its screensaver (previous: `com.google.android.backdrop/.Backdrop`, saved in `~/hobbs-shield/screensaver-before-186.txt` on .100). **v1.1.0 adds `/admin`** (password, LAN-only, manages calendar links with masked display/test/history-revert); settings + scrypt password hash live in LOCAL `/var/lib/hobbs-organizer/data` (uid 10001, 700) — calendars now come from `data/settings.json`, the secrets file is only a first-start seed. Inside the container a small front proxy on :3000 stamps the real client IP (Next.js runs on 127.0.0.1:3001) so admin can refuse non-LAN and proxied requests. Password reset: `docker exec -u 10001 hobbs-family-organizer-organizer-1 node container/reset-admin.mjs`. Source = `tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/`; deploy dir `/mnt/docker-data/compose/hf-pdocker-02/hobbs-organizer` (bundle + `compose.override.yaml` + `.env`, repo copy `compose/hf-pdocker-02/hobbs-organizer/`). **Trusted-LAN mode** (empty token, per user). Secrets on LOCAL `/var/lib/hobbs-organizer/secrets/` (root 600 -- NFS root_squash would block start.mjs). Both Shields (.179, .186) run APK v1.4 pointed at it. See memory `hobbs-family-organizer`. ├── [COMPOSE] Farm Agent (:8889) ├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host, **v1.12.1** (was v1.7.0 from 2023 until 2026-10-26) ├── [COMPOSE] Telegraf -- metrics collection to InfluxDB @@ -100,6 +100,8 @@ All service data lives on the **Synology NAS** via NFS, mounted at `/mnt/docker- └── bart/{media,openvas,watchtower}/ ``` +**hf-pdocker-02 fixed 2026-11-04 after the 11-03 power event** (dockerd was dead 27h: stale Swarm membership made it fatal-exit when .192:2377 was unreachable, and `/mnt/docker-data` had not mounted): `docker swarm leave --force` done, and the `/mnt/docker-data` fstab line now has `nofail,x-systemd.after=network-online.target,x-systemd.mount-timeout=60s` (backup `/etc/fstab.bak-20261104-nfs-nofail`). Its other NAS lines (`/mnt/infrastructure`, `/mnt/NAS` cifs, `/data/media`, `/mnt/nas-media`) still lack it. **bart and .192 still report `Swarm=active`** -- .192 is the old manager so it boots fine; bart would fail the same way as .100 did if .192 is down at boot (not yet fixed, user approved only .100). + **NFS mount (all nodes):** `192.168.86.30:/volume1/Docker /mnt/docker-data nfs rw,soft,timeo=300,intr,_netdev,nfsvers=3,nofail,x-systemd.after=network-online.target,x-systemd.mount-timeout=60s 0 0` **Note (hf-pdocker-01):** Both NFS mounts hardened with `nofail,x-systemd.after=network-online.target,x-systemd.mount-timeout=60s` on 2026-05-20 to prevent boot-time race (network not up when NFS tried to mount, cascading ~10 container failures). diff --git a/compose/hf-pdocker-02/hobbs-organizer/README.md b/compose/hf-pdocker-02/hobbs-organizer/README.md index b9c9f14..482ef54 100644 --- a/compose/hf-pdocker-02/hobbs-organizer/README.md +++ b/compose/hf-pdocker-02/hobbs-organizer/README.md @@ -1,5 +1,11 @@ # Hobbs Family Organizer (Shield TV dashboard) — hf-pdocker-02 +**Shield photo app (2026-11-04): `com.dayline.photos` v1.0.** A launcher tile plus a screensaver, both showing `/photo`. +- Build: `sudo ./build-photos-apk.sh`. It writes `~/hobbs-shield/Hobbs-Family-Photos-.apk` on .100. +- Install: `adb install -r `. +- Set the screensaver: `adb shell settings put secure screensaver_components com.dayline.photos/.PhotoDreamService`. +- Signing key: `/var/lib/hobbs-organizer/apk-signing/` plus a NAS copy. Keep it; every update must use it. + **v1.2.0 (2026-11-04): photo frame at http://192.168.86.100:8340/photo** — Google Photos shared albums (admin → Photos tab: add share link, Check, Sync now; display interval/fit/shuffle/date) + uploads (resized in the browser). Cache: `/var/lib/hobbs-organizer/data/photos//` (+ `uploads/`), re-synced every 6h. Stale check: `/api/health/photos` (503 after 48h). diff --git a/compose/hf-pdocker-02/hobbs-organizer/build-photos-apk.sh b/compose/hf-pdocker-02/hobbs-organizer/build-photos-apk.sh new file mode 100644 index 0000000..bb7637a --- /dev/null +++ b/compose/hf-pdocker-02/hobbs-organizer/build-photos-apk.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Builds the Hobbs Family Photos Shield APK (com.dayline.photos) on hf-pdocker-02. +# Usage: sudo ./build-photos-apk.sh -> ~/hobbs-shield/Hobbs-Family-Photos-.apk +# The signing key is created on first run in /var/lib/hobbs-organizer/apk-signing (root 600) +# and copied to the NAS. NEVER delete it: updates must be signed with the same key. +set -euo pipefail +DEPLOY=/mnt/docker-data/compose/hf-pdocker-02/hobbs-organizer +SRC="$DEPLOY/shield/photos-source" +SIGN=/var/lib/hobbs-organizer/apk-signing +NAS_SIGN=/mnt/docker-data/configs/hobbs-organizer/apk-signing +WORK=/var/tmp/hfo-photos-build +OUT=/home/glyptic/hobbs-shield +IMAGE=eclipse-temurin:17-jdk-jammy + +[ "$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; } +install -d -m 700 "$SIGN" + +if [ ! -f "$SIGN/hobbs-photos.jks" ]; then + echo "== creating signing key" + PASS=$(openssl rand -base64 32 | tr -d '/+=' | cut -c1-32) + docker run --rm -v "$SIGN:/signing" "$IMAGE" keytool -genkeypair -keystore /signing/hobbs-photos.jks \ + -storetype PKCS12 -alias hobbs-photos -keyalg RSA -keysize 4096 -validity 20000 \ + -dname "CN=Hobbs Family Photos, O=Hobbs Farm" -storepass "$PASS" -keypass "$PASS" >/dev/null + printf 'storeFile=/signing/hobbs-photos.jks\nstorePassword=%s\nkeyAlias=hobbs-photos\nkeyPassword=%s\n' "$PASS" "$PASS" > "$SIGN/keystore.properties" + chmod 600 "$SIGN"/* +fi +# Off-box copy (NAS root_squash: write as glyptic, keep it private). +sudo -u glyptic install -d -m 700 "$NAS_SIGN" +for f in hobbs-photos.jks keystore.properties; do + cat "$SIGN/$f" | sudo -u glyptic sh -c "umask 077; cat > '$NAS_SIGN/$f'" +done + +rm -rf "$WORK"; mkdir -p "$WORK"; cp -a "$SRC/." "$WORK/" +cp "$SIGN/keystore.properties" "$WORK/keystore.properties" + +docker volume create hobbs-android-sdk >/dev/null +docker run --rm -v "$WORK:/src" -v hobbs-android-sdk:/opt/cache -v "$SIGN:/signing:ro" -w /src "$IMAGE" bash -euo pipefail -c ' + export ANDROID_HOME=/opt/cache/sdk GRADLE_USER_HOME=/opt/cache/gradle-home + command -v unzip >/dev/null || { apt-get update -qq && apt-get install -y -qq unzip curl >/dev/null; } + if [ ! -x $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager ]; then + echo "== downloading Android command-line tools" + curl -fsSL -o /tmp/clt.zip https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip + mkdir -p $ANDROID_HOME/cmdline-tools && unzip -q /tmp/clt.zip -d /tmp/clt && mv /tmp/clt/cmdline-tools $ANDROID_HOME/cmdline-tools/latest + fi + if [ ! -d $ANDROID_HOME/platforms/android-35 ] || [ ! -d $ANDROID_HOME/build-tools/35.0.0 ]; then + echo "== installing SDK platform 35 + build-tools 35.0.0" + (yes 2>/dev/null || true) | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses >/dev/null || true + $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platforms;android-35" "build-tools;35.0.0" >/dev/null + fi + if [ ! -x /opt/cache/gradle-8.9/bin/gradle ]; then + echo "== downloading Gradle 8.9" + curl -fsSL -o /tmp/gradle.zip https://services.gradle.org/distributions/gradle-8.9-bin.zip + unzip -q /tmp/gradle.zip -d /opt/cache + fi + echo "sdk.dir=$ANDROID_HOME" > local.properties + /opt/cache/gradle-8.9/bin/gradle --no-daemon -q assembleRelease + $ANDROID_HOME/build-tools/35.0.0/apksigner verify --print-certs app/build/outputs/apk/release/app-release.apk | grep -E "Signer #1 certificate (DN|SHA-256)" + $ANDROID_HOME/build-tools/35.0.0/aapt2 dump badging app/build/outputs/apk/release/app-release.apk | grep -E "^package:|application-debuggable|leanback" +' +VER=$(grep -oP 'versionName "\K[^"]+' "$SRC/app/build.gradle") +install -d -o glyptic -g glyptic "$OUT" +install -o glyptic -g glyptic -m 644 "$WORK/app/build/outputs/apk/release/app-release.apk" "$OUT/Hobbs-Family-Photos-$VER.apk" +sha256sum "$OUT/Hobbs-Family-Photos-$VER.apk" +rm -rf "$WORK" diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/Hobbs-Family-Photos-1.0.apk b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/Hobbs-Family-Photos-1.0.apk new file mode 100644 index 0000000000000000000000000000000000000000..7bb2cf910e577a1a28326ea83c887f04adadecc1 GIT binary patch literal 15370 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 literal 0 HcmV?d00001 diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/.gitignore b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/.gitignore new file mode 100644 index 0000000..6b3ffc0 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +app/build/ +local.properties +keystore.properties +*.jks +*.keystore diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/BUILDING.md b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/BUILDING.md new file mode 100644 index 0000000..22f0d3d --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/BUILDING.md @@ -0,0 +1,14 @@ +# Hobbs Family Photos (Shield app) + +Package `com.dayline.photos`. It has a launcher tile plus an Android TV screen saver (DreamService). Both show the +organizer's `/photo` page in a locked-down WebView. + +- The default URL `http://192.168.86.100:8340/photo` is baked in through `BuildConfig.DEFAULT_URL`. + - To override it, write `files/base_url` with `adb shell run-as com.dayline.photos`. The release build is debuggable for exactly this. +- The signing key is **not in git**. Lose it and every future update needs an uninstall and reinstall on each TV. + - It lives at `/var/lib/hobbs-organizer/apk-signing/` on hf-pdocker-02, with a copy on the NAS at `/mnt/docker-data/configs/hobbs-organizer/apk-signing/`. + - `keystore.properties` sets `storeFile`, `storePassword`, `keyAlias` and `keyPassword`. +- Build: `compose/hf-pdocker-02/hobbs-organizer/build-photos-apk.sh` on hf-pdocker-02. It runs a JDK 17 container with the + Android SDK (platform 35, build-tools 35.0.0) and Gradle 8.9, cached in the Docker volume `hobbs-android-sdk`. +- Set it as the screen saver from the TV (Settings → Device Preferences → Screen saver), or over ADB: + `settings put secure screensaver_components com.dayline.photos/.PhotoDreamService` diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/build.gradle b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/build.gradle new file mode 100644 index 0000000..4002b18 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "com.android.application" +} + +// Signing key lives outside the repo (see BUILDING.md). keystore.properties provides +// storeFile, storePassword, keyAlias, keyPassword. +def keystoreProps = new Properties() +def keystoreFile = rootProject.file("keystore.properties") +if (keystoreFile.exists()) keystoreFile.withInputStream { keystoreProps.load(it) } + +android { + namespace "com.dayline.photos" + compileSdk 35 + + defaultConfig { + applicationId "com.dayline.photos" + minSdk 26 + targetSdk 35 + versionCode 1 + versionName "1.0" + buildConfigField "String", "DEFAULT_URL", "\"http://192.168.86.100:8340/photo\"" + } + + buildFeatures { + buildConfig true + } + + signingConfigs { + release { + if (keystoreProps["storeFile"]) { + storeFile file(keystoreProps["storeFile"]) + storePassword keystoreProps["storePassword"] + keyAlias keystoreProps["keyAlias"] + keyPassword keystoreProps["keyPassword"] + } + } + } + + buildTypes { + release { + minifyEnabled false + // debuggable so `adb shell run-as com.dayline.photos` can write files/base_url, + // the same provisioning model as the dashboard app. + debuggable true + signingConfig signingConfigs.release + } + } + + lint { + abortOnError false + checkReleaseBuilds false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/AndroidManifest.xml b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f091fed --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoActivity.java b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoActivity.java new file mode 100644 index 0000000..761bd9c --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoActivity.java @@ -0,0 +1,45 @@ +package com.dayline.photos; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; + +/** Launcher tile: the photo frame, full screen, screen kept on. Back exits. */ +public final class PhotoActivity extends Activity { + private PhotoFrameView frame; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + frame = new PhotoFrameView(this); + setContentView(frame.view()); + hideSystemUi(); + frame.load(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) hideSystemUi(); + } + + @SuppressWarnings("deprecation") + private void hideSystemUi() { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + @Override + protected void onDestroy() { + if (frame != null) frame.destroy(); + frame = null; + super.onDestroy(); + } +} diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoDreamService.java b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoDreamService.java new file mode 100644 index 0000000..75c9c35 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoDreamService.java @@ -0,0 +1,37 @@ +package com.dayline.photos; + +import android.service.dreams.DreamService; + +/** Android TV screen saver showing the photo frame. Any remote button wakes the TV as usual. */ +public final class PhotoDreamService extends DreamService { + private PhotoFrameView frame; + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + setInteractive(false); + setFullscreen(true); + setScreenBright(true); + frame = new PhotoFrameView(this); + setContentView(frame.view()); + } + + @Override + public void onDreamingStarted() { + super.onDreamingStarted(); + if (frame != null) frame.load(); + } + + @Override + public void onDreamingStopped() { + if (frame != null) frame.view().stopLoading(); + super.onDreamingStopped(); + } + + @Override + public void onDetachedFromWindow() { + if (frame != null) frame.destroy(); + frame = null; + super.onDetachedFromWindow(); + } +} diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoFrameView.java b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoFrameView.java new file mode 100644 index 0000000..d65c13e --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/java/com/dayline/photos/PhotoFrameView.java @@ -0,0 +1,135 @@ +package com.dayline.photos; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Color; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Handler; +import android.os.Looper; +import android.webkit.CookieManager; +import android.webkit.SslErrorHandler; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * A locked-down full-screen WebView showing the organizer's /photo page. + * Shared by the launcher activity and the screen saver. + */ +final class PhotoFrameView { + private static final long RETRY_MS = 30_000; + private static final String BASE_URL_FILE = "base_url"; + + private final WebView webView; + private final Handler handler = new Handler(Looper.getMainLooper()); + private final String url; + private final String origin; + private boolean destroyed; + + @SuppressLint("SetJavaScriptEnabled") + PhotoFrameView(Context context) { + url = configuredUrl(context); + Uri parsed = Uri.parse(url); + origin = parsed.getScheme() + "://" + parsed.getAuthority(); + + WebView.setWebContentsDebuggingEnabled(false); + webView = new WebView(context); + webView.setBackgroundColor(Color.BLACK); + webView.setFocusable(false); + + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setAllowFileAccess(false); + settings.setAllowContentAccess(false); + settings.setJavaScriptCanOpenWindowsAutomatically(false); + settings.setSupportMultipleWindows(false); + settings.setSupportZoom(false); + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(true); + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); + settings.setMediaPlaybackRequiresUserGesture(true); + settings.setUserAgentString(settings.getUserAgentString() + " HobbsPhotoFrame/1.0"); + CookieManager.getInstance().setAcceptCookie(false); + + webView.setWebChromeClient(new WebChromeClient()); + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return !sameOrigin(request.getUrl()); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler sslHandler, SslError error) { + sslHandler.cancel(); + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + if (request.isForMainFrame()) { + showMessage("Waiting for the photo server…"); + scheduleRetry(); + } + } + }); + } + + WebView view() { + return webView; + } + + void load() { + if (!destroyed) webView.loadUrl(url); + } + + void destroy() { + destroyed = true; + handler.removeCallbacksAndMessages(null); + webView.stopLoading(); + webView.setWebViewClient(new WebViewClient()); + webView.destroy(); + } + + private boolean sameOrigin(Uri uri) { + return (uri.getScheme() + "://" + uri.getAuthority()).equalsIgnoreCase(origin); + } + + private void scheduleRetry() { + handler.removeCallbacksAndMessages(null); + handler.postDelayed(this::load, RETRY_MS); + } + + private void showMessage(String text) { + String html = "" + text + ""; + webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null); + } + + /** files/base_url (written over ADB run-as) overrides the built-in default. */ + private static String configuredUrl(Context context) { + File file = new File(context.getFilesDir(), BASE_URL_FILE); + if (file.isFile() && file.length() > 0 && file.length() < 2048) { + try (FileInputStream input = new FileInputStream(file)) { + byte[] data = new byte[(int) file.length()]; + int read = input.read(data); + String value = new String(data, 0, Math.max(read, 0), StandardCharsets.UTF_8).trim(); + Uri parsed = Uri.parse(value); + if (("http".equals(parsed.getScheme()) || "https".equals(parsed.getScheme())) && parsed.getHost() != null) { + return value; + } + } catch (IOException ignored) { + // fall back to the default + } + } + return BuildConfig.DEFAULT_URL; + } +} diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/ic_photos.xml b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/ic_photos.xml new file mode 100644 index 0000000..6b06a37 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/ic_photos.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/tv_banner.xml b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/tv_banner.xml new file mode 100644 index 0000000..a8034d8 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/drawable/tv_banner.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/strings.xml b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d69add4 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Hobbs Family Photos + diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/styles.xml b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..e668f1b --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/dream_info.xml b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/dream_info.xml new file mode 100644 index 0000000..7e82cbc --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/dream_info.xml @@ -0,0 +1,2 @@ + + diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/network_security_config.xml b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..8a76775 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/build.gradle b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/build.gradle new file mode 100644 index 0000000..48d2fa1 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/build.gradle @@ -0,0 +1,3 @@ +plugins { + id "com.android.application" version "8.7.3" apply false +} diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/gradle.properties b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/gradle.properties new file mode 100644 index 0000000..ed2d02a --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=false +android.nonTransitiveRClass=true diff --git a/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/settings.gradle b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/settings.gradle new file mode 100644 index 0000000..5e5a387 --- /dev/null +++ b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/photos-source/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "HobbsPhotoFrame" +include(":app")