From d4b37437a3b59ab3b7fc5b03abdbe07a818a3c15 Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Mon, 25 May 2026 16:21:07 -0300 Subject: [PATCH 1/4] Add release signing, fix release build, update README - Configure release signing via environment variables (android-signing-env) - Enable BuildConfig generation and guard HttpLoggingInterceptor to debug only - Move okhttp.logging to implementation so release build compiles - Update README: add E2EE, verification, push notifications, download link Co-Authored-By: Claude Sonnet 4.6 --- README.md | 43 +++++++++++++------ app/build.gradle.kts | 14 +++++- .../ltadeu6/matrix/matrix/MatrixSession.kt | 15 ++++--- 3 files changed, 53 insertions(+), 19 deletions(-) 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..26e07bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,16 @@ android { versionName = "0.1.0" } + signingConfigs { + create("release") { + storeFile = file(System.getenv("ANDROID_KEYSTORE_PATH") ?: "") + storeType = System.getenv("ANDROID_KEYSTORE_TYPE") ?: "PKCS12" + keyAlias = System.getenv("ANDROID_KEY_ALIAS") ?: "" + storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") ?: "" + keyPassword = System.getenv("ANDROID_KEY_PASSWORD") ?: "" + } + } + buildTypes { release { isMinifyEnabled = true @@ -25,6 +35,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") } } @@ -39,6 +50,7 @@ android { buildFeatures { compose = true + buildConfig = true } packaging { @@ -68,5 +80,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..d412285 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,8 @@ 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 okhttp3.Interceptor import retrofit2.Retrofit import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -1331,12 +1332,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) From 863cbb6b792775c0622e23a88f35d864e94a8678 Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Mon, 25 May 2026 16:34:24 -0300 Subject: [PATCH 2/4] Externalize PUSH_GATEWAY_URL and clean up signing config - Move PUSH_GATEWAY_URL from hardcoded constant to BuildConfig field, read from PUSH_GATEWAY_URL env var at build time - Hardcode keyAlias="release" and remove storeType (PKCS12 is default), since these are fixed values in android-signing-env - Remove unused okhttp3.Interceptor import Co-Authored-By: Claude Sonnet 4.6 --- app/build.gradle.kts | 5 +++-- app/src/main/java/com/ltadeu6/matrix/matrix/MatrixSession.kt | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26e07bd..ac3d8d4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,13 +16,14 @@ android { targetSdk = 35 versionCode = 1 versionName = "0.1.0" + buildConfigField("String", "PUSH_GATEWAY_URL", + "\"${System.getenv("PUSH_GATEWAY_URL") ?: ""}\"") } signingConfigs { create("release") { storeFile = file(System.getenv("ANDROID_KEYSTORE_PATH") ?: "") - storeType = System.getenv("ANDROID_KEYSTORE_TYPE") ?: "PKCS12" - keyAlias = System.getenv("ANDROID_KEY_ALIAS") ?: "" + keyAlias = "release" storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") ?: "" keyPassword = System.getenv("ANDROID_KEY_PASSWORD") ?: "" } 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 d412285..123b8f8 100644 --- a/app/src/main/java/com/ltadeu6/matrix/matrix/MatrixSession.kt +++ b/app/src/main/java/com/ltadeu6/matrix/matrix/MatrixSession.kt @@ -26,7 +26,6 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import com.ltadeu6.matrix.BuildConfig -import okhttp3.Interceptor import retrofit2.Retrofit import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -1360,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 = From e4e960024d94eec498e02defeda13cb8a1294ce9 Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Mon, 25 May 2026 17:04:42 -0300 Subject: [PATCH 3/4] Bump version to 0.1.1 Fix push notifications: PUSH_GATEWAY_URL was empty in v0.1.0 because it was externalized to BuildConfig before android-signing-env exported it. Co-Authored-By: Claude Sonnet 4.6 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ac3d8d4..494db06 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ 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") ?: ""}\"") } From 1d99440b01394ffcdbd008dc88ba3b1d9fd4daed Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Mon, 25 May 2026 17:06:52 -0300 Subject: [PATCH 4/4] Update CLAUDE.md with release build and secrets setup Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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