Skip to main content

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_HOME pointing 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
  • adb available in PATH

Release signing inputs​

Both Android apps read release signing values from Gradle properties:

  • MYAPP_UPLOAD_STORE_FILE
  • MYAPP_UPLOAD_STORE_PASSWORD
  • MYAPP_UPLOAD_KEY_ALIAS
  • MYAPP_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 metricMobileTerminal
Previous local AAB DEX bytes32,714,31632,905,052
Optimized AAB DEX bytes6,208,1926,299,520
Optimization coverage80.64%80.66%
Obfuscation coverage81.18%81.19%
Shrinking coverage81.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:8082 rebuilt and installed Debug on the C9D and connected to the existing Metro server on port 8082. 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 (-useuniqueclassmembernames ignored); 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.1 with the new architecture and Hermes enabled
  • Build Tools and compile SDK 37; target SDK 36; minimum SDK 24
  • NDK 28.2.13676358, Kotlin 2.2.10, and Gradle 9.4.1
  • Node >=22.11.0; the completed physical-device verification used Node 26.7.0 through 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.