Skip to main content
New Expensify for Android provides a native experience with full offline support, camera receipt scanning, and Material Design integration.

Prerequisites

System Requirements

  • macOS, Windows, or Linux
  • Node.js 18+ and npm
  • Java Development Kit (JDK) 17+
  • Android Studio (latest stable version)
  • Android SDK Platform 34+
  • Android SDK Build-Tools
Follow the React Native Environment Setup for detailed prerequisites.

Quick Start with Rock

New Expensify uses Rock to download pre-built native artifacts, avoiding lengthy Gradle builds.

Running the App

# Install dependencies
npm install

# Start Metro bundler (required)
npm run start

# In another terminal, run Android app
npm run android
For standalone NewDot (without HybridApp integration), use npm run android-standalone.

How Rock Works

  1. Fingerprint Generation: Creates fingerprint from native dependencies
  2. Remote Check: Looks for matching build on S3
  3. Download or Build: Downloads if available, builds locally if not
  4. Fast Iteration: Only rebuilds when native code changes
Files that trigger rebuild:
  • package.json
  • android/build.gradle
  • android/app/build.gradle
  • Native module changes

Manual Build Setup

1. Configure MapBox

# Run MapBox configuration script
npm run configure-mapbox
Enter your MapBox token when prompted (same token as iOS).

2. React Native Environment

Follow the official React Native Android setup guide to install:
  • Android Studio
  • Android SDK
  • Android SDK Platform
  • Android Virtual Device (AVD)

3. Optional: ccache Setup

Speed up C/C++ compilation with ccache:
# Install ccache (macOS)
brew install ccache

# Check cache statistics
ccache --show-stats
The build system automatically detects and uses ccache when available. No configuration needed!

4. Run the App

# Development emulator
npm run android

Android-Specific Features

Camera Receipt Scanning

Android camera integration with runtime permissions:
src/components/AttachmentPicker/launchCamera/launchCamera.android.ts
import {PermissionsAndroid} from 'react-native';
import {launchCamera as launchCameraImagePicker} from 'react-native-image-picker';

const launchCamera: LaunchCamera = (options, callback) => {
    // Check camera permissions
    PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA)
        .then((permission) => {
            if (permission !== PermissionsAndroid.RESULTS.GRANTED) {
                throw new Error('User did not grant permissions');
            }
            launchCameraImagePicker(options, callback);
        })
        .catch((error) => {
            callback({
                errorMessage: error.message,
                errorCode: 'permission',
            });
        });
};
See Receipt Scanning for more details on mobile receipt capture.

Share Intent Handling

Android share intents for receipts and files:
android/app/src/main/java/com/expensify/chat/MainActivity.kt
class MainActivity : ReactActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(null)
        
        if (intent != null) {
            handleIntent(intent)
        }
    }
    
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleIntent(intent)
    }
    
    private fun handleIntent(intent: Intent) {
        try {
            val intentHandler = IntentHandlerFactory.getIntentHandler(
                this, 
                intent.type, 
                intent.toString()
            )
            intentHandler?.handle(intent)
        } catch (exception: Exception) {
            Log.e("handleIntentException", exception.toString())
        }
    }
}

Push Notifications

Android uses FCM through Airship:
android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java
import com.urbanairship.AirshipExtender;
import com.urbanairship.channel.AirshipChannelListener;

public class CustomAirshipExtender extends AirshipExtender {
    @Override
    protected NotificationProvider onCreateNotificationProvider(
        @NonNull Context context,
        @NonNull AirshipConfigOptions configOptions
    ) {
        return new CustomNotificationProvider(context, configOptions);
    }
}
See Push Notifications for configuration details.

Background Tasks

Android background processing for sync and notifications:
android/app/src/main/java/com/expensify/chat/customairshipextender/GpsTripService.kt
class GpsTripService : JobIntentService() {
    override fun onHandleWork(intent: Intent) {
        // Handle GPS trip updates in background
    }
}

Hardware Back Button

Custom back button handling:
override fun onBackPressed() {
    // Let React Native handle back navigation
    if (!moveTaskToBack(false)) {
        super.onBackPressed()
    }
}

Hardware Keyboard Shortcuts

android/app/src/main/java/com/expensify/chat/MainActivity.kt
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
    if (event.keyCode == KeyEvent.KEYCODE_ESCAPE) {
        return false // Handled by Android
    }
    KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event)
    return super.onKeyDown(keyCode, event)
}

Running on Physical Device

Enable USB Debugging

  1. Open Settings > About Phone
  2. Tap “Build Number” 7 times to enable Developer Options
  3. Go to Settings > Developer Options
  4. Enable “USB Debugging”
  5. Connect device via USB

Run App

# List connected devices
adb devices

# Run on connected device
npm run android
For some devices, you may need to enable “Install via USB” in Developer Options.

Development Workflow

Hot Reload

JavaScript changes automatically reload:
  • Fast Refresh: Automatic for most changes
  • Manual Reload: Double-tap R or shake device
  • Debug Menu: Shake device or Cmd + M (macOS) / Ctrl + M (Windows/Linux)

Debugging

New Expensify uses Hermes JS engine:
  1. Navigate to chrome://inspect
  2. Click “Configure” and add Metro server: localhost:8081
  3. Find “Hermes React Native” target
  4. Click “inspect” to open DevTools
See React Native Debugging

Testing Web in Android Emulator

To test the web app in Chrome on Android emulator:
# Setup HTTPS certificates and install on emulator
npm run setupNewDotWebForEmulators android

# Or for `https://127.0.0.1:8082` (simpler)
adb push "$(mkcert -CAROOT)/rootCA.pem" /storage/emulated/0/Download/
# Then install certificate via Settings > Security
adb reverse tcp:8082 tcp:8082

Performance Profiling

Enable Source Maps

In android/app/build.gradle:
android/app/build.gradle
project.ext.react = [
    enableHermes: true,
    hermesFlagsRelease: ["-O", "-output-source-map"],
]

Record Performance Traces

  1. Build app in production mode
  2. Navigate to feature to profile
  3. Four-finger tap to open menu
  4. Select “Use Profiling”
  5. Interact with app
  6. Four-finger tap to stop
  7. Share trace file

Symbolicate Traces

# Place Profile<version>.cpuprofile at project root
# Generate source map from same branch
npm run symbolicate-release:android

# Open Profile_trace_for_<version>-converted.json in:
# - https://www.speedscope.app
# - https://ui.perfetto.dev  
# - chrome://tracing

Prebuilt React Native Artifacts

Speed up builds by using prebuilt React Native:

Enable Prebuilt Artifacts

Edit android/gradle.properties (Standalone) or Mobile-Expensify/Android/gradle.properties (HybridApp):
patchedArtifacts.forceBuildFromSource=false

Configure GitHub CLI

# Install GitHub CLI
brew install gh  # macOS
# or visit https://cli.github.com

# Create Personal Access Token with scopes:
# - repo
# - read:org
# - gist  
# - read:packages

# Login
echo "YOUR_TOKEN" | gh auth login --with-token

# Verify
gh auth status

Push Notification Setup (Development)

To receive push notifications while testing:

HybridApp

Edit Mobile-Expensify/Android/assets/airshipconfig.properties:
inProduction = true

Standalone

Copy production config to development:
cp android/app/src/main/assets/airshipconfig.properties \
   android/app/src/development/assets/airshipconfig.properties

Troubleshooting

Rock Build Issues

  1. Reinstall dependencies:
    npm run i-standalone  # standalone
    npm install           # hybrid
    
  2. Run again:
    npm run android-standalone  # or npm run android
    
  1. Clean Android directory:
    git clean -fdx android/              # standalone
    git clean -fdx ./Mobile-Expensify    # hybrid
    
  2. Try running again
  1. Check GitHub Actions: Android Builds
  2. Compare fingerprints:
    npx rock fingerprint -p android --verbose
    

Gradle Issues

# Clean Gradle cache
cd android
./gradlew clean

# Clear Gradle daemon
./gradlew --stop

# Remove build artifacts
rm -rf ~/.gradle/caches/

Metro Bundler Issues

# Clear Metro cache
npm start -- --reset-cache

# Clear watchman
watchman watch-del-all

ADB Connection Issues

# Restart ADB server
adb kill-server
adb start-server

# Check connected devices
adb devices

# Reverse port for Metro (if needed)
adb reverse tcp:8081 tcp:8081

Emulator Performance

1

Enable Hardware Acceleration

In Android Studio: Tools > AVD Manager > Edit Device > Show Advanced SettingsSet:
  • Graphics: Hardware - GLES 2.0
  • Boot option: Cold boot
2

Allocate More RAM

Increase RAM in AVD settings (2048 MB minimum)
3

Use x86_64 Image

x86_64 images are faster than ARM on Intel/AMD processors

Certificate Installation Fails

If you see “adbd cannot run as root in production builds”:
# Use simpler localhost approach
adb reverse tcp:8082 tcp:8082
# Access via https://127.0.0.1:8082
See Stack Overflow solution.

Native Modules

Creating Native Android Modules

android/app/src/main/java/com/expensify/chat/CustomModule.kt
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod

class CustomModule(reactContext: ReactApplicationContext) : 
    ReactContextBaseJavaModule(reactContext) {
    
    override fun getName() = "CustomModule"
    
    @ReactMethod
    fun customMethod() {
        // Native implementation
    }
}

Register Module

android/app/src/main/java/com/expensify/chat/CustomPackage.kt
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext

class CustomPackage : ReactPackage {
    override fun createNativeModules(
        reactContext: ReactApplicationContext
    ): List<NativeModule> {
        return listOf(CustomModule(reactContext))
    }
}

Build Configurations

Debug vs Release

# Debug build (default)
npm run android

# Release build
npm run android-build

Build Variants

  • debug: Development with debugging enabled
  • release: Production-ready optimized build
  • development: Hybrid development variant
# Build specific variant
cd android
./gradlew assembleDebug
./gradlew assembleRelease

Resources

React Native Docs

Official React Native Android setup

Android Developers

Android development resources

Rock Documentation

Learn about Rock build system

Airship Android SDK

Push notification integration

Next Steps

Receipt Scanning

Mobile receipt capture

Push Notifications

Configure push notifications

Offline Mode

Offline functionality

iOS Platform

iOS development setup

Build docs developers (and LLMs) love