Android Setup
This page consolidates the Android build notes that were previously spread across repo notes.
App locations
- Mobile POS app:
apps/mobile - POS terminal app:
apps/pos-terminal
Common commands
Run these from the monorepo root:
yarn mobile:start
yarn mobile:android
yarn mobile:apk
yarn mobile:aab
yarn terminal:start
yarn terminal:android
All workspace Android run, APK, AAB, and clean scripts apply the shared native
dependency compatibility patches before invoking React Native or Gradle. This
also repairs dependencies restored from cache without running postinstall.
For direct ./gradlew commands, first run
node scripts/patch-react-native-dependencies.js from the repository root.
Mobile uses Metro port 8081; Terminal start/run scripts default to 8082.
The physical-device shortcuts yarn mobile:android:8081 and
yarn terminal:android:8082 target 0074490208420196 and A65C009P00400034;
the target must be connected and authorized.
Terminal desktop emulator (1920×1080)
yarn terminal:emu # Open the emulator only
yarn terminal:android:8082:emu # Open it, wait for boot, build/install Terminal
These commands use the existing Large_Desktop AVD (1920×1080, landscape,
160 dpi). They find its actual emulator serial, so they also work when another
emulator occupies emulator-5554 and when physical devices are connected.
The helper wakes/unlocks the emulator and sets its screen timeout to 30 minutes.
The emulator remains open after the command finishes. Emulator launch logs are
written to bsr-terminal-emulator.log in the system temporary directory.
New emulator processes use DNS servers 1.1.1.1,8.8.8.8 so switching the host's
Wi-Fi does not leave the emulator forwarding DNS to the previous router. Override
them with POS_TERMINAL_DNS_SERVERS if the network requires different DNS servers.
Close the emulator and rerun the command to apply DNS changes; app data is retained.
If API calls fail with Network Error, check that Android is connected to
AndroidWifi and can resolve the API and Firestore hosts.
If the AVD is missing on another machine, create an AVD named Large_Desktop
in Android Studio Device Manager with a 1920×1080 desktop hardware profile,
160 dpi and a system image matching the host CPU architecture. To use another
existing AVD, set POS_TERMINAL_AVD, for example:
POS_TERMINAL_AVD=My_Desktop yarn terminal:android:8082:emu
The helper builds only the running emulator's CPU architecture to reduce build
time. Extra React Native options are passed through, for example --no-packager.
It selects the device and Metro port 8082. On macOS it uses Android
Studio's bundled JDK if JAVA_HOME is unset and java is unavailable.
When another project uses ports 8081/8082
The default ports stay unchanged. Use these alternative shortcuts when running alongside another React Native project, such as SSK Smart City. Run each Metro start command and its Android command in separate terminals:
# Mobile: Metro and Android must both use 8091
yarn mobile:start:8091
yarn mobile:android:8091
# POS Terminal: Metro and Android must both use 8092
yarn terminal:start:8092
yarn terminal:android:8092
# Or, for emulator-5554:
yarn terminal:android:8092:emu
These Android shortcuts target the same devices as their 8081/8082
counterparts. For another device, use
ANDROID_SERIAL=<serial> yarn mobile:android --device <serial> --port 8091
or ANDROID_SERIAL=<serial> yarn terminal:android --device <serial> --port 8092.
The Android run command configures the matching ADB reverse forwarding.
Check the Metro project before reusing an occupied port:
lsof -nP -iTCP:8081 -iTCP:8082 -sTCP:LISTEN
curl -i http://127.0.0.1:8081/status
curl -g -i 'http://[::1]:8081/status'
The X-React-Native-Project-Root response header must identify the intended app.
IPv4 and IPv6 can reach different Metro processes on the same port, so a
packager-status:running response alone does not establish which project serves
the device. Choose the alternative ports when either address belongs to another
project.
Run Mobile and Terminal on two Android devices
When two Android devices are connected at the same time, target each command at
the intended device with --device and ANDROID_SERIAL.
List the connected device serials first:
adb devices
Example:
List of devices attached
R58M123456A device
ZY22ABCDE9 device
Run Mobile POS on one device and POS Terminal on the other from separate terminals:
ANDROID_SERIAL=R58M123456A yarn mobile:android --device R58M123456A
ANDROID_SERIAL=ZY22ABCDE9 yarn terminal:android --device ZY22ABCDE9
The equivalent workspace commands are:
yarn workspace @bsr/mobile android --device R58M123456A
yarn workspace @bsr/pos-terminal android --device ZY22ABCDE9
--device also scopes React Native's launch and port-forwarding steps; do not
rely on ANDROID_SERIAL alone with multiple devices. The scripts keep Mobile
on 8081 and Terminal on 8082. If port forwarding needs to be restored manually:
adb -s R58M123456A reverse tcp:8081 tcp:8081
adb -s ZY22ABCDE9 reverse tcp:8082 tcp:8082
Run these from inside the mobile app when direct Gradle access is needed:
cd apps/mobile/android
./gradlew clean assembleRelease
./gradlew clean bundleRelease
Install an already-built Mobile release APK from the repository root, targeting
one device explicitly (replace the example serial with adb devices output):
adb -s R58M123456A install -r apps/mobile/android/app/build/outputs/apk/release/app-release.apk
For Terminal, use its device serial and the same output path under
apps/pos-terminal/.
Required local tooling
- Node.js version compatible with
>=22.11.0 - Yarn Classic
1.22.x - Java with
JAVA_HOMEpointing to a valid JDK - Android SDK installed and reachable from Gradle
- Android build-tools and platform SDK required by the project
- Android NDK version expected by Gradle
adbavailable inPATH
Release signing inputs
Both Android apps read release signing values from Gradle properties:
MYAPP_UPLOAD_STORE_FILEMYAPP_UPLOAD_STORE_PASSWORDMYAPP_UPLOAD_KEY_ALIASMYAPP_UPLOAD_KEY_PASSWORD
Verify where these values come from on each machine:
~/.gradle/gradle.properties- project
gradle.properties - CI/CD secret injection
APK install signature mismatch
If APK installation fails with:
INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package th.co.boonsiri.pos signatures do not match newer version
the device already has th.co.boonsiri.pos installed with a different signing
key. Android will not update an installed app with an APK signed by another
key.
If preserving local data, use a build signed with the same key or test on a separate emulator. Uninstall only when deleting the existing app data is approved:
adb -s R58M123456A uninstall th.co.boonsiri.pos
adb -s R58M123456A install -r apps/mobile/android/app/build/outputs/apk/release/app-release.apk
For Terminal the package is th.co.boonsiri.posterminal; target its device and
APK explicitly. Uninstalling removes local app data and requires signing in again.
R8 release optimization
Mobile and Terminal release builds now enable minifyEnabled and
shrinkResources, using proguard-android-optimize.txt and each app's
proguard-rules.pro. This addresses the low DEX optimization / 1% obfuscation
reported for Mobile 0.5.24 in Play Console. R8 optimizes Java/Kotlin DEX code;
this setting does not replace JavaScript bundling or 16 KB native-library checks.
Time4J and date-picker keep rules
Both apps retain these targeted rules:
-keep public class net.time4j.android.ApplicationStarter
-keep public class net.time4j.PrettyTime
-keep class net.time4j.android.spi.AndroidResourceLoader {
public <init>();
}
Release testing initially reproduced a startup crash reported as
react-native-date-picker is not installed correctly. The underlying exception
was that AndroidResourceLoader had no zero-argument constructor. Time4J creates
this loader through reflection; its class-only consumer rule was insufficient
with this build's R8 full mode. Keep the constructor explicitly instead of
suppressing the error or disabling R8.
Build and verify
From the repository root, run the dependency patches, then build either app:
node scripts/patch-react-native-dependencies.js
cd apps/pos-terminal/android
./gradlew :app:bundleRelease :app:assembleRelease
Use apps/mobile/android for Mobile. The signed AAB and APK are under
app/build/outputs/bundle/release/app-release.aab and
app/build/outputs/apk/release/app-release.apk, relative to that app's Android
directory. Retain app/build/outputs/mapping/release/mapping.txt with the release
for crash retracing.
The AAB must include BUNDLE-METADATA/com.android.tools/r8.json and
BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map. Check the
optimization options and stats in r8.json; enabled coverage is
100 - no*Percentage. Coverage is distinct from the actual reduction in DEX bytes.
An AAB is not directly installable with adb install. Use Google's
bundletool to generate a signed
APK set from the exact AAB using build-apks --connected-device --device-id=...,
then install it with install-apks --apks=... --device-id=.... Supply the project's
release keystore and read passwords from private files rather than exposing them
in command arguments or logs. Handle a signing mismatch as described above.
Recorded results on 2026-09-28
| Local artifact metric | Mobile | Terminal |
|---|---|---|
| Previous local AAB DEX bytes | 32,714,316 | 32,905,052 |
| Optimized AAB DEX bytes | 6,208,192 | 6,299,520 |
| Optimization coverage | 80.64% | 80.66% |
| Obfuscation coverage | 81.18% | 81.19% |
| Shrinking coverage | 81.21% | 81.22% |
Both apps built signed release AAB/APK files with R8 9.2.14, passed release
lint and APK signature checks, and included R8 metadata and mapping. These are
local build measurements, not an uploaded Play Console assessment.
- Mobile 525 / 0.5.25: tested two cold launches, native version display, empty-login validation and theme switching on an Android 17 emulator with 16,384-byte pages. The later AAB 526 / 0.5.26 was converted and installed on the Mobile phone; the login screen and version were verified. The incompatible existing app was removed with explicit approval.
- Terminal 423 / 0.4.23: tested release startup to branch selection and the native Customer Display advertising presentation on a second emulated display, also with 16,384-byte pages. No ReactNativeJS/AndroidRuntime errors were found in the checked final processes.
- After pushing Terminal changes,
yarn terminal:android:8082rebuilt and installed Debug on the C9D and connected to the existing Metro server on port8082. Debug installation is separate from the R8 Release verification; Release runs its bundled JavaScript without Metro. - Authenticated sales, date-picker interaction, scanning, printing, notifications, full two-screen transactions and Play Console assessment remain to be verified.
- Non-fatal build warnings remain in
play-services-auth:21.5.0(stack maps) and Time4J (-useuniqueclassmembernamesignored); recheck when updating dependencies.
Implementation references: Mobile 3245dc66, version update 25b2fa23, merge
into uat e343b482, and Terminal on uat 26f222ae.
React Native core upgrade notes
The current Android baseline for both apps is:
- React Native
0.87.1with the new architecture and Hermes enabled - Build Tools and compile SDK
37; target SDK36; minimum SDK24 - NDK
28.2.13676358, Kotlin2.2.10, and Gradle9.4.1 - Node
>=22.11.0; the completed physical-device verification used Node26.7.0through nvm
When upgrading React Native core, use React Native Upgrade Helper as the baseline template diff, for example:
https://react-native-community.github.io/upgrade-helper/?from=0.85.2&to=0.87.1
Do not copy template files over this repo blindly. The mobile app has
repo-specific native configuration that must be preserved, including monorepo
paths in android/app/build.gradle, explicit Node/Hermes paths,
react-native-vector-icons font linking, Firebase Google Services, release
signing, app versioning, and the Reanimated/Worklets Gradle task workaround.
React Native 0.87 uses Android Gradle Plugin 9. Keep
android.builtInKotlin=false and android.newDsl=false until the remaining
native dependencies no longer require the legacy Kotlin and variant APIs.