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)
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

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)
- 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<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
| 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)

View file

@ -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)
}

View file

@ -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 =