Developer Guide

September 20, 2026 · View on GitHub

This guide explains how to set up and develop the Messages app locally without Android Studio.

Prerequisites

System Requirements

  • OS: Linux (Ubuntu 20.04+, Fedora 36+), macOS (12+), or Windows with WSL2
  • RAM: 8GB minimum, 16GB recommended
  • Disk: 20GB free space for SDK + emulator + build artifacts
  • CPU: x86_64 with virtualization support (Intel VT-x or AMD-V)

Required Software

ToolVersionPurpose
JDK21 LTSBuild system (Gradle)
Android SDKAPI 36Compile app
Android EmulatorLatestTest app
Gradle9.6.0Build automation (included via wrapper)

Step-by-Step Setup

1. Install JDK 21

Linux (Ubuntu/Debian)

# Using Adoptium (recommended)
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add -
echo "deb https://packages.adoptium.net/artifactory/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
sudo apt update
sudo apt install temurin-21-jdk

# Verify installation
java -version

Linux (Fedora/RHEL)

# Using SDKMAN (works on all distros)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.4-tem

# Verify
java -version

macOS

# Using Homebrew
brew install --cask temurin21

# Or using SDKMAN
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.4-tem

Windows (WSL2)

# Inside WSL2 Ubuntu
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 21.0.4-tem

2. Install Android SDK (No Android Studio)

Download Command-Line Tools

# Create SDK directory
mkdir -p ~/android/cmdline-tools

# Download latest command-line tools (check https://developer.android.com/studio#command-line-tools for updates)
# Linux:
wget https://dl.google.com/android/repository/commandlinetools-linux-11076708.zip -O /tmp/cmdline-tools.zip

# macOS (Intel):
# wget https://dl.google.com/android/repository/commandlinetools-mac-11076708.zip -O /tmp/cmdline-tools.zip

# macOS (Apple Silicon):
# wget https://dl.google.com/android/repository/commandlinetools-mac_arm64-11076708.zip -O /tmp/cmdline-tools.zip

# Extract and organize
unzip /tmp/cmdline-tools.zip -d ~/android/cmdline-tools
mv ~/android/cmdline-tools/cmdline-tools ~/android/cmdline-tools/latest
rm /tmp/cmdline-tools.zip

Set Environment Variables

Add to ~/.bashrc or ~/.zshrc:

# Android SDK
export ANDROID_HOME=$HOME/android
export ANDROID_SDK_ROOT=$HOME/android
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/emulator

# JDK (if using SDKMAN)
export JAVA_HOME=$HOME/.sdkman/candidates/java/current

# Gradle (project-specific)
export PATH=$PATH:$HOME/tools/gradle-9.6.0/bin

Reload your shell:

source ~/.bashrc  # or source ~/.zshrc

Accept Licenses and Install SDK

# Accept all licenses
yes | sdkmanager --licenses

# Install required SDK components
sdkmanager \
  "platforms;android-36" \
  "build-tools;36.0.0" \
  "platform-tools" \
  "emulator" \
  "system-images;android-36;google_apis;x86_64"

# Verify installation
sdkmanager --list_installed

3. Install Gradle (Optional)

The project includes a Gradle wrapper, but for faster builds you can install Gradle globally:

# Download Gradle 9.6.0
mkdir -p ~/tools
wget https://services.gradle.org/distributions/gradle-9.6.0-bin.zip -O /tmp/gradle.zip
unzip /tmp/gradle.zip -d ~/tools
rm /tmp/gradle.zip

# Verify
~/tools/gradle-9.6.0/bin/gradle --version

4. Set Up Android Emulator

Create AVD (Android Virtual Device)

# List available system images
sdkmanager --list | grep "system-images;android-36"

# Create AVD
# Format: avdmanager create avd -n <name> -k <system-image> -d <device>
avdmanager create avd \
  -n "Pixel_7_API_36" \
  -k "system-images;android-36;google_apis;x86_64" \
  -d "pixel_7"

# Verify AVD creation
avdmanager list avd

Configure AVD (Optional)

Edit ~/.android/avd/Pixel_7_API_36.avd/config.ini:

# Display
hw.lcd.width=1080
hw.lcd.height=2400
hw.lcd.density=420

# Performance
hw.ramSize=2048
vm.heapSize=256

# Storage
disk.dataPartition.size=2048M
hw.sdCard.size=512M

# Navigation (gesture or 3-button)
hw.mainKeys=no
hw.trackBall=no

Start Emulator

# Start emulator in background
emulator -avd Pixel_7_API_36 -no-window -no-audio &

# Or start with GUI (requires display)
emulator -avd Pixel_7_API_36

# Wait for emulator to boot (check status)
adb wait-for-device
adb shell getprop sys.boot_completed
# Should return "1" when ready

5. Clone and Build the Project

# Clone repository
git clone https://github.com/an1ndra/Messages.git
cd Messages

# Make scripts executable
chmod +x scripts/*.sh

# Build debug APK
./gradlew assembleDebug

# Or using project's Gradle
~/tools/gradle-9.6.0/bin/gradle assembleDebug --no-daemon

6. Install and Run

# Install on emulator
adb install -r app/build/outputs/apk/debug/app-debug.apk

# Launch app
adb shell am start -n com.anindra.messages/.MainActivity

# Set as default SMS app (required for full functionality)
adb shell cmd role add-role-holder android.app.role.SMS com.anindra.messages

Development Environment

Complete Environment Setup Script

Save as setup-dev.sh and run:

#!/bin/bash
set -e

echo "=== Setting up Messages development environment ==="

# Create directories
mkdir -p ~/tools ~/android/cmdline-tools

# Install SDKMAN
if [ ! -d "$HOME/.sdkman" ]; then
    curl -s "https://get.sdkman.io" | bash
    source "$HOME/.sdkman/bin/sdkman-init.sh"
fi

# Install JDK 21
sdk install java 21.0.4-tem 2>/dev/null || true

# Download Android command-line tools
if [ ! -d "$HOME/android/cmdline-tools/latest" ]; then
    wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708.zip -O /tmp/cmdline-tools.zip
    unzip -q /tmp/cmdline-tools.zip -d ~/android/cmdline-tools
    mv ~/android/cmdline-tools/cmdline-tools ~/android/cmdline-tools/latest
    rm /tmp/cmdline-tools.zip
fi

# Set environment
export ANDROID_HOME=$HOME/android
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator

# Accept licenses and install SDK
yes | sdkmanager --licenses > /dev/null 2>&1
sdkmanager "platforms;android-36" "build-tools;36.0.0" "platform-tools" "emulator" "system-images;android-36;google_apis;x86_64" > /dev/null 2>&1

# Create AVD
if ! avdmanager list avd | grep -q "Pixel_7_API_36"; then
    avdmanager create avd -n "Pixel_7_API_36" -k "system-images;android-36;google_apis;x86_64" -d "pixel_7"
fi

# Install Gradle
if [ ! -d "$HOME/tools/gradle-9.6.0" ]; then
    wget -q https://services.gradle.org/distributions/gradle-9.6.0-bin.zip -O /tmp/gradle.zip
    unzip -q /tmp/gradle.zip -d ~/tools
    rm /tmp/gradle.zip
fi

echo "=== Setup complete! ==="
echo "Add to ~/.bashrc or ~/.zshrc:"
echo "  export ANDROID_HOME=\$HOME/android"
echo "  export PATH=\$PATH:\$ANDROID_HOME/cmdline-tools/latest/bin:\$ANDROID_HOME/platform-tools:\$ANDROID_HOME/emulator"
echo "  export JAVA_HOME=\$HOME/.sdkman/candidates/java/current"

IDE Setup (Optional)

If you prefer an IDE (without Android Studio):

VS Code

  1. Install Extension Pack for Java
  2. Install Gradle for Java
  3. Open the Messages folder
  4. VS Code will auto-detect the project

IntelliJ IDEA Community Edition

  1. Download from jetbrains.com
  2. Open project folder
  3. Import Gradle project when prompted
  4. Set JDK to 21 in Project Settings

Project Structure

Messages/
├── app/
│   ├── src/main/
│   │   ├── java/com/anindra/messages/
│   │   │   ├── MessagesApplication.kt     # App entry point (owns Repository singleton)
│   │   │   ├── MainActivity.kt            # Single activity + navigation (+ AppViewModel)
│   │   │   ├── LinkText.kt                # Linkified/OTP text rendering
│   │   │   ├── crash/                     # Crash capture + report dialog
│   │   │   ├── diagnostics/               # Diagnostics report + display-mode pick
│   │   │   ├── data/                      # SQLite, models, settings, SIM/MMS/backup helpers
│   │   │   │   ├── Repository.kt          # SQLite (DB v17) + Flow queries + provider sync
│   │   │   │   ├── Models.kt              # Data classes
│   │   │   │   ├── SettingsStore.kt       # SharedPreferences
│   │   │   │   ├── MmsSupport.kt / MmsProviderReader.kt
│   │   │   │   ├── SimCard.kt / SimLabels.kt / SimSelection.kt
│   │   │   │   └── BackupCrypto.kt / BackupPolicy.kt / KeywordFilter.kt
│   │   │   ├── sms/                       # SMS handling
│   │   │   │   ├── SmsReceiver.kt         # Incoming SMS
│   │   │   │   ├── SmsSupport.kt          # Send + notifications + tones
│   │   │   │   ├── MmsReceiver.kt         # WAP_PUSH_DELIVER (SMS-app eligibility)
│   │   │   │   ├── ScheduledMessageSender.kt
│   │   │   │   ├── SmsStatusReceiver.kt   # Sent/delivery status
│   │   │   │   ├── QuickReplyReceiver.kt  # Notification inline reply
│   │   │   │   ├── MarkReadReceiver.kt    # Notification mark-as-read
│   │   │   │   ├── NoConfirmationSmsSendService.kt
│   │   │   │   ├── ForegroundTracker.kt   # Foreground state for notifications
│   │   │   │   └── ReceiverWakeLock.kt    # BroadcastReceiver wakelock helper
│   │   │   └── ui/                        # Compose screens + helpers
│   │   │       ├── ConversationsScreen.kt # Home list + search + archive
│   │   │       ├── ChatScreen.kt          # Message bubbles + menu + scheduling
│   │   │       ├── NewChatScreen.kt       # Contact picker / manual entry
│   │   │       ├── ContactDetailsScreen.kt# Contact profile
│   │   │       ├── SettingsScreen.kt      # App settings
│   │   │       ├── AdvancedSettingsScreen.kt
│   │   │       ├── AccessibilityScreen.kt # Accessibility mode options
│   │   │       ├── TrashScreen.kt         # Trash restore/purge
│   │   │       ├── OtpDetector.kt         # Keyword-gated OTP matcher
│   │   │       ├── MessageGrouping.kt     # Message list grouping rules
│   │   │       ├── BubbleEntrance.kt / MessageDetails.kt / A11y.kt
│   │   │       ├── Components.kt / BlockedKeywordsDialog.kt
│   │   │       ├── BackupLocation.kt / PhotoDecode.kt / PhotoUriCache.kt
│   │   │       └── theme/                 # Theme.kt, Type.kt, A11yOptions.kt
│   │   └── res/                           # Resources
│   └── build.gradle.kts                   # App dependencies
├── scripts/                               # Test automation (git submodule
│                                          #   git@github.com:an1ndra/Messages-scripts.git;
│                                          #   git submodule update --init scripts;
│                                          #   agent rules + task tracking live
│                                          #   in scripts/AGENTS.md, scripts/TODO.md)
├── docs/                                  # Development guides (this file, Development.md)
└── README.md                             # Project overview

Architecture

Single-Activity Architecture

The app uses a single MainActivity with a manual navRoute state (no Navigation-Compose dependency):

// MainActivity.kt
setContent {
    when (navRoute) {
        "list" -> ConversationsScreen(...)
        "chat" -> ChatScreen(...)
        "settings" -> SettingsScreen(...)
        "advanced" -> AdvancedSettingsScreen(...)
        "accessibility" -> AccessibilityScreen(...)
        // new, details, trash, opening ...
    }
}

Reactive Data Flow

SQLite DB → Flow queries → Repository → ViewModel → Compose UI

All data updates automatically when the database changes.

Key Patterns

  • State Hoisting: UI state lifted to ViewModel
  • CollectAsState: Flow collection in Compose
  • Side Effects: LaunchedEffect for async operations
  • Dependency Injection: Manual via Application class

Testing

Manual Testing

Use the test scripts in scripts/:

# Run all tests
scripts/run-all-tests.sh

# Test specific feature
scripts/test-back-nav.sh      # Back navigation
scripts/test-chat-menu.sh     # Chat menu options
scripts/test-message-lock.sh  # Message locking

Test Scripts Environment

Scripts assume:

  • Emulator: emulator-5554
  • Resolution: 1080x2400 @ 420dpi
  • Shell: Bash (not zsh)

Automated UI Testing

For comprehensive testing, use the provided test scripts as examples for:

  • UI navigation
  • Gesture simulation
  • Screenshot capture
  • State verification

Building for Release

Debug Build

./gradlew assembleDebug
# Output: app/build/outputs/apk/debug/app-debug.apk

Release Build

# Requires release keystore in project root
./gradlew assembleRelease
# Output: app/build/outputs/apk/release/app-release.apk

Signing

Release builds are signed automatically when release.keystore exists in the project root and both password environment variables are set (never commit the keystore or passwords):

keytool -genkey -v -keystore release.keystore \
  -alias messages -keyalg RSA -keysize 2048 -validity 10000

export KEYSTORE_PASSWORD=your_password
export KEY_PASSWORD=your_password
./gradlew assembleRelease

Without the keystore/env vars, assembleRelease produces an unsigned APK (app-release-unsigned.apk) — this is what F-Droid's build server expects.

Common Development Tasks

Adding a New Feature

  1. Create feature branch: git checkout -b feature/new-feature
  2. Add data model in data/Models.kt if needed
  3. Add database column/migration in data/Repository.kt
  4. Add UI in ui/ directory
  5. Track completion in scripts/TODO.md (scripts submodule)
  6. Create test script in scripts/
  7. Build and test: ./gradlew assembleDebug

Database Migrations

When modifying the database:

  1. Increment version in Repository.kt (currently v17)
  2. Add migration in onUpgrade() (incremental per-version ALTER/CREATE/INDEX)
  3. Test with existing data (don't clear app data)
  4. Update scripts/TODO.md with migration notes (scripts submodule)

UI Changes

Follow Material 3 guidelines:

  • Use MaterialTheme.colorScheme.* for colors
  • Never hardcode hex colors (except avatar palette)
  • Test in both light and dark modes
  • Maintain accessibility (contrast ratios)

Troubleshooting

Build Errors

# Clean and rebuild
./gradlew clean assembleDebug

# Check for dependency conflicts
./gradlew app:dependencies

Emulator Issues

# Restart emulator
adb emu kill
emulator -avd Pixel_7_API_36

# Clear app data
adb shell pm clear com.anindra.messages

# Check emulator status
adb devices
adb shell getprop ro.build.version.sdk

Permission Errors

# Grant all permissions
scripts/grant-permissions.sh

# Reset to see permission dialogs
scripts/reset-permissions.sh

SDK Manager Issues

# Update SDK tools
sdkmanager --update

# Reinstall specific package
sdkmanager --install "platforms;android-36"

# Check installed packages
sdkmanager --list_installed

Code Style

  • Kotlin: Follow official conventions
  • Compose: Prefer readability over brevity
  • Comments: Only for non-obvious logic
  • Naming: Descriptive, follow Android conventions

Performance

  • Database: Use Flow for reactive updates
  • Images: Lazy loading with Coil
  • Lists: LazyColumn with keys for stable animations
  • State: Minimize recompositions

Security

  • No internet permission (offline-only)
  • Local database only (no cloud sync)
  • Biometric lock for sensitive messages
  • No analytics or tracking

Contributing

  1. Fork the repository
  2. Create feature branch
  3. Commit changes
  4. Push to branch
  5. Create Pull Request

Follow existing code style and add tests where applicable.

License

This project is licensed under GPL-3.0 - see LICENSE for details.