# Apps for Android and offline play Publish in Studio or through MCP on the web. The Android app is a player: discovery, a personal library, downloads and playback. It does not expose creator or administrator tools. HTML, JavaScript, Three.js/WebGL2 and supported WASM run on the device. Online requests use the phone's connection; a public IP or a listening server on the phone is unnecessary. ## Prepare an offline build Package `index.html`, scripts, styles, fonts, audio, models and other required files together. Prefer relative asset paths. A CDN, an external API, cloud AI, a remote database and internet multiplayer still need a network connection. A downloadable app is not automatically an offline app: test its actual menus and features in airplane mode. In **Studio → App details → Browser compatibility → Device access & offline play**, choose whether to allow downloads and whether the app requires an internet connection. The same options are available to agents with `player_options_get` and `player_options_update`. Read `policy_revision`, then pass it as `base_policy_revision`. Updates preserve other browser permissions. Downloads are verified against a signed, device/account-bound manifest and activated only after their files match the recorded sizes and hashes. A partial download is not playable. Installed builds stay in Android's private application storage; the previous build remains until its replacement activates. The player can remove downloads without deleting account saves. Private and Plus builds have shorter offline access periods than public builds. Administrators configure those periods. Revocation is checked when online; an offline device cannot learn about a new revocation immediately. Download access is not DRM. Do not bundle secrets or assume a downloaded browser build is impossible to copy. ## Save the same game on web and mobile Use the [save SDK](/docs/save-sdk.md) and [progress-store example](/docs/progress-example.md). The Android player runs the same progress SDK and uses the same account, project, slot and progress/browser channel identities. Keep `hackemist-save.json` versioned and provide migrations when changing your data format. ```js let state = { level: 1, score: 0 }; const saves = Hackemist.createProgressStore({ namespace: 'game', initial: () => { state = { level: 1, score: 0 }; render(); }, capture: () => ({ ...state }), restore: saved => { state = { ...saved }; render(); } }); await saves.ready; // After an actual change: saves.changed(); ``` Supported localStorage data is also captured on the device. Enable browser-data account saving in Studio to sync that channel between devices; otherwise its copy stays on the phone. In-memory state, IndexedDB, engine databases and server-owned state are not universally reconstructable. Use explicit save integration for them; the platform cannot infer an engine's complete state from its rendered pixels. Never put passwords, API credentials or other secrets into account saves. Offline changes commit locally first. Sync coalesces changes, signs ordered operations with the device key, retries the same operation safely and uses revision comparisons. Another device's save, a deleted slot, a reset or a changed save format must not be silently overwritten. The player gets a conflict choice. Checkpoints and local copies provide recovery within configured quotas; the server does not choose a winner by device clock. ## Request device access Declare only resources the app actually uses. Studio and `player_options_update` support `camera`, `microphone`, `location`, `bluetooth`, `wifi`, `clipboard_read`, `clipboard_write` and `sensors`. Site-wide administrator switches and membership capabilities still apply. Each app needs its own player consent; granting Android permission to Hackemist does not grant it to every hosted app. Use ordinary browser APIs for camera, microphone, location, touch, text fields, gamepads and rendering. Request media after a clear user action, explain the purpose in your interface, and stop media tracks when finished. Do not request every permission at startup. For a consistent web/native flow, call `Hackemist.device.requestPermission('camera')` (or the declared resource) before its browser API. Continue only when `allowed` is true. If `reload_required` is true, let the player reopen the app with its new permission policy before using that API. Android also intercepts ordinary camera/microphone/location requests through its per-app permission broker. For native Bluetooth, use the player bridge and explicit service UUIDs. The player chooses a device; app code cannot supply an arbitrary MAC address or control another app's connection. ```js const capabilities = await Hackemist.device.capabilities(); const connection = await Hackemist.device.connection(); // declared wifi/status access // In Android, after the player taps your Connect button: await Hackemist.device.requestBluetooth({ services: [SERVICE_UUID] }); const result = await Hackemist.device.readBluetooth({ service: SERVICE_UUID, characteristic: CHARACTERISTIC_UUID }); // result.value is base64url, at most 512 bytes await Hackemist.device.disconnectBluetooth(); ``` Bluetooth availability differs between browsers and Android devices. A Wi-Fi declaration allows connection status, not arbitrary network scanning, access-point changes or router control. File pickers use the operating system's explicit document chooser. Browser/device APIs that are unavailable must have a useful fallback; a declaration cannot add missing hardware or browser support. Both streamed and downloaded Android apps use a local HTTPS origin shaped like `https://p-<40 hex characters>.player.hackemist.invalid`, partitioned by account, app and save slot. No DNS record or listening server on the phone is needed. If your external backend checks CORS origins, accommodate the native origin as well as your approved web origins, and keep the service's own authentication/authorization. CORS is not proof of a player's identity. Hackemist account credentials remain in the native shell and are not credentials for your backend. ## Multiplayer inside your app Use `Hackemist.peers.connect()` for availability, invitations and WebRTC data channels, or `Hackemist.multiplayer.connect()` for existing shared rooms. Your own app can provide invitations, chat or collaboration without a separate Hackemist lobby. The player reuses the same SDK and authorization checks. Signaling, initial connection setup, TURN relaying where needed and remote services require connectivity. P2P rendering and app logic run on users' devices; the platform is not an authoritative anti-cheat server. ## Test the experience Test phone portrait/landscape, a tablet, native text entry, direct touch, loading without a network, restarting the process, switching save slots, reconnecting after two devices changed progress, permission denial/revocation and updating a build without losing saves. Test physical cameras, microphones, Bluetooth peripherals and GPU-heavy scenes on actual devices. A software-rendered emulator does not establish physical-device compatibility. Related: [Creator guide](/docs/creator-guide.md), [browser peers](/docs/browser-peers.md), [external connections](/docs/connections.md), [save SDK](/docs/save-sdk.md).