diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..5244db5f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +# Bitchat Android - Agent Guide + +This document provides context, architectural insights, and development standards for AI agents working on the Bitchat Android codebase. + +## 1. Project Overview +**Bitchat** is a decentralized, off-grid communication application focused on privacy and censorship resistance. It utilizes mesh networking (primarily Bluetooth LE and Tor/Arti) to enable peer-to-peer messaging without centralized servers. + +**Key Technologies:** +- **Language:** Kotlin (JVM Target 1.8) +- **UI Framework:** Jetpack Compose (Material 3) +- **Asynchronous:** Kotlin Coroutines & Flow +- **Networking:** Bluetooth Low Energy (BLE), Tor (Arti Rust bridge), OkHttp +- **Architecture:** MVVM with Clean Architecture principles +- **Build System:** Gradle (Kotlin DSL) + +## 2. Architecture & Directory Structure +The application follows a clean architecture pattern, heavily modularized by feature within the `app` module. + +**Root Package:** `com.bitchat.android` + +| Directory | Purpose | +|-----------|---------| +| `ui/` | **Presentation Layer**: Jetpack Compose screens, themes, and ViewModels. | +| `service/` | **Core Service**: Contains `MeshForegroundService`, managing persistent background connectivity. | +| `mesh/` | **Mesh Networking**: Logic for peer discovery, advertising, and message routing. | +| `protocol/` | **Wire Protocol**: Definitions of messages exchanged between peers. | +| `crypto/` | **Security**: Cryptographic primitives and key management. | +| `noise/` | **Encryption**: Implementation of the Noise Protocol Framework for secure channels. | +| `identity/` | **User Identity**: Management of user profiles and public/private keys. | +| `features/` | **App Features**: Sub-modules for `voice`, `file`, and `media` handling. | +| `nostr/` | **Relay Integration**: Logic for Nostr protocol integration and relay management. | +| `geohash/` | **Location**: Utilities for location-based features and geohashing. | +| `net/` | **Networking**: General network utilities and abstractions. | + +## 3. Key Components + +### UI Layer (Jetpack Compose) +- **Activity**: Single-Activity architecture (`MainActivity.kt`). +- **Navigation**: Jetpack Compose Navigation. +- **State Management**: `ViewModel` exposing `StateFlow` to Composables. +- **Theme**: Custom theme definitions in `ui/theme`. + +### Networking & Connectivity +- **MeshForegroundService**: The critical component that keeps the mesh network alive. It manages the lifecycle of BLE scanning/advertising and other transport layers. +- **BLE Stack**: Located in `mesh/` and `net/`, handles the intricacies of Android Bluetooth interactions. +- **Tor/Arti**: Integrated via JNI (`jniLibs`) to provide anonymous internet routing where available. + +## 4. Development Standards + +### Code Style +- **Kotlin**: Adhere to official Kotlin coding conventions. +- **Compose**: Use functional components. Hoist state to ViewModels where possible. +- **Coroutines**: Use `suspend` functions for all I/O operations. strictly avoid blocking the main thread. +- **Naming**: Clear, descriptive names. Follow standard Android naming patterns (e.g., `*ViewModel`, `*Repository`, `*Screen`). + +### Testing +- **Unit Tests**: Located in `app/src/test/`. Use for business logic, protocols, and utility testing. +- **Instrumented Tests**: Located in `app/src/androidTest/`. Use for UI and permission integration testing. +- **Execution**: + - Unit: `./gradlew test` + - Instrumented: `./gradlew connectedAndroidTest` + +## 5. Critical Constraints & Gotchas +1. **Permissions**: The app relies heavily on dangerous runtime permissions (Location, Bluetooth Scan/Connect/Advertise, Audio Recording). Always verify permission handling patterns in `MainActivity` or permission wrappers before adding new hardware features. +2. **Hardware Dependency**: Features like BLE are difficult to emulate. When writing code for these, focus on robust error handling and defensive programming as hardware behavior can be flaky. +3. **Background Limits**: Android enforces strict background execution limits. Network operations intended to persist must be tied to the `MeshForegroundService`. + +## 6. Common Tasks +- **Build Debug APK**: `./gradlew assembleDebug` +- **Lint Check**: `./gradlew lint` +- **Clean Build**: `./gradlew clean` + +--- +*Note: This file is intended to assist AI agents in navigating and modifying the codebase efficiently. Always verify context by reading the actual files before making changes.* diff --git a/app/src/main/java/com/bitchat/android/geohash/AndroidGeocoderProvider.kt b/app/src/main/java/com/bitchat/android/geohash/AndroidGeocoderProvider.kt new file mode 100644 index 00000000..670d4cfc --- /dev/null +++ b/app/src/main/java/com/bitchat/android/geohash/AndroidGeocoderProvider.kt @@ -0,0 +1,52 @@ +package com.bitchat.android.geohash + +import android.content.Context +import android.location.Address +import android.location.Geocoder +import android.os.Build +import android.util.Log +import kotlinx.coroutines.suspendCancellableCoroutine +import java.util.Locale +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class AndroidGeocoderProvider(context: Context) : GeocoderProvider { + private val geocoder = Geocoder(context, Locale.getDefault()) + private val TAG = "AndroidGeocoderProvider" + + override suspend fun getFromLocation(latitude: Double, longitude: Double, maxResults: Int): List
{ + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + suspendCancellableCoroutine { cont -> + try { + geocoder.getFromLocation( + latitude, + longitude, + maxResults, + object : Geocoder.GeocodeListener { + override fun onGeocode(addresses: MutableList) { + if (cont.isActive) cont.resume(addresses) + } + + override fun onError(errorMessage: String?) { + if (cont.isActive) { + Log.e(TAG, "Geocode error: $errorMessage") + cont.resume(emptyList()) + } + } + } + ) + } catch (e: Exception) { + if (cont.isActive) cont.resumeWithException(e) + } + } + } else { + @Suppress("DEPRECATION") + try { + geocoder.getFromLocation(latitude, longitude, maxResults) ?: emptyList() + } catch (e: Exception) { + Log.e(TAG, "Geocode failed", e) + emptyList() + } + } + } +} diff --git a/app/src/main/java/com/bitchat/android/geohash/GeocoderFactory.kt b/app/src/main/java/com/bitchat/android/geohash/GeocoderFactory.kt new file mode 100644 index 00000000..2c1e809c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/geohash/GeocoderFactory.kt @@ -0,0 +1,19 @@ +package com.bitchat.android.geohash + +import android.content.Context +import android.location.Geocoder + +/** + * Factory to provide the best available geocoder. + */ +object GeocoderFactory { + fun get(context: Context): GeocoderProvider { + // If Google Play Services Geocoder is present, use it. + // Otherwise, fall back to OpenStreetMap. + return if (Geocoder.isPresent()) { + AndroidGeocoderProvider(context) + } else { + OpenStreetMapGeocoderProvider() + } + } +} diff --git a/app/src/main/java/com/bitchat/android/geohash/GeocoderProvider.kt b/app/src/main/java/com/bitchat/android/geohash/GeocoderProvider.kt new file mode 100644 index 00000000..bb4d69c2 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/geohash/GeocoderProvider.kt @@ -0,0 +1,13 @@ +package com.bitchat.android.geohash + +import android.location.Address + +/** + * Interface for reverse geocoding providers. + */ +interface GeocoderProvider { + /** + * Get a list of Address objects from latitude and longitude. + */ + suspend fun getFromLocation(latitude: Double, longitude: Double, maxResults: Int): List +} diff --git a/app/src/main/java/com/bitchat/android/geohash/GeohashBookmarksStore.kt b/app/src/main/java/com/bitchat/android/geohash/GeohashBookmarksStore.kt index d4ccb9bf..81d83b79 100644 --- a/app/src/main/java/com/bitchat/android/geohash/GeohashBookmarksStore.kt +++ b/app/src/main/java/com/bitchat/android/geohash/GeohashBookmarksStore.kt @@ -167,12 +167,11 @@ class GeohashBookmarksStore private constructor(private val context: Context) { if (gh.isEmpty()) return if (_bookmarkNames.value?.containsKey(gh) == true) return if (resolving.contains(gh)) return - if (!Geocoder.isPresent()) return resolving.add(gh) CoroutineScope(Dispatchers.IO).launch { try { - val geocoder = Geocoder(context, Locale.getDefault()) + val geocoderProvider = GeocoderFactory.get(context) val name: String? = if (gh.length <= 2) { // Composite admin name from multiple points val b = Geohash.decodeToBounds(gh) @@ -186,9 +185,8 @@ class GeohashBookmarksStore private constructor(private val context: Context) { val admins = linkedSetOf