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>
This commit is contained in:
ltadeu6 2026-05-25 16:21:07 -03:00
parent 5791742c95
commit d4b37437a3
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D
3 changed files with 53 additions and 19 deletions

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

@ -18,6 +18,16 @@ android {
versionName = "0.1.0" 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 { buildTypes {
release { release {
isMinifyEnabled = true isMinifyEnabled = true
@ -25,6 +35,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
) )
signingConfig = signingConfigs.getByName("release")
} }
} }
@ -39,6 +50,7 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
packaging { packaging {
@ -68,5 +80,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,8 @@ 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 okhttp3.Interceptor
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 +1332,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)