feat(hobbs-organizer): v1.3.0 photo gallery + Shield photo app 1.1, slideshow 15s

- /photo/gallery: D-pad grid, OK opens full screen, arrows browse, Back returns
- 400px gallery tiles cached per photo (sync + uploads)
- com.dayline.photos 1.1: launcher tile opens the gallery, takes remote input,
  Back steps through WebView history; screensaver unchanged
- slideshow interval 20s -> 15s (live settings)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
This commit is contained in:
2026-11-04 21:19:24 -06:00
parent 5cfd2de432
commit 00e0cc42cf
16 changed files with 382 additions and 33 deletions
@@ -16,8 +16,8 @@ android {
applicationId "com.dayline.photos"
minSdk 26
targetSdk 35
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.1"
buildConfigField "String", "DEFAULT_URL", "\"http://192.168.86.100:8340/photo\""
}
@@ -5,7 +5,7 @@ import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
/** Launcher tile: the photo frame, full screen, screen kept on. Back exits. */
/** Launcher tile: the photo gallery (remote-driven grid), full screen, screen kept on. Back steps back, then exits. */
public final class PhotoActivity extends Activity {
private PhotoFrameView frame;
@@ -13,9 +13,10 @@ public final class PhotoActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
frame = new PhotoFrameView(this);
frame = new PhotoFrameView(this, "/gallery", true);
setContentView(frame.view());
hideSystemUi();
frame.view().requestFocus();
frame.load();
}
@@ -25,6 +26,13 @@ public final class PhotoActivity extends Activity {
if (hasFocus) hideSystemUi();
}
@Override
@SuppressWarnings("deprecation")
public void onBackPressed() {
if (frame != null && frame.goBack()) return;
super.onBackPressed();
}
@SuppressWarnings("deprecation")
private void hideSystemUi() {
getWindow().getDecorView().setSystemUiVisibility(
@@ -12,7 +12,7 @@ public final class PhotoDreamService extends DreamService {
setInteractive(false);
setFullscreen(true);
setScreenBright(true);
frame = new PhotoFrameView(this);
frame = new PhotoFrameView(this, "", false);
setContentView(frame.view());
}
@@ -22,8 +22,9 @@ 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.
* A locked-down full-screen WebView showing the organizer's photo pages.
* Shared by the launcher activity (interactive gallery, /photo/gallery) and the screen saver
* (non-interactive slideshow, /photo).
*/
final class PhotoFrameView {
private static final long RETRY_MS = 30_000;
@@ -34,17 +35,26 @@ final class PhotoFrameView {
private final String url;
private final String origin;
private boolean destroyed;
/** Set by load(): the next finished page becomes the bottom of the Back stack. */
private boolean clearHistoryWhenLoaded;
/**
* @param subPath appended to the configured photo URL (e.g. "/gallery"), or "" for the slideshow
* @param interactive true to take remote (D-pad) input; the screen saver passes false
*/
@SuppressLint("SetJavaScriptEnabled")
PhotoFrameView(Context context) {
url = configuredUrl(context);
PhotoFrameView(Context context, String subPath, boolean interactive) {
String base = configuredUrl(context);
while (base.endsWith("/")) base = base.substring(0, base.length() - 1);
url = base + subPath;
Uri parsed = Uri.parse(url);
origin = parsed.getScheme() + "://" + parsed.getAuthority();
WebView.setWebContentsDebuggingEnabled(false);
webView = new WebView(context);
webView.setBackgroundColor(Color.BLACK);
webView.setFocusable(false);
webView.setFocusable(interactive);
webView.setFocusableInTouchMode(interactive);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
@@ -58,7 +68,7 @@ final class PhotoFrameView {
settings.setLoadWithOverviewMode(true);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
settings.setMediaPlaybackRequiresUserGesture(true);
settings.setUserAgentString(settings.getUserAgentString() + " HobbsPhotoFrame/1.0");
settings.setUserAgentString(settings.getUserAgentString() + " HobbsPhotoFrame/" + BuildConfig.VERSION_NAME);
CookieManager.getInstance().setAcceptCookie(false);
webView.setWebChromeClient(new WebChromeClient());
@@ -68,6 +78,15 @@ final class PhotoFrameView {
return !sameOrigin(request.getUrl());
}
@Override
public void onPageFinished(WebView view, String finishedUrl) {
// Drop the "waiting for server" page (and anything before it) from the Back stack.
if (clearHistoryWhenLoaded && finishedUrl != null && finishedUrl.startsWith(origin)) {
clearHistoryWhenLoaded = false;
view.clearHistory();
}
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler sslHandler, SslError error) {
sslHandler.cancel();
@@ -88,7 +107,16 @@ final class PhotoFrameView {
}
void load() {
if (!destroyed) webView.loadUrl(url);
if (destroyed) return;
clearHistoryWhenLoaded = true;
webView.loadUrl(url);
}
/** Back key: step back inside the pages (e.g. close a full-screen photo). False = nothing left, exit. */
boolean goBack() {
if (destroyed || !webView.canGoBack()) return false;
webView.goBack();
return true;
}
void destroy() {