From fcba1106480cc48a1809e6dddd6d18cf9b36fb26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 8 Nov 2026 20:15:11 +0100 Subject: [PATCH] Android: the app's files and storage in its files folder, Back as Escape or exit Restores the Android work backed out in fe82608, now tested on a device (Pixel 4a, with engine PR #182): - webRoot() looks in $BLOOM_ASSET_PATH/assets// first: the Activity extracts the APK's assets to its files folder and says where. - localStorage is saved in // on Android. - Back follows tv.menuExits(): it leaves the app where Menu would leave an Apple TV app, else it is Escape (setMenuButtonExits, each frame). - frame.ts exports ANDROID. --- src/config.ts | 13 ++++++++++--- src/frame.ts | 4 +++- src/input.ts | 5 ++++- src/storage.ts | 7 +++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/config.ts b/src/config.ts index 97b9613..2f4c22c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -34,7 +34,10 @@ export interface HookElement { export interface TvHooks { /** The element whose buttons get the focus (a menu or a dialog), or null for none. */ overlay?(): HookElement | null; - /** Whether Menu leaves the app now (Apple wants it to on the root screen). Default: false. */ + /** + * Whether Menu leaves the app now (Apple wants it to on the root screen). Default: false. + * Android's Back button follows it too: it leaves the app when this is true, else it is Escape. + */ menuExits?(): boolean; /** * Called when no overlay is open: a direction (dx, dy each -1, 0, 1), and whether Select was @@ -122,14 +125,18 @@ let root: string | null = null; /** * The folder index.html is in, ending in "/": `assets//` in an app bundle (next to - * the executable on iOS and tvOS, Contents/Resources on macOS), else the configured root. + * the executable on iOS and tvOS, Contents/Resources on macOS, in $BLOOM_ASSET_PATH on Android, + * where the Activity extracts the APK's assets to its files folder), else the configured root. */ export function webRoot(): string { if (root !== null) return root; const exe = process.execPath ?? process.argv[0] ?? ""; const dir = exe.slice(0, exe.lastIndexOf("/") + 1); const bundle = settings.bundle; - for (const bundled of [dir + "assets/" + bundle + "/", dir + "../Resources/assets/" + bundle + "/"]) { + const places = [dir + "assets/" + bundle + "/", dir + "../Resources/assets/" + bundle + "/"]; + const extracted = process.env["BLOOM_ASSET_PATH"]; + if (extracted !== undefined && extracted !== "") places.unshift(extracted + "/assets/" + bundle + "/"); + for (const bundled of places) { if (existsSync(bundled + "index.html")) { root = bundled; return root; } } let r = settings.root; diff --git a/src/frame.ts b/src/frame.ts index 758356e..8286345 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -8,12 +8,14 @@ * requestAnimationFrame callbacks, then the page (onAfterFrame). */ import { initWindow, windowShouldClose, closeWindow, beginDrawing, endDrawing, clearBackground, setTargetFPS, getScreenWidth, getScreenHeight } from "@bloomengine/engine"; -import { QualityPreset, setQualityPreset, setTaaEnabled, setRenderScale, setAutoResolution, isMobile, isTV, getDisplayScale, getSafeAreaInset, SafeAreaEdge, setProfilerEnabled } from "@bloomengine/engine/core"; +import { QualityPreset, setQualityPreset, setTaaEnabled, setRenderScale, setAutoResolution, isMobile, isTV, getPlatform, Platform, getDisplayScale, getSafeAreaInset, SafeAreaEdge, setProfilerEnabled } from "@bloomengine/engine/core"; /** iPhone, iPad (and Android): touch input, no window to close, the app bundle for files. */ export const MOBILE = isMobile(); /** Apple TV: the Siri Remote (no pointer), seen from across the room. */ export const TV = isTV(); +/** Android: a phone or tablet like MOBILE, with a Back button and its files in the app's files folder. */ +export const ANDROID = getPlatform() === Platform.ANDROID; let callback: ((time: number) => void) | null = null; let rafQueue: { id: number; fn: (time: number) => void }[] = []; diff --git a/src/input.ts b/src/input.ts index e6125bd..64d275e 100644 --- a/src/input.ts +++ b/src/input.ts @@ -35,7 +35,7 @@ import { } from "./dom/index.ts"; import { config } from "./config.ts"; import { computedValue, document, fitView, mediaChanged, viewCanvas, windowTarget } from "./browser.ts"; -import { MOBILE, TV, cssSize, onBeforeFrame, pixelRatio, quit, screenSize } from "./frame.ts"; +import { ANDROID, MOBILE, TV, cssSize, onBeforeFrame, pixelRatio, quit, screenSize } from "./frame.ts"; // ---------------------------------------------------------------- hit testing @@ -460,6 +460,9 @@ onBeforeFrame(() => { } checkObservers(document._viewport, pixelRatio()); if (TV) { remote(); return; } + // Android's Back button is Escape, except where Menu would leave an Apple TV app (the native + // layer then hands Back to the system) + if (ANDROID) setMenuButtonExits(config().tv.menuExits?.() ?? false); // macOS: Bloom's window has no menu bar, so the standard Cmd-Q / Cmd-W close it here if (!MOBILE && (isKeyDown(Key.LEFT_SUPER) || isKeyDown(Key.RIGHT_SUPER)) && (isKeyPressed(Key.Q) || isKeyPressed(Key.W))) quit(); diff --git a/src/storage.ts b/src/storage.ts index 9fa76d7..b222324 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -8,11 +8,12 @@ * The file is $BLOOM_STORAGE, else config's storageFile, else * `` in the app's folder: ~/.config// (or * $XDG_CONFIG_HOME//) on a desktop and Apple TV, Library/Application - * Support// in an iOS app's container. + * Support// in an iOS app's container, / in an Android app's files + * folder ($BLOOM_ASSET_PATH). */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { config } from "./config.ts"; -import { MOBILE, frameCount, onAfterFrame, onQuit } from "./frame.ts"; +import { ANDROID, MOBILE, frameCount, onAfterFrame, onQuit } from "./frame.ts"; /** `~/...` -> $HOME/... (on iOS HOME is the app's container). */ export function expandHome(path: string): string { @@ -25,6 +26,8 @@ export function storagePath(): string { if (own !== undefined && own !== "") return expandHome(own); const c = config(); if (c.storageFile !== "") return expandHome(c.storageFile); + const files = env["BLOOM_ASSET_PATH"]; + if (ANDROID && files !== undefined && files !== "") return files + "/" + c.app + "/" + c.storageName; if (MOBILE) return (env["HOME"] ?? ".") + "/Library/Application Support/" + c.app + "/" + c.storageName; const xdg = env["XDG_CONFIG_HOME"]; return (xdg !== undefined && xdg !== "" ? xdg : (env["HOME"] ?? ".") + "/.config") + "/" + c.app + "/" + c.storageName;