Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
4 changed files with 27 additions and 74 deletions
22
CLAUDE.md
22
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)
|
# 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
|
||||||
|
|
|
||||||
43
README.md
43
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -110,37 +103,27 @@ Enter your full Matrix username (`@user:homeserver.tld`) and password directly.
|
||||||
|
|
||||||
```
|
```
|
||||||
app/src/main/java/com/ltadeu6/matrix/
|
app/src/main/java/com/ltadeu6/matrix/
|
||||||
├── MainActivity.kt — single Activity; handles SSO deep link
|
├── MainActivity.kt — single Activity; handles SSO deep link
|
||||||
├── navigation/
|
├── navigation/
|
||||||
│ └── AppNavigation.kt — Compose NavHost (Login → Rooms → Chat)
|
│ └── AppNavigation.kt — Compose NavHost (Login → Rooms → Chat)
|
||||||
├── auth/
|
├── auth/
|
||||||
│ ├── LoginScreen.kt — login UI
|
│ ├── LoginScreen.kt — login UI
|
||||||
│ └── LoginViewModel.kt — SSO URL builder, login calls
|
│ └── LoginViewModel.kt — SSO URL builder, login calls
|
||||||
├── rooms/
|
├── rooms/
|
||||||
│ └── RoomListScreen.kt — joined rooms list
|
│ └── RoomListScreen.kt — joined rooms list
|
||||||
├── chat/
|
├── chat/
|
||||||
│ ├── ChatScreen.kt — message list + input bar
|
│ ├── ChatScreen.kt — message list + input bar
|
||||||
│ └── ChatViewModel.kt — sends messages, maps sync state
|
│ └── ChatViewModel.kt — sends messages, maps sync state
|
||||||
├── 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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue