Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

4 changed files with 27 additions and 74 deletions

View file

@ -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) # First-time setup: generate the Gradle wrapper (requires Gradle installed)
gradle wrapper --gradle-version 8.9 gradle wrapper --gradle-version 8.9
# Build debug APK (inside nix dev shell) # Build debug APK
nix develop --command gradle --no-daemon assembleDebug ./gradlew assembleDebug
# Build release APK — requires android-signing-env (sets signing vars + PUSH_GATEWAY_URL) # Build release APK
android-signing-env nix develop --command gradle --no-daemon assembleRelease ./gradlew assembleRelease
# Install on connected device/emulator # Install on connected device/emulator
./gradlew installDebug ./gradlew installDebug
@ -24,20 +24,6 @@ android-signing-env nix develop --command gradle --no-daemon assembleRelease
./gradlew clean ./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. The project has no unit tests yet. Android Studio is the recommended way to open, sync, and run the project.
## Architecture ## Architecture

View file

@ -10,13 +10,6 @@ A minimal Android Matrix chat client with a terminal aesthetic — dark backgrou
- Text-only chat (no images, no avatars) - Text-only chat (no images, no avatars)
- Real-time sync via Matrix `/sync` long-polling - Real-time sync via Matrix `/sync` long-polling
- Persistent session (survives app restart) - 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 ## Screenshots / UI description
@ -124,23 +117,13 @@ app/src/main/java/com/ltadeu6/matrix/
├── matrix/ ├── matrix/
│ ├── MatrixApi.kt — Retrofit interface (Matrix REST v3) │ ├── MatrixApi.kt — Retrofit interface (Matrix REST v3)
│ ├── MatrixModels.kt — serialization data classes + local models │ ├── MatrixModels.kt — serialization data classes + local models
│ ├── MatrixSession.kt — singleton: auth, sync loop, state flows │ └── 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/ └── 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<SyncState>` that all screens observe. The background sync loop continuously updates it via `/sync` long-polling. **State flow**: `MatrixSession` owns a `syncState: StateFlow<SyncState>` 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 ## Dependencies
| Library | Purpose | | Library | Purpose |
@ -152,10 +135,10 @@ When the app is in the foreground or background, notifications are fired directl
| retrofit2-kotlinx-serialization-converter | Retrofit ↔ kotlinx bridge | | retrofit2-kotlinx-serialization-converter | Retrofit ↔ kotlinx bridge |
| kotlinx.coroutines | Async / sync loop | | kotlinx.coroutines | Async / sync loop |
| androidx.browser (Custom Tabs) | SSO browser login | | androidx.browser (Custom Tabs) | SSO browser login |
| libolm (native) | E2EE: Olm/Megolm encryption |
| Firebase Messaging | FCM push notifications |
## Known limitations ## Known limitations
- No E2E encryption (plain-text transport only; suitable for unencrypted rooms)
- No push notifications
- No media/file messages (text only) - No media/file messages (text only)
- Room list is not paginated (loads whatever `/sync` returns on first sync) - Room list is not paginated (loads whatever `/sync` returns on first sync)

View file

@ -14,19 +14,8 @@ android {
applicationId = "com.ltadeu6.matrix" applicationId = "com.ltadeu6.matrix"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 2 versionCode = 1
versionName = "0.1.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") ?: "")
keyAlias = "release"
storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") ?: ""
keyPassword = System.getenv("ANDROID_KEY_PASSWORD") ?: ""
}
} }
buildTypes { buildTypes {
@ -36,7 +25,6 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
signingConfig = signingConfigs.getByName("release")
} }
} }
@ -51,7 +39,6 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
packaging { packaging {
@ -81,5 +68,5 @@ dependencies {
implementation(platform(libs.firebase.bom)) implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging) implementation(libs.firebase.messaging)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
implementation(libs.okhttp.logging) debugImplementation(libs.okhttp.logging)
} }

View file

@ -25,7 +25,7 @@ import android.util.Log
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import com.ltadeu6.matrix.BuildConfig import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -1331,16 +1331,12 @@ class MatrixSession private constructor(private val appContext: Context) {
} }
private fun buildApi(homeserver: String) { private fun buildApi(homeserver: String) {
val clientBuilder = OkHttpClient.Builder() val logging = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
if (BuildConfig.DEBUG) { .addInterceptor(logging)
val logging = okhttp3.logging.HttpLoggingInterceptor().apply { .build()
level = okhttp3.logging.HttpLoggingInterceptor.Level.BODY
}
clientBuilder.addInterceptor(logging)
}
val client = clientBuilder.build()
api = Retrofit.Builder() api = Retrofit.Builder()
.baseUrl(homeserver) .baseUrl(homeserver)
.client(client) .client(client)
@ -1359,7 +1355,8 @@ class MatrixSession private constructor(private val appContext: Context) {
companion object { companion object {
private const val TAG = "MatrixCrypto" private const val TAG = "MatrixCrypto"
val PUSH_GATEWAY_URL: String get() = BuildConfig.PUSH_GATEWAY_URL /** Cloudflare Worker push gateway URL. */
const val PUSH_GATEWAY_URL = "https://matrix-push-gateway.ltadeu6.workers.dev"
@Volatile private var instance: MatrixSession? = null @Volatile private var instance: MatrixSession? = null
fun getInstance(context: Context): MatrixSession = fun getInstance(context: Context): MatrixSession =