diff --git a/CLAUDE.md b/CLAUDE.md index 79f5603..9b436bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,11 +8,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co # First-time setup: generate the Gradle wrapper (requires Gradle installed) gradle wrapper --gradle-version 8.9 -# Build debug APK -./gradlew assembleDebug +# Build debug APK (inside nix dev shell) +nix develop --command gradle --no-daemon assembleDebug -# Build release APK -./gradlew assembleRelease +# Build release APK — requires android-signing-env (sets signing vars + PUSH_GATEWAY_URL) +android-signing-env nix develop --command gradle --no-daemon assembleRelease # Install on connected device/emulator ./gradlew installDebug @@ -24,6 +24,20 @@ gradle wrapper --gradle-version 8.9 ./gradlew clean ``` +### Secrets setup (required before release build) + +```bash +# Creates symlinks for google-services.json, firebase-service-account.json, .mc/credentials.json +# Only needed once per boot (symlinks point into /run/agenix which is tmpfs) +matrix-android-secrets /home/ltadeu6/Projetos/Codigo/matrix-android +``` + +### Deploy Cloudflare Worker (push gateway) + +```bash +cloudflare-worker-env nix develop --command wrangler deploy --config gateway/wrangler.toml +``` + The project has no unit tests yet. Android Studio is the recommended way to open, sync, and run the project. ## Architecture diff --git a/README.md b/README.md index 3368c62..38d60b7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ A minimal Android Matrix chat client with a terminal aesthetic — dark backgrou - Text-only chat (no images, no avatars) - Real-time sync via Matrix `/sync` long-polling - Persistent session (survives app restart) +- End-to-end encryption (E2EE) via libolm / Megolm +- SAS device verification +- Push notifications (foreground via sync loop; background via FCM + Cloudflare Worker) + +## Download + +Pre-built APKs are available on the [releases page](https://git.tadix.dev/ltadeu6/matrix-android/releases). ## Screenshots / UI description @@ -103,27 +110,37 @@ Enter your full Matrix username (`@user:homeserver.tld`) and password directly. ``` app/src/main/java/com/ltadeu6/matrix/ -├── MainActivity.kt — single Activity; handles SSO deep link +├── MainActivity.kt — single Activity; handles SSO deep link ├── navigation/ -│ └── AppNavigation.kt — Compose NavHost (Login → Rooms → Chat) +│ └── AppNavigation.kt — Compose NavHost (Login → Rooms → Chat) ├── auth/ -│ ├── LoginScreen.kt — login UI -│ └── LoginViewModel.kt — SSO URL builder, login calls +│ ├── LoginScreen.kt — login UI +│ └── LoginViewModel.kt — SSO URL builder, login calls ├── rooms/ -│ └── RoomListScreen.kt — joined rooms list +│ └── RoomListScreen.kt — joined rooms list ├── chat/ -│ ├── ChatScreen.kt — message list + input bar -│ └── ChatViewModel.kt — sends messages, maps sync state +│ ├── ChatScreen.kt — message list + input bar +│ └── ChatViewModel.kt — sends messages, maps sync state ├── matrix/ -│ ├── MatrixApi.kt — Retrofit interface (Matrix REST v3) -│ ├── MatrixModels.kt — serialization data classes + local models -│ └── MatrixSession.kt — singleton: auth, sync loop, state flows +│ ├── MatrixApi.kt — Retrofit interface (Matrix REST v3) +│ ├── MatrixModels.kt — serialization data classes + local models +│ ├── MatrixSession.kt — singleton: auth, sync loop, state flows +│ ├── CryptoService.kt — E2EE: Olm/Megolm, OTK management +│ ├── VerificationService.kt — SAS device verification +│ ├── NotificationHelper.kt — notification channel + posting +│ └── MatrixFirebaseMessagingService.kt — FCM push (app killed) +├── verification/ +│ └── VerificationScreen.kt — verification UI └── ui/theme/ - └── Theme.kt — terminal color scheme (green on black) + └── Theme.kt — terminal color scheme (green on black) ``` **State flow**: `MatrixSession` owns a `syncState: StateFlow` that all screens observe. The background sync loop continuously updates it via `/sync` long-polling. +## Push notifications + +When the app is in the foreground or background, notifications are fired directly from the sync loop. When the app is killed, push is delivered via FCM through a self-hosted Cloudflare Worker (`gateway/`) that acts as a Matrix push gateway. + ## Dependencies | Library | Purpose | @@ -135,10 +152,10 @@ app/src/main/java/com/ltadeu6/matrix/ | retrofit2-kotlinx-serialization-converter | Retrofit ↔ kotlinx bridge | | kotlinx.coroutines | Async / sync loop | | androidx.browser (Custom Tabs) | SSO browser login | +| libolm (native) | E2EE: Olm/Megolm encryption | +| Firebase Messaging | FCM push notifications | ## Known limitations -- No E2E encryption (plain-text transport only; suitable for unencrypted rooms) -- No push notifications - No media/file messages (text only) - Room list is not paginated (loads whatever `/sync` returns on first sync) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc605d0..494db06 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,19 @@ android { applicationId = "com.ltadeu6.matrix" minSdk = 26 targetSdk = 35 - versionCode = 1 - versionName = "0.1.0" + versionCode = 2 + versionName = "0.1.1" + buildConfigField("String", "PUSH_GATEWAY_URL", + "\"${System.getenv("PUSH_GATEWAY_URL") ?: ""}\"") + } + + signingConfigs { + create("release") { + storeFile = file(System.getenv("ANDROID_KEYSTORE_PATH") ?: "") + keyAlias = "release" + storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") ?: "" + keyPassword = System.getenv("ANDROID_KEY_PASSWORD") ?: "" + } } buildTypes { @@ -25,6 +36,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") } } @@ -39,6 +51,7 @@ android { buildFeatures { compose = true + buildConfig = true } packaging { @@ -68,5 +81,5 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.messaging) debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.okhttp.logging) + implementation(libs.okhttp.logging) } diff --git a/app/src/main/java/com/ltadeu6/matrix/matrix/MatrixSession.kt b/app/src/main/java/com/ltadeu6/matrix/matrix/MatrixSession.kt index ca29ff2..123b8f8 100644 --- a/app/src/main/java/com/ltadeu6/matrix/matrix/MatrixSession.kt +++ b/app/src/main/java/com/ltadeu6/matrix/matrix/MatrixSession.kt @@ -25,7 +25,7 @@ import android.util.Log import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.logging.HttpLoggingInterceptor +import com.ltadeu6.matrix.BuildConfig import retrofit2.Retrofit import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -1331,12 +1331,16 @@ class MatrixSession private constructor(private val appContext: Context) { } private fun buildApi(homeserver: String) { - val logging = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } - val client = OkHttpClient.Builder() + val clientBuilder = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) - .addInterceptor(logging) - .build() + if (BuildConfig.DEBUG) { + val logging = okhttp3.logging.HttpLoggingInterceptor().apply { + level = okhttp3.logging.HttpLoggingInterceptor.Level.BODY + } + clientBuilder.addInterceptor(logging) + } + val client = clientBuilder.build() api = Retrofit.Builder() .baseUrl(homeserver) .client(client) @@ -1355,8 +1359,7 @@ class MatrixSession private constructor(private val appContext: Context) { companion object { private const val TAG = "MatrixCrypto" - /** Cloudflare Worker push gateway URL. */ - const val PUSH_GATEWAY_URL = "https://matrix-push-gateway.ltadeu6.workers.dev" + val PUSH_GATEWAY_URL: String get() = BuildConfig.PUSH_GATEWAY_URL @Volatile private var instance: MatrixSession? = null fun getInstance(context: Context): MatrixSession =