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 0000000..7bb2cf9 Binary files /dev/null and b/tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0/shield/Hobbs-Family-Photos-1.0.apk differ 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")