← Hyper Browser

Hyper Browser — architecture

Android · Kotlin · Jetpack Compose · single activity, no DI framework, no repository layer. ~7,900 lines in 8 files: one UI file, one storage facade, one image decoder.

UI

Compose, all state hoisted into HyperBrowserApp() — no ViewModel; SharedPreferences is the only persistence

DirectoryPane ×2

Pane.LEFT / Pane.RIGHT, each with its own root, path stack, sort, selection and list state (BrowserPaneState, saved per pane).

CommandStrip

Full-height scrollable toolbar between the panes. TransferDirection drives copy/move only — delete, rename and gallery follow SelectionInfo, the pane that actually holds the selection.

ImageViewerScreen

GalleryStage = GRID_SMALL (4 col, 192px) → GRID_MEDIUM (2 col, 640px) → SINGLE. Pinch moves between stages; in SINGLE it zooms, with pan clamped to the overflow the zoom creates.

Dialogs

Every destructive or ambiguous action is a dialog: delete plans with recursive counts, bulk-folder transfer, folder picker, EXIF, "Set image as", layout & sort.

▼ ▼ ▼

Domain

plain functions over FileEntry — plan first, then execute, so the UI can show what is about to happen

planTransfer() → TransferRequest

Resolves direction, selection and destination into an explicit plan, classifies it (files-into-folder, folder-into-folder, bulk), and hands the UI a prompt to confirm before anything moves.

buildDeletePlan() → DeletePlan

Walks each target recursively and counts files/folders before the delete, so ">1 folder" can raise an itemized "are you sure".

UndoRecord / .HyperBrowserTrash

Delete moves into a hidden trash folder beside the original where the provider allows a move; copy/move record what they created. One step of undo. no undo cross-provider

▼ ▼ ▼

Storage facade

object Storage — the single branch point in the app: if (DriveUris.isDrive(uri)) … else …

Storage

entry · children · parent · mimeType · openInput · read. Everything above this line sees only FileEntry(uri, name, mimeType, size, …) and never knows which backend it came from.

SAF / local

content:// tree + document URIs via DocumentFile. Seekable, so images decode by region straight off disk. Virtual documents are exported to a concrete MIME type first.

DriveClient + DriveUris

Drive REST v3 behind synthetic gdrive://<fileId> URIs. Not seekable — a file must be downloaded whole before a pixel can be decoded, which is the root of every Drive slowness.

AppBackground / Wallpapers / ImageRotation

Side channels to the platform: background copied into app storage, center-cropped WallpaperManager.setBitmap, and in-place rotation written as an EXIF tag rather than a re-encode.

▼ ▼ ▼

Image pipeline

the part that earns its complexity: a 500 MB TIFF and a 60 MP RAW must open on a phone

ByteSource — memory-mapped random access

A local file is mmap'd (BufferSource), so a 500 MB TIFF is never on the heap. Drive has no equivalent: its bytes arrive as an ArraySource in RAM.

RawImage — what BitmapFactory can't read

TIFF and camera RAW (ARW, CR2, NEF, DNG, RAF). Parses the TIFF/IFD tree itself; every offset bounds-checked, every allocation capped, since these are untrusted bytes.

SensorCodec — the RAW inspector

Per-vendor sensor decoders (packed, ARW2, Nikon, lossless JPEG) + bilinear demosaic. The inspector's "compressed" toggle instead pulls the camera's largest embedded JPEG.

tap image→ overview decoded at 2× viewport→ zoom > fit→ visible rect only, region-decoded at the scale it's shown→ memory stays flat at any zoom
Caps: 16 MP drawable / 8192 px texture — past that Android's canvas refuses to draw. ThumbnailCache: LRU sized to ⅙ of max heap, keyed uri@px.

DriveDownload — the 4.0.1 addition

Because Drive can't seek, ≥200 MB files read in 256 KB chunks through readReporting(), publishing bytes-read on a StateFlow. The single view draws the cached thumbnail with a progress bar over it; Cancel flips a flag the read loop checks between chunks — cancelling the coroutine alone would leave the download running to the end.

Why the grid is still fast on the same file

Thumbnails sample aggressively (inSampleSize) and are cached, so even a 501 MB TIFF renders one with a short delay. Only the full-resolution single view pays the whole-file cost — which is why the fix is a progress bar, not a thumbnail cap.

Shape of the code

deliberately flat — the file sizes are the architecture diagram

MainActivity.kt · 4,563

All UI, all app state, the gallery and the decode orchestration.

RawImage.kt · 1,794

TIFF/RAW container parsing and tiled rendering.

SensorCodec.kt · 622

Vendor sensor formats and demosaic.

Storage.kt · 326

Backend facade and MIME resolution.

DriveClient.kt · 265

Drive auth, listing, read/export.

FileOperations · 114
ImageRotation · 141
AppBackground · 100

Delete plans & trash, EXIF rotation, wallpapers.