Start here
Introduction
What Holtdroid is, who it is for, and how a session looks from the user's side.
Это содержимое пока не доступно на вашем языке.
Holtdroid is a self-hosted web portal that launches Android devices on demand for testing APKs. It is built for teams whose builds land in a chat channel and whose apps lean on WebViews: QA engineers, web developers working on hybrid apps, support engineers reproducing a customer bug.
Why it exists
Section titled “Why it exists”I’m a frontend engineer, and I build Holtdroid for my own use first. Most users of the products I’ve worked on open them inside an app’s embedded WebView or in Chrome, and a fair share of them do it on old potato phones 🥔 with cursed combinations of Android and WebView versions. Their bugs only reproduce on exactly those combinations.
I’m also very lazy, and debugging over ADB through Chrome DevTools is flaky and awkward enough to drain whatever motivation was left. So instead of fighting it I wrote a tool that starts the WebView I need and then digs into what the page actually does: the network requests on the critical loading path, graphics glitches in images and SVG, how well Cloudflare caches the assets, and more.
Maybe I’m the only weirdo out there who rewrites SVG filters by hand and loses sleep over cache hit rates so that people on a bad connection get a page that loads fast and doesn’t cook their phone. Just in case I’m not, I made it public ^.^ If something should be fixed or added, I’d be glad to hear it: open an issue.
A session, end to end
Section titled “A session, end to end”-
Open the portal. The home page shows one card per app (package name) found in your configured sources (a folder or bucket that CI or a Slack sync job fills, plus your own uploads): its latest version, how many builds and versions there are and which sources they come from. Launch on a card starts the latest build; click the card to see every build of the app with its version, who posted it, its note and changelog. Builds of one version for different processors (x86 and ARM) share a row, and a launch takes the one the chosen device runs natively. Anyone signed in can give an app a description (what it is for, how to test it): it shows above the builds and, shortened, on the app’s card.
-
Pick a build, choose a VPN location if you need one, press Launch. The provider is Any by default: the first provider with a free device, in the order of
providersin the config. Pick a provider to choose one of its devices, busy ones included. A fresh Android device boots (about a minute). The ⚡ button next to it skips the dialog and launches with the provider and location you used last time (or the defaults). When every device is busy, or the one you picked, the launch waits in line and starts as soon as one frees up. -
The APK is installed as-is and started with WebView debugging enabled at runtime.
-
The session page shows the device screen on the left and Chrome DevTools on the right, with a picker for the inspected target (any WebView page, iframe or worker in any process). The portal’s header shows the app in place of the logo: click its name and icon to switch to another build; the time left, Extend and Stop sit on its right. Restarting, reinstalling and saving the app’s data are in the app card next to the phone. By default the screen’s side follows the screen: it grows or shrinks until the screen fills its height or the side reaches 70% of the page. A landscape screen splits the page top and bottom instead, the screen above the tools. Dragging the divider sizes it by hand; the button on the divider turns following back on.
The screen sits in a phone body with keys on its right edge, like a real phone: the volume rocker turns the volume up (upper half, marked +) or down (lower half, −), and the power key locks the device or turns its screen off, as the icon next to it shows. Hovering the rocker shows the device’s media volume with a slider and a tile that mutes Android itself; the rocker turns amber while the device is muted. Hovering the power key offers Wake and unlock, locking and turning the screen off right away, and picks what a click on the key does. The device cards sit in a column on the left of the screen panel, which scrolls (fading at the edge that has more) when they don’t fit, and the phone takes the rest; only one card’s panel shows at a time. The volume and power keys move with the device’s rotation, to the top or bottom edge in landscape. The network card shows the VPN location, the external IP and the network conditions; hovering it opens a panel styled like Android’s quick settings shade with Wi-Fi, mobile data and airplane mode tiles, the VPN locations and a link to the Network tab next to DevTools. On emulators with VPN locations that tab holds the emulated network conditions: slow or lossy networks and scenarios such as a subway ride, each with its description, and regions such as Bangladesh that add the round trips to CDN edges and to your servers there. The DNS tab beside it lists every name the device looks up and rewrites names to your own addresses, with a TTL limit and saved sets of rewrites (see DNS log and rewrites), and Routes shows where every connection goes: each server’s country and network, the location it left through, and far servers or connections that aren’t reused (see Routes). A small handle above the phone slides out a bar with the notifications, quick settings and a screenshot on hover, and one on the phone’s bottom edge slides out Android’s back, home and recent apps buttons; in landscape the navigation bar moves to the side, as on a phone. A curved handle on the phone’s top-right corner slides out rotate left and right buttons; rotation is in the display card too. Under the network card, the stream card picks the video codec (H.264, the lightest for the server, by default; H.265 or AV1 where the browser decodes them), quality (the device’s own up to 1080p, then 720p, 480p and 360p; a sharper screen is streamed at 1080p), frame rate (45 fps by default, up to the device’s own and never above 60) and bitrate, switches the device audio (it plays in your browser by default, from your first click on the page unless the browser allows autoplay for the portal; until then the card says “Click anywhere for sound”), shows the frames, bitrate and dropped frames the browser decodes right now, and switches the keyboard between text input and a virtual USB keyboard that types in any alphabet with the layout set on the device. The codec, the bitrate and a frame rate below 45 fps are remembered in the browser; quality and faster frame rates last for one session. The clipboard card turns sync on or off with a click (both ways by default; hover to pick one direction) and keeps the last 10 synced texts, shown with · for spaces and ↵ for line breaks, each with buttons to copy it or save it as a text file. The date and time card shows the device’s clock and is marked while the clock is shifted, not in real time or in a time zone picked by hand; hovering it opens real-time sync, shifting the clock back or forward by any amount, an exact moment entered in your own time zone (it unfolds in place), and the time zone, which follows the VPN location unless you pick another one for the session. The battery card shows the charge (65% and on battery at start) and opens battery saver, charging and the charge level with presets. The display card shows the brightness, the theme and the font size; hovering it opens a brightness slider and adaptive brightness (for this session only; the stream itself doesn’t get darker, the device and its apps see the setting), the dark theme (light, dark or matching the portal’s theme, which a device launched in that mode already has when the app first opens), Android’s system navigation on emulators (gestures by default, or the back, home and recent apps buttons), the font size with a preview and the display size, Android’s setting that scales buttons, icons and text by changing the density (the screen picked at launch keeps its resolution). The language card picks the system language and the keyboard layouts from a grid of flags: a click on a layout adds or removes it (up to 7, remembered in your browser and guessed from your keyboard where the browser allows). When SSH is on, the SSH card says whether a computer is connected to this device; hovering it lists the ADB clients and the ports your computers publish on the device, the
adbcommand for this device on each connected computer, or the command that connects a computer. A panel opened by hovering stays open once you click inside it, until you click elsewhere or press Esc. When the device vibrates, the page shows a notification and plays a buzz. The screenshot button is with the notifications and quick settings above the phone; recording has its own card (recordings).On devices that can fake it, the GPS location card follows the VPN location until you tap the map, drag the dot, paste coordinates or use the locate button on the map for your own position. Under the map, Route walks, runs or drives the device through stops you tap on the map, along roads (or in straight lines) and optionally in a loop, speeding up, slowing for turns and drifting around the chosen pace; it keeps going until you stop it, even with the tab closed. Apps see the position move but no speed or bearing.
An amber dot marks a card whose settings differ from the defaults, and its panel has a reset button.
-
Walk away. After the idle timeout (30 minutes by default) the device is deleted and the slot frees up. The home page shows every active session, its owner and how long it has left.
What it is not
Section titled “What it is not”- Not a device farm for automated test runs (yet). The contracts allow it, but the first goal is interactive debugging.
- Not a way to modify or re-sign APKs. Anything that needs repackaging is out of scope on purpose.
- Not a Play Store environment: sessions run redroid, a rooted AOSP userdebug build without Google services.
Requirements
Section titled “Requirements”- A Kubernetes cluster (1.31+) with x86_64 nodes with binder and a Mesa GPU for sessions, dedicated to them. Each session takes about 1 GB of memory idle and a core while its screen streams.
- Permission to run Pods with extra capabilities (Pod Security level
privileged) in Holtdroid’s namespace, and a runtime with NRI (containerd 2.0+) on the session nodes. - A forward-auth proxy in front of the ingress (Authelia, oauth2-proxy, …), unless you run it locally with authentication disabled.
- For builds from Slack: a Slack app with a bot token and a small sync job that copies them into a source.