Android emulators
Run the app on several emulators at once, including one per supported Android version.
bun run emulators runs the whole stack and opens OpenBeacon on several Android emulators that move around Derby. You can run it in two ways:
- Route mode (the default) starts four copies of one Android Virtual Device (AVD), each following a different route. It's a quick way to see several group members moving on the map.
- Version mode starts one emulator per Android version. Use it to check a change on old and new Android at the same time, or to reproduce a bug on one specific version.
Before you start
You need Bun, Python 3, Java 17, Docker Compose and the Android SDK with the emulator and platform tools. In Android Studio's SDK Manager, install the Android NDK and accept the SDK licences. adb and emulator must be on your PATH.
- Route mode needs at least one AVD. Create one in Android Studio's Device Manager.
- Version mode creates its own AVDs, so it also needs Android SDK Command-line Tools (latest) and
ANDROID_HOMEset to your SDK.
Stop anything already using Metro's port 8081 or the emulator ports the run needs. Each emulator takes two ports from 5554 upwards, so four emulators use 5554–5560 and all ten versions use 5554–5572.
What it does
- Installs dependencies.
- Starts PostgreSQL and Redis in Docker, applies database migrations and starts the backend with hot reload. It skips this step if you point it at another backend.
- Builds the Android development APK, unless you pass
--apk. - Starts Metro and the emulators, installs the app on each and opens it. If the app closes during startup, it tries again, up to three times per emulator.
- Moves each emulator's GPS once a second until you press Ctrl+C.
On the first run it creates .env from .env.example with a generated auth secret. It then stops and lists the storage settings you still need to fill in: S3_ACCESS_KEY_ID, S3_ACCESS_KEY, S3_BUCKET_NAME, R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET and R2_PM_TILES_KEY. Pictures and maps need real storage credentials and a PMTiles object. It never overwrites an existing .env.
The four routes are:
| Route | Speed | Where |
|---|---|---|
| Stationary | 0 mph | Central Derby |
| Walking | 3 mph | A loop through the centre |
| Jogging | 6 mph | A loop in Darley Park |
| Car | 70 mph | The A6 south-east of Derby, reversing at each end |
Press Ctrl+C to stop the backend, Metro and the emulators. The database and cache keep running; stop them with docker compose stop db redis. Each emulator's log is saved to a temporary folder, which is printed at startup and again if something fails.
Run specific Android versions
bun run emulators:versionsThis starts one emulator for every supported Android version. To run only some, list their API levels:
bun run emulators:versions 24 36
bun run emulators:versions 31bun run emulators:versions is a shortcut for bun run emulators --android-versions, so the two lines above are the same as bun run emulators --android-versions 24 36 and bun run emulators --android-versions 31. Running a single version is the easiest way to reproduce a bug someone reported on their Android version.
The supported API levels are 24, 26, 28, 29, 30, 31, 33, 34, 35 and 36. The compatibility matrix maps them to Android versions. Any other number is rejected. Duplicates are ignored, and the emulators always start in order from oldest to newest.
For each version, the script:
- installs the Google APIs system image with
sdkmanagerif it's missing, usingarm64-v8aon ARM machines andx86_64on others, and reinstalls any image left half-installed; - creates an AVD called
OpenBeacon_API_<level>on a Pixel 6 profile with a 6 GB data partition and a hardware keyboard, and recreates it if its settings don't match; - gives the emulators routes in turn: the first version is stationary, the second walking, the third jogging, the fourth driving, and then the routes repeat.
AVDs go in ANDROID_AVD_HOME, or ~/.android/avd if that isn't set.
Options
| Option | Default | What it does |
|---|---|---|
--avd NAME | First AVD that emulator -list-avds prints | Route mode: the AVD to run four copies of. Can't be used with --android-versions. |
--android-versions [API ...] | Off. With no levels, all ten. | Version mode: one emulator per listed API level, creating any AVDs and system images that are missing. |
--memory-mb MIB | 6144 in route mode. In version mode, a default for each version (see the matrix). | RAM for each emulator, in MiB. Must be a positive whole number. |
--skip-memory-check | Off | Starts the emulators even if the machine doesn't seem to have enough free RAM. |
--api-url URL | http://10.0.2.2:3000 | The backend the app talks to. Any other value uses that backend and doesn't start a local one. It must be an http or https origin with no path, query or fragment. |
--external-backend | Off | Uses a backend you're already running at the default address, instead of starting one. |
--apk PATH | Builds a debug APK | Installs an existing Expo development APK and skips the Android build. |
-h, --help | Prints the options. |
You can combine version mode with the other options, for example:
bun run emulators:versions 24 30 36 --memory-mb 4096
bun run emulators:versions 34 --apk apps/mobile/android/app/build/outputs/apk/debug/app-debug.apk
bun run emulators --avd Medium_Phone --api-url https://api.example.com10.0.2.2 is how an Android emulator reaches your computer. The API URL is built into the development bundle, so restart the command when you switch backends. That backend's auth and network settings must allow the URL.
Memory
Before building anything, the script adds up the RAM each emulator needs plus 2 GiB of headroom. It then compares the total with the free memory reported by /proc/meminfo on Linux or vm_stat on macOS. If there isn't enough, it stops and suggests fewer emulators, a lower --memory-mb or --skip-memory-check. If it can't read free memory, it skips the check.
Route mode needs about 26 GiB by default: four emulators at 6 GiB plus headroom. Version mode with all ten versions needs about 56 GiB. Most machines need a shorter list of versions or a lower --memory-mb.
Android SDK and Java
The script uses ANDROID_HOME, or ANDROID_SDK_ROOT if that isn't set. If that SDK has no NDK or no accepted licence and ~/Android/Sdk has both, it uses ~/Android/Sdk instead and says so. On Linux, if /usr/lib/jvm/java-17-openjdk exists and your current Java isn't 17, it switches to Java 17 for the run.
If the Android build fails, check that the NDK is installed, the SDK licences are accepted, and ANDROID_HOME points to that SDK.
Android compatibility matrix
The app's Android build settings come from the Expo 55 and React Native 0.83 defaults. The app doesn't override them.
| Setting | Value | Meaning |
|---|---|---|
minSdk | 24 (Android 7.0) | The oldest version that can install the app |
targetSdk | 36 (Android 16) | The version whose behaviour rules the app follows. Google Play needs 36 for new apps and updates. |
compileSdk | 36 (Android 16) | The SDK the app is built against |
The minimum is a technical floor, not a promise that every phone at that version works. Raising targetSdk brings in newer Android rules even on older phones, so check the version changes below when you update Expo or React Native.
| Android | API | Emulator RAM | What changes for OpenBeacon | Emulator | Real phone |
|---|---|---|---|---|---|
| 7.0 | 24 | 3 GiB | The minimum. java.util.Base64 doesn't exist below API 26, so native code uses Base64Codec, which falls back to android.util.Base64. | Checked | Not yet tested |
| 8.0 | 26 | 3 GiB | First version with java.util.Base64. | Checked | Not yet tested |
| 9 | 28 | 4 GiB | Checked | Not yet tested | |
| 10 | 29 | 4 GiB | Background location becomes its own permission. The tracking service declares the location foreground service type. | Checked | Not yet tested |
| 11 | 30 | 4 GiB | Background location must be granted in Settings and requested after foreground location. | Checked | Not yet tested |
| 12 | 31 | 6 GiB | People can grant approximate location only, including in the background. Foreground services generally can't start from the background. | Checked | Not yet tested |
| 13 | 33 | 6 GiB | Notification permission. A foreground service still shows its notification in Task Manager if it's denied, and people can stop the app from there. | Checked | Not yet tested |
| 14 | 34 | 8 GiB | Stricter while-in-use checks when starting a location service from the background, and a foreground service permission is required. | Checked | Not yet tested |
| 15 | 35 | 8 GiB | The Robolectric SDK the offline queue tests run on, the newest that works with Java 17. | Checked | Not yet tested |
| 16 | 36 | 8 GiB | The target and compile SDK. | Checked | Not yet tested |
"Checked" means the app ran on that version's emulator with bun run emulators:versions. That's how the Android 7 location upload bug was found and fixed.
Versions not in the list:
- Android 7.1 (25), 8.1 (27) and 12L (32) are above the minimum, so the app installs on them, but they have no emulator in the script.
- Android 17 isn't in the script yet and hasn't been checked.
- Versions below 7.0 can't install the app.
Emulators don't replace real phones for battery, GPS, background behaviour or manufacturer power management.
Real-device test plan
No real phone has been signed off yet. This is the coverage being worked towards:
| Coverage | Device | What it checks | Status |
|---|---|---|---|
| Floor | Android 7.0 emulator, and a physical phone if one turns up | Install, launch, sign-in, map, capture and the encrypted queue at the minimum version | Not yet tested |
| Older phone | A phone launched around 2021, on Android 11 or 12 | Background permission, battery, overnight and offline behaviour | Not yet tested |
| Manufacturer differences | Samsung and one other manufacturer, including a budget phone, ideally Android 12–14 | Power management, permission recovery, performance | Not yet tested |
| Current | Android 15 and 16 phones | The most common versions, foreground service and notification behaviour | Not yet tested |
| Newest | Android 17 emulator or phone | Forward compatibility with target 36 | Not yet tested |
| Version changes | Android 10–14 emulators as needed | Background permission, approximate location, service starts and notifications | Not yet tested |
One phone can cover more than one row. For each run, record:
- the phone: manufacturer, model, launch year, Android version and API level, security patch and Google Play services version;
- the conditions: app build, network and battery mode;
- the run itself: tester, date, results and any bugs.
On every device, try these cases:
- allowing, denying and later granting location, background location and notifications;
- approximate instead of precise location;
- turning location services off;
- screen off, rebooting and force-stopping the app;
- manufacturer battery restrictions;
- long periods offline, then reconnecting.
Check that locations still arrive encrypted and up to date, and measure battery use and delay rather than assuming tracking never stops.