Compare commits

...

4 commits

Author SHA1 Message Date
ltadeu6
1d99440b01
Update CLAUDE.md with release build and secrets setup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:06:52 -03:00
ltadeu6
e4e960024d
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 <noreply@anthropic.com>
2026-05-25 17:04:42 -03:00
ltadeu6
863cbb6b79
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 <noreply@anthropic.com>
2026-05-25 16:34:24 -03:00
ltadeu6
d4b37437a3
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 <noreply@anthropic.com>
2026-05-25 16:21:07 -03:00
4 changed files with 74 additions and 27 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 # Build debug APK (inside nix dev shell)
./gradlew assembleDebug nix develop --command gradle --no-daemon assembleDebug
# Build release APK # Build release APK — requires android-signing-env (sets signing vars + PUSH_GATEWAY_URL)
./gradlew assembleRelease android-signing-env nix develop --command gradle --no-daemon assembleRelease
# Install on connected device/emulator # Install on connected device/emulator
./gradlew installDebug ./gradlew installDebug
@ -24,6 +24,20 @@ gradle wrapper --gradle-version 8.9
./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,6 +10,13 @@ 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
@ -117,13 +124,23 @@ 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 |
@ -135,10 +152,10 @@ app/src/main/java/com/ltadeu6/matrix/
| 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,8 +14,19 @@ android {
applicationId = "com.ltadeu6.matrix" applicationId = "com.ltadeu6.matrix"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 2
versionName = "0.1.0" 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 { buildTypes {
@ -25,6 +36,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
signingConfig = signingConfigs.getByName("release")
} }
} }
@ -39,6 +51,7 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
packaging { packaging {
@ -68,5 +81,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)
debugImplementation(libs.okhttp.logging) implementation(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 okhttp3.logging.HttpLoggingInterceptor import com.ltadeu6.matrix.BuildConfig
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,12 +1331,16 @@ class MatrixSession private constructor(private val appContext: Context) {
} }
private fun buildApi(homeserver: String) { private fun buildApi(homeserver: String) {
val logging = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } val clientBuilder = OkHttpClient.Builder()
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
.addInterceptor(logging) if (BuildConfig.DEBUG) {
.build() val logging = okhttp3.logging.HttpLoggingInterceptor().apply {
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)
@ -1355,8 +1359,7 @@ class MatrixSession private constructor(private val appContext: Context) {
companion object { companion object {
private const val TAG = "MatrixCrypto" private const val TAG = "MatrixCrypto"
/** Cloudflare Worker push gateway URL. */ val PUSH_GATEWAY_URL: String get() = BuildConfig.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 =