Android: previews, recording services, push, and billing
The Android code viewer and its shared TextMate grammars, a WebView profile per HTML document, PDF and cached file previews, the foreground services for dictation and Review Mode, push tokens, and Google Play Billing.
Checked against the source on 17 September 2026
On this page
TextMate grammars shared with the desktop
The Android code viewer uses the grammar inventory of the desktop’s Shiki highlighter. scripts/integrations/android_textmate/sync-android-textmate-grammars.mjs reads @shikijs/langs from the desktop’s Shiki install, resolves 24 advertised languages to 33 grammars including embedded ones, normalizes their capture maps for tm4e, and writes the grammars, a manifest with a SHA-256 per grammar and the source version, a language list, and the licenses into assets/textmate. The current source is @shikijs/langs 4.2.0. Run the script after the workspace Shiki version changes.
node scripts/integrations/android_textmate/sync-android-textmate-grammars.mjsHighlighting uses Sora’s GrammarRegistry with tm4e and native Oniguruma. A whole file opens in a read-only Sora CodeEditor, and a line-targeted snippet renders as a LazyColumn of tokenized lines. Highlighting runs on 2 shared workers with a cache of 24 documents. Diffs use the same viewer with the diff grammar, and patches are fetched with maxBytes 450,000 and cached per project and path.
Markdown uses the app’s own parser, and LaTeX renders natively. Mermaid diagrams render in a WebView with a bundled mermaid.min.js and network loads blocked, and the PlanToCodeDiagram JavaScript interface the page reports through is removed again when the view is released.
HTML documents: one profile and one origin per preview
- Preview originhttps://ptc-<preview-id>.invalid
A fresh UUID under the reserved .invalid domain, which never resolves, with its own WebView profile ptc-html-<preview-id>. Closing the preview wipes that profile’s storage, cookies, and geolocation permissions and closes the desktop grant.
- InterceptorshouldInterceptRequest
Streams each GET to the preview origin from the desktop, whichever frame sends it, unless the path has .., a backslash, a colon, or NUL. Any other request to a .invalid host or an unknown scheme gets 403, and https, data, and blob requests go on to WebView.
- Desktop grantfiles.openHtmlPreview
Registers the preview ID with a root folder: the project when it contains the document, otherwise the document’s folder. The desktop resolves every read inside that root, so a path that leaves it, also through a symlink, gets 403.
- LinksshouldOverrideUrlLoading
A tap on an http or https link goes to Android while the page stays put. Links to the preview origin load inside it, and every other navigation is dropped.
The app requires the MULTI_PROFILE WebView feature and asks for a WebView update when it is missing. Profiles left behind by an earlier process are deleted before the first new one, and closing a preview destroys its WebView.
WebView reads each intercepted resource through a blocking InputStream on its resource thread, which holds only the chunk being read. The desktop resolves every path again inside the grant, so a symlink that leads outside it also ends in 403.
JavaScript and DOM storage are on. File and content access are off, mixed content is never allowed, the app disallows cleartext traffic, the cache is disabled, Safe Browsing is on, and an SSL error cancels the load. No JavaScript interface is added. When the renderer process dies, the controller releases the WebView and offers a reload.
PDFs, cached previews, and media streams
PDF pages render with PdfRenderer on the IO dispatcher, each page scaled by the smaller of 2 and 2,560 divided by its long edge and drawn in ARGB_8888 on white. A PDF link inside the website preview is caught by the WebView’s download listener and fetched with OkHttp under a 60-second call timeout, with cookies read from the WebView’s CookieManager for every redirect hop. The browser session keeps the downloaded file and its scroll position across back and forward navigation.
Files open through CachedPreviewReader. It fetches the preview metadata with SHA-256, size, and content version, downloads in validated chunks, aborts when the version changes mid-download, and verifies the digest before use. PreviewDiskCache is keyed by account, desktop, project, path, and representation, keeps up to 512 MiB, and drops entries unused for 3 days.
- Fingerprintfiles.getPreviewMetadata
On every open the desktop hashes the whole file and returns only its SHA-256, size, MIME type, and content version. A cached copy counts as a match only when the phone’s own hash of it agrees.
- Downloadfiles.readBinary
Chunks of at most 4 MiB arrive in order, and the download fails when the content version changes. The finished file must match the SHA-256 before it replaces the cached copy.
- Cache512 MiB · 3 days
App-private, per account, desktop, project, path, and rendition. Entries unused for 3 days go first, then the least recently used above 512 MiB, never the newest or one that’s open.
Audio and video play in ExoPlayer through a custom data source that reads ranges of 512 KiB into a 128 MiB cache. A media stream that is playing keeps the relay open after the Activity stops.
Two recorder services
- Recorder serviceReviewScreenRecorderService
Starts after a one-time consent, RECORD_AUDIO, and the system capture prompt, and keeps recording while you use other apps. It writes H.264 at 5 Mbps and 30 fps and AAC at 128 kbps, at most 1,280 pixels on the long edge.
- Hand-offsendOrderedBroadcast
The result starts as RESULT_CANCELED, so the service deletes the file unless the Workspace tab answers RESULT_OK while it still shows the recording’s session and project. Once the upload has started, leaving the tab no longer stops it.
- Size limit100 MiB
Checked before upload, for recordings and for videos picked from the device. At the target bitrates, a recording reaches it after about 2 min 44 s.
- Analysis/api/llm/video/analyze
The server writes the upload to a temporary file, sends it to Gemini, inline below 19 MiB and through Gemini’s File API from 19 MiB, and deletes the temporary file before it answers. The analysis goes into the chat draft.
The manifest declares three foreground service types: mediaProjection and microphone for the Review Mode recorder, microphone for dictation, and mediaPlayback for media previews. Microphone hardware is marked optional, so Google Play does not hide the app from devices without one.
Dictation records with MediaRecorder as AAC at 128 kbps and 44.1 kHz into an .m4a file and updates the level meter every 50 milliseconds. The recording is posted as multipart to /api/audio/transcriptions with the model openai/gpt-4o-transcribe, and the desktop contributes the transcription prompt through system.voiceTranscriptionContext when it answers within 750 milliseconds.
The Review Mode recorder service calls startForeground before it obtains the MediaProjection, with the mediaProjection and microphone types on Android 11 and later, mediaProjection alone on Android 10, and no type on older versions. A VirtualDisplay scaled to at most 1,280 pixels on the long edge feeds a single MediaRecorder that takes the screen surface and the microphone and writes MPEG-4.
The MP4 is uploaded as multipart to /api/llm/video/analyze. Recordings and videos picked from the device are both refused over 100 MiB before upload. Leaving the Workspace tab, or switching to another session or project, while recording cancels the recording.
Push tokens and the desktop_online hint
When Firebase issues a new token, the messaging service stores it and takes a claim from the process-wide push token coordinator. Syncing returns NoRegion when no region is stored, so a token never goes to a guessed backend, and PUT api/devices/push-token runs only while the claim’s owner is still current.
A desktop_online push is never shown. It writes a durable hint with the desktop ID, the relay instance, the connection generation, and the presence revision, valid for 5 minutes, and sends a broadcast inside the app. When the Activity is visible, the network is up, and the desktop is the selected one, recovery starts at once and the hint is acknowledged. Otherwise the hint waits for the next onStart.
Google Play Billing and the connected preview
PlanToCode Pro on Android is one Google Play product, com.plantocode.pro, with weekly, monthly, and yearly base plans, bought through Billing 9 with pending purchases and automatic service reconnection. A purchase is verified with POST api/billing/google-play/subscription/verify and acknowledged only after the server confirms an active subscription. The entitlement is cached in memory for its owner with a deadline on elapsedRealtime, so changing the clock has no effect and a relaunch verifies again.
- Probesystem.ping
Sent through the relay before a desktop request while there’s no subscription and no active preview. If the relay answers that the desktop is offline, the request fails with that answer and nothing starts.
- Startapi/billing/workspace-preview
The PUT goes out only after an accepted ping, and only if the account and the selected desktop haven’t changed. The server starts one 10-day preview per account and answers a repeat with the same dates.
- DeadlineelapsedRealtime
The phone subtracts the time the PUT took from remainingMilliseconds and counts down on the monotonic clock, so changing the date can’t extend access. After the deadline a request still pings the desktop and is then refused.
Without a subscription, each account gets one 10-day connected preview that the regional server starts and stores. Opening the app only reads it with GET api/billing/workspace-preview. Responses must carry exactly the keys for their status, and a 401 is treated as rejected authentication, which signs the app out.
Gates before a Google Play release
bundleRelease and assembleRelease depend on validate16KbNativeLibraries, which parses the ELF program headers of every merged native library for arm64-v8a and x86_64 and fails when a PT_LOAD segment is aligned below 16,384 bytes. validateGooglePlayReleaseInputs fails the build without signing inputs, so an unsigned bundle cannot go out by mistake.
Pull requests run testDebugUnitTest, lintDebug, and compileDebugAndroidTestKotlin. Instrumented tests, among them the parity suites for live timeline growth, continuous PDF reading, preview interaction, and dictation controls, run on the pixel2api35 managed device.