diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..6541566
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: gradle
+ directory: /
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 3
diff --git a/.github/readme/google-play-badge.png b/.github/readme/google-play-badge.png
new file mode 100644
index 0000000..c77b746
Binary files /dev/null and b/.github/readme/google-play-badge.png differ
diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml
index 07f752e..5dc3d99 100644
--- a/.github/workflows/assemble.yml
+++ b/.github/workflows/assemble.yml
@@ -17,19 +17,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- - uses: actions/cache@v2
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
-
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
+ - uses: gradle/gradle-build-action@v2
- name: Assemble debug variant
run: ./gradlew assembleDebug --no-daemon
diff --git a/.github/workflows/check_size.yml b/.github/workflows/check_size.yml
new file mode 100644
index 0000000..1a855e0
--- /dev/null
+++ b/.github/workflows/check_size.yml
@@ -0,0 +1,45 @@
+name: Check Size
+
+on:
+ pull_request:
+
+jobs:
+ check-size:
+ name: Check Size
+ runs-on: ubuntu-latest
+
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-java@v2
+ with:
+ distribution: 'adopt'
+ java-version: '11'
+ - uses: gradle/gradle-build-action@v2
+
+ - name: Fetch bundletool
+ run: |
+ curl -s -L https://github.com/google/bundletool/releases/download/1.9.0/bundletool-all-1.9.0.jar --create-dirs -o bin/bundletool.jar
+ chmod +x bin/bundletool.jar
+ echo "#!/bin/bash" >> bin/bundletool
+ echo 'java -jar $(dirname "$0")/bundletool.jar "$@"' >> bin/bundletool
+ chmod +x bin/bundletool
+ echo "$(pwd)/bin" >> $GITHUB_PATH
+
+ - name: Save Size
+ env:
+ PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ mkdir -p ./apk_size
+ echo $(./tools/check-size.sh | tail -1 | cut -d ',' -f2-) > ./apk_size/size.txt
+ echo $PULL_REQUEST_NUMBER > ./apk_size/pr_number.txt
+ - uses: actions/upload-artifact@v3
+ with:
+ name: apk-size
+ path: |
+ apk_size/size.txt
+ apk_size/pr_number.txt
+ retention-days: 5
diff --git a/.github/workflows/comment_size.yml b/.github/workflows/comment_size.yml
new file mode 100644
index 0000000..288b98c
--- /dev/null
+++ b/.github/workflows/comment_size.yml
@@ -0,0 +1,44 @@
+name: Comment APK Size
+
+on:
+ workflow_run:
+ workflows: [ "Check Size" ]
+ types:
+ - completed
+
+jobs:
+ comment-size:
+ name: Comment Size
+ runs-on: ubuntu-latest
+ if: >
+ ${{ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion == 'success' }}
+
+ steps:
+ - uses: dawidd6/action-download-artifact@v2
+ with:
+ name: apk-size
+ workflow: ${{ github.event.workflow_run.workflow_id }}
+
+ - name: Check release size
+ run: |
+ ls -R
+ echo "::set-output name=APK_SIZE::$(cat size.txt)"
+ echo "::set-output name=PR_NUMBER::$(cat pr_number.txt)"
+ id: size
+
+ - name: Find Comment
+ uses: peter-evans/find-comment@v1
+ id: fc
+ with:
+ issue-number: ${{ steps.size.outputs.PR_NUMBER }}
+ comment-author: 'github-actions[bot]'
+ body-includes: APK Size
+ - name: Publish size to PR
+ uses: peter-evans/create-or-update-comment@v1
+ with:
+ comment-id: ${{ steps.fc.outputs.comment-id }}
+ issue-number: ${{ steps.size.outputs.PR_NUMBER }}
+ body: |
+ APK Size: ${{ steps.size.outputs.APK_SIZE }}
+ edit-mode: replace
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 02289f3..d84ef48 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,29 +17,26 @@ jobs:
steps:
- uses: actions/checkout@v2
- - uses: actions/cache@v2
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
-
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
+ - uses: gradle/gradle-build-action@v2
+
+ - name: Create pip requirements
+ run: |
+ echo "matrix-synapse==v1.60.0" > requirements.txt
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
+ cache: 'pip'
- name: Start synapse server
run: |
- pip install matrix-synapse
- curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
+ pip install -r requirements.txt
+ curl -sL https://gist.githubusercontent.com/ouchadam/e3ad09ec382bd91a66d88ab575ea7c31/raw/run.sh \
| bash -s -- --no-rate-limit
- name: Run all unit tests
diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml
new file mode 100644
index 0000000..50143be
--- /dev/null
+++ b/.github/workflows/update-gradle-wrapper.yml
@@ -0,0 +1,15 @@
+name: Update Gradle Wrapper
+
+on:
+ schedule:
+ - cron: "0 0 * * *"
+
+jobs:
+ update-gradle-wrapper:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Update Gradle Wrapper
+ uses: gradle-update/update-gradle-wrapper-action@v1
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..bd2a505
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a3911a3..b94975e 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,18 @@
-# SmallTalk [![Assemble](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml/badge.svg)](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml) [![codecov](https://codecov.io/gh/ouchadam/small-talk/branch/main/graph/badge.svg?token=ETFSLZ9FCI)](https://codecov.io/gh/ouchadam/small-talk) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
+# SmallTalk [![Assemble](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml/badge.svg)](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml) [![codecov](https://codecov.io/gh/ouchadam/small-talk/branch/main/graph/badge.svg?token=ETFSLZ9FCI)](https://codecov.io/gh/ouchadam/small-talk) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/github/v/release/ouchadam/small-talk?include_prereleases) ![](https://img.shields.io/badge/%5Bmatrix%5D%20-%23small--talk%3Aiswell.cool-blueviolet)
`SmallTalk` is a minimal, modern, friends and family focused Android messenger. Heavily inspired by Whatsapp and Signal, powered by Matrix.
+
![header](https://github.com/ouchadam/small-talk/blob/main/.github/readme/header.png?raw=true)
+[](https://play.google.com/store/apps/details?id=app.dapk.st)
----
+
+
+
-Project mantra
-- Tiny app size - currently 1.72mb~ when provided via app bundle.
+
+### Project mantra
+- Tiny app size - currently 1.80mb~ when provided via app bundle.
- Focused on reliability and stability.
- Bare-bones feature set.
@@ -15,16 +20,17 @@ Project mantra
---
-#### Feature list
+### Feature list
-- Login with username/password (home servers must serve `${domain}.well-known/matrix/client`)
+- Login with Matrix ID/Password
- Combined Room and DM interface
- End to end encryption
- Message bubbles, supporting text, replies and edits
- Push notifications (DMs always notify, Rooms notify once)
- Importing of E2E room keys from Element clients
+- [UnifiedPush](https://unifiedpush.org/)
-#### Planned
+### Planned
- Device verification (technically supported but has no UI)
- Invitations (technically supported but has no UI)
@@ -47,4 +53,8 @@ Project mantra
- Greenfield matrix SDK implementation, focus on separation, testability and parallelisation.
- Heavily optimised build script, clean _cacheless_ builds are sub 10 seconds with a warmed up gradle daemon.
- Avoids code generation where possible in favour of build speed, this mainly means manual DI.
-- A pure kotlin test harness to allow for critical flow assertions [Smoke Tests](https://github.com/ouchadam/small-talk/blob/main/test-harness/src/test/kotlin/SmokeTest.kt), currently Linux x86-64 only.
+- A pure kotlin test harness to allow for critical flow assertions [Smoke Tests](https://github.com/ouchadam/small-talk/blob/main/test-harness/src/test/kotlin/SmokeTest.kt), currently Linux x86-64 only.
+
+---
+
+#### Join the conversation @ https://matrix.to/#/#small-talk:iswell.cool
diff --git a/app/build.gradle b/app/build.gradle
index 5509a8a..5ed4c0c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,9 +10,20 @@ android {
ndkVersion "25.0.8141415"
defaultConfig {
applicationId "app.dapk.st"
- versionCode 2
- versionName "0.0.1-alpha1"
- resConfigs "en"
+ def versionJson = new groovy.json.JsonSlurper().parseText(rootProject.file('version.json').text)
+ versionCode versionJson.code
+ versionName versionJson.name
+
+ if (isDebugBuild) {
+ resConfigs "en", "xxhdpi"
+ variantFilter { variant ->
+ if (variant.buildType.name == "release") {
+ setIgnore(true)
+ }
+ }
+ } else {
+ resConfigs "en"
+ }
}
bundle {
@@ -23,6 +34,7 @@ android {
buildTypes {
debug {
+ versionNameSuffix = " [debug]"
matchingFallbacks = ['release']
signingConfig.storeFile rootProject.file("tools/debug.keystore")
}
@@ -33,16 +45,24 @@ android {
'proguard/app.pro',
"proguard/serializationx.pro",
"proguard/olm.pro"
+
+ // actual releases are signed with a different config
signingConfig = buildTypes.debug.signingConfig
}
}
+ compileOptions {
+ coreLibraryDesugaringEnabled true
+ }
+
packagingOptions {
resources.excludes += "DebugProbesKt.bin"
}
}
dependencies {
+ coreLibraryDesugaring Dependencies.google.jdkLibs
+
implementation project(":features:home")
implementation project(":features:directory")
implementation project(":features:login")
@@ -51,8 +71,10 @@ dependencies {
implementation project(":features:messenger")
implementation project(":features:profile")
implementation project(":features:navigator")
+ implementation project(":features:share-entry")
implementation project(':domains:store')
+ implementation project(":domains:android:compose-core")
implementation project(":domains:android:core")
implementation project(":domains:android:tracking")
implementation project(":domains:android:push")
@@ -79,5 +101,5 @@ dependencies {
implementation Dependencies.mavenCentral.matrixOlm
implementation Dependencies.mavenCentral.kotlinSerializationJson
- debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
+ debugImplementation Dependencies.mavenCentral.leakCanary
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fe65c3e..6e493a2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,11 @@
+
+
+
diff --git a/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt b/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt
index 1ec1edb..8db7750 100644
--- a/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt
+++ b/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt
@@ -33,7 +33,7 @@ internal class SharedPreferencesDelegate(
override suspend fun clear() {
coroutineDispatchers.withIoContext {
- preferences.edit().clear().apply()
+ preferences.edit().clear().commit()
}
}
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt
index a2de52e..0590253 100644
--- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt
+++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt
@@ -1,26 +1,29 @@
package app.dapk.st
import android.app.Application
+import android.content.Intent
import android.util.Log
import app.dapk.st.core.CoreAndroidModule
import app.dapk.st.core.ModuleProvider
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.attachAppLogger
+import app.dapk.st.core.extensions.ResettableUnsafeLazy
import app.dapk.st.core.extensions.Scope
-import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.directory.DirectoryModule
-import app.dapk.st.messenger.MessengerModule
+import app.dapk.st.domain.StoreModule
import app.dapk.st.graph.AppModule
-import app.dapk.st.graph.FeatureModules
import app.dapk.st.home.HomeModule
import app.dapk.st.login.LoginModule
+import app.dapk.st.messenger.MessengerModule
import app.dapk.st.notifications.NotificationsModule
import app.dapk.st.profile.ProfileModule
+import app.dapk.st.push.firebase.FirebasePushService
+import app.dapk.st.push.PushModule
import app.dapk.st.settings.SettingsModule
+import app.dapk.st.share.ShareEntryModule
import app.dapk.st.work.TaskRunnerModule
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.cancel
import kotlin.reflect.KClass
class SmallTalkApplication : Application(), ModuleProvider {
@@ -28,8 +31,10 @@ class SmallTalkApplication : Application(), ModuleProvider {
private val appLogger: (String, String) -> Unit = { tag, message -> _appLogger?.invoke(tag, message) }
private var _appLogger: ((String, String) -> Unit)? = null
- private val appModule: AppModule by unsafeLazy { AppModule(this, appLogger) }
- private val featureModules: FeatureModules by unsafeLazy { appModule.featureModules }
+ private val lazyAppModule = ResettableUnsafeLazy { AppModule(this, appLogger) }
+ private val lazyFeatureModules = ResettableUnsafeLazy { appModule.featureModules }
+ private val appModule by lazyAppModule
+ private val featureModules by lazyFeatureModules
private val applicationScope = Scope(Dispatchers.IO)
override fun onCreate() {
@@ -40,15 +45,17 @@ class SmallTalkApplication : Application(), ModuleProvider {
val logger: (String, String) -> Unit = { tag, message ->
Log.e(tag, message)
- GlobalScope.launch {
- eventLogStore.insert(tag, message)
- }
+ applicationScope.launch { eventLogStore.insert(tag, message) }
}
attachAppLogger(logger)
_appLogger = logger
+ onApplicationLaunch(notificationsModule, storeModule)
+ }
+
+ private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
applicationScope.launch {
- notificationsModule.firebasePushTokenUseCase().registerCurrentToken()
+ featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
storeModule.localEchoStore.preload()
}
@@ -67,10 +74,24 @@ class SmallTalkApplication : Application(), ModuleProvider {
SettingsModule::class -> featureModules.settingsModule
ProfileModule::class -> featureModules.profileModule
NotificationsModule::class -> featureModules.notificationsModule
+ PushModule::class -> featureModules.pushModule
MessengerModule::class -> featureModules.messengerModule
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
CoreAndroidModule::class -> appModule.coreAndroidModule
+ ShareEntryModule::class -> featureModules.shareEntryModule
else -> throw IllegalArgumentException("Unknown: $klass")
} as T
}
+
+ override fun reset() {
+ featureModules.pushModule.pushTokenRegistrar().unregister()
+ appModule.coroutineDispatchers.io.cancel()
+ applicationScope.cancel()
+ lazyAppModule.reset()
+ lazyFeatureModules.reset()
+
+ val notificationsModule = featureModules.notificationsModule
+ val storeModule = appModule.storeModule.value
+ onApplicationLaunch(notificationsModule, storeModule)
+ }
}
diff --git a/app/src/main/kotlin/app/dapk/st/graph/AndroidBase64.kt b/app/src/main/kotlin/app/dapk/st/graph/AndroidBase64.kt
new file mode 100644
index 0000000..dad547f
--- /dev/null
+++ b/app/src/main/kotlin/app/dapk/st/graph/AndroidBase64.kt
@@ -0,0 +1,13 @@
+package app.dapk.st.graph
+
+import app.dapk.st.core.Base64
+
+class AndroidBase64 : Base64 {
+ override fun encode(input: ByteArray): String {
+ return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT)
+ }
+
+ override fun decode(input: String): ByteArray {
+ return android.util.Base64.decode(input, android.util.Base64.DEFAULT)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt
index a5868be..24e0232 100644
--- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt
+++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt
@@ -1,16 +1,17 @@
package app.dapk.st.graph
-import android.app.Activity
import android.app.Application
+import android.app.PendingIntent
+import android.content.ContentResolver
import android.content.Context
import android.content.Intent
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.os.Build
import app.dapk.db.DapkDb
import app.dapk.st.BuildConfig
import app.dapk.st.SharedPreferencesDelegate
-import app.dapk.st.core.BuildMeta
-import app.dapk.st.core.CoreAndroidModule
-import app.dapk.st.core.CoroutineDispatchers
-import app.dapk.st.core.SingletonFlows
+import app.dapk.st.core.*
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.directory.DirectoryModule
@@ -20,8 +21,6 @@ import app.dapk.st.home.MainActivity
import app.dapk.st.imageloader.ImageLoaderModule
import app.dapk.st.login.LoginModule
import app.dapk.st.matrix.MatrixClient
-import app.dapk.st.matrix.MatrixTaskRunner
-import app.dapk.st.matrix.MatrixTaskRunner.MatrixTask
import app.dapk.st.matrix.auth.authService
import app.dapk.st.matrix.auth.installAuthService
import app.dapk.st.matrix.common.*
@@ -34,7 +33,11 @@ import app.dapk.st.matrix.device.installEncryptionService
import app.dapk.st.matrix.device.internal.ApiMessage
import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
-import app.dapk.st.matrix.message.*
+import app.dapk.st.matrix.message.MessageEncrypter
+import app.dapk.st.matrix.message.MessageService
+import app.dapk.st.matrix.message.installMessageService
+import app.dapk.st.matrix.message.internal.ImageContentReader
+import app.dapk.st.matrix.message.messageService
import app.dapk.st.matrix.push.installPushService
import app.dapk.st.matrix.push.pushService
import app.dapk.st.matrix.room.*
@@ -44,6 +47,8 @@ import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
import app.dapk.st.messenger.MessengerActivity
import app.dapk.st.messenger.MessengerModule
import app.dapk.st.navigator.IntentFactory
+import app.dapk.st.navigator.MessageAttachment
+import app.dapk.st.notifications.MatrixPushHandler
import app.dapk.st.notifications.NotificationsModule
import app.dapk.st.olm.DeviceKeyFactory
import app.dapk.st.olm.OlmPersistenceWrapper
@@ -51,18 +56,17 @@ import app.dapk.st.olm.OlmWrapper
import app.dapk.st.profile.ProfileModule
import app.dapk.st.push.PushModule
import app.dapk.st.settings.SettingsModule
+import app.dapk.st.share.ShareEntryModule
import app.dapk.st.tracking.TrackingModule
-import app.dapk.st.work.TaskRunner
import app.dapk.st.work.TaskRunnerModule
import app.dapk.st.work.WorkModule
-import app.dapk.st.work.WorkScheduler
import com.squareup.sqldelight.android.AndroidSqliteDriver
import kotlinx.coroutines.Dispatchers
import java.time.Clock
internal class AppModule(context: Application, logger: MatrixLogger) {
- private val buildMeta = BuildMeta(BuildConfig.VERSION_NAME)
+ private val buildMeta = BuildMeta(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
private val trackingModule by unsafeLazy {
TrackingModule(
isCrashTrackingEnabled = !BuildConfig.DEBUG
@@ -71,8 +75,8 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
private val database = DapkDb(driver)
-
- private val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
+ private val clock = Clock.systemUTC()
+ val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
val storeModule = unsafeLazy {
StoreModule(
@@ -80,31 +84,41 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
preferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", coroutineDispatchers),
errorTracker = trackingModule.errorTracker,
credentialPreferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-credentials-preferences", coroutineDispatchers),
- databaseDropper = { includeCryptoAccount ->
- val cursor = driver.executeQuery(
- identifier = null,
- sql = "SELECT name FROM sqlite_master WHERE type = 'table' ${if (includeCryptoAccount) "" else "AND name != 'dbCryptoAccount'"}",
- parameters = 0
- )
- while (cursor.next()) {
- cursor.getString(0)?.let {
- driver.execute(null, "DELETE FROM $it", 0)
- }
- }
- },
+ databaseDropper = DefaultDatabaseDropper(coroutineDispatchers, driver),
coroutineDispatchers = coroutineDispatchers
)
}
private val workModule = WorkModule(context)
private val imageLoaderModule = ImageLoaderModule(context)
- private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers)
- val domainModules = DomainModules(matrixModules, trackingModule.errorTracker)
+ private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver)
+ val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers)
val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory {
- override fun home(activity: Activity) = Intent(activity, MainActivity::class.java)
- override fun messenger(activity: Activity, roomId: RoomId) = MessengerActivity.newInstance(activity, roomId)
- override fun messengerShortcut(activity: Activity, roomId: RoomId) = MessengerActivity.newShortcutInstance(activity, roomId)
+ override fun notificationOpenApp(context: Context) = PendingIntent.getActivity(
+ context,
+ 1000,
+ home(context)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ override fun notificationOpenMessage(context: Context, roomId: RoomId) = PendingIntent.getActivity(
+ context,
+ roomId.hashCode(),
+ messenger(context, roomId)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ override fun home(context: Context) = Intent(context, MainActivity::class.java)
+ override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId)
+ override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId)
+ override fun messengerAttachments(context: Context, roomId: RoomId, attachments: List) = MessengerActivity.newMessageAttachment(
+ context,
+ roomId,
+ attachments
+ )
})
val featureModules = FeatureModules(
@@ -112,10 +126,12 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
matrixModules,
domainModules,
trackingModule,
+ coreAndroidModule,
imageLoaderModule,
context,
buildMeta,
coroutineDispatchers,
+ clock,
)
}
@@ -124,10 +140,12 @@ internal class FeatureModules internal constructor(
private val matrixModules: MatrixModules,
private val domainModules: DomainModules,
private val trackingModule: TrackingModule,
+ private val coreAndroidModule: CoreAndroidModule,
imageLoaderModule: ImageLoaderModule,
context: Context,
buildMeta: BuildMeta,
coroutineDispatchers: CoroutineDispatchers,
+ clock: Clock,
) {
val directoryModule by unsafeLazy {
@@ -155,12 +173,14 @@ internal class FeatureModules internal constructor(
matrixModules.room,
storeModule.value.credentialsStore(),
storeModule.value.roomStore(),
+ clock
)
}
- val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile) }
+ val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, buildMeta) }
val settingsModule by unsafeLazy {
SettingsModule(
storeModule.value,
+ pushModule,
matrixModules.crypto,
matrixModules.sync,
context.contentResolver,
@@ -168,19 +188,26 @@ internal class FeatureModules internal constructor(
coroutineDispatchers
)
}
- val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync) }
+ val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) }
val notificationsModule by unsafeLazy {
NotificationsModule(
- matrixModules.push,
- matrixModules.sync,
- storeModule.value.credentialsStore(),
- domainModules.pushModule.registerFirebasePushTokenUseCase(),
imageLoaderModule.iconLoader(),
storeModule.value.roomStore(),
context,
+ intentFactory = coreAndroidModule.intentFactory(),
+ dispatchers = coroutineDispatchers,
+ deviceMeta = DeviceMeta(Build.VERSION.SDK_INT)
)
}
+ val shareEntryModule by unsafeLazy {
+ ShareEntryModule(matrixModules.sync, matrixModules.room)
+ }
+
+ val pushModule by unsafeLazy {
+ domainModules.pushModule
+ }
+
}
internal class MatrixModules(
@@ -189,6 +216,7 @@ internal class MatrixModules(
private val workModule: WorkModule,
private val logger: MatrixLogger,
private val coroutineDispatchers: CoroutineDispatchers,
+ private val contentResolver: ContentResolver,
) {
val matrix by unsafeLazy {
@@ -205,7 +233,8 @@ internal class MatrixModules(
installAuthService(credentialsStore)
installEncryptionService(store.knownDevicesStore())
- val olmAccountStore = OlmPersistenceWrapper(store.olmStore())
+ val base64 = AndroidBase64()
+ val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64)
val singletonFlows = SingletonFlows(coroutineDispatchers)
val olm = OlmWrapper(
olmStore = olmAccountStore,
@@ -225,13 +254,16 @@ internal class MatrixModules(
services.roomService().joinedMembers(it).map { it.userId }
}
},
+ base64 = base64,
coroutineDispatchers = coroutineDispatchers,
)
- installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler())) { serviceProvider ->
+ val imageContentReader = AndroidImageContentReader(contentResolver)
+ installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), imageContentReader) { serviceProvider ->
MessageEncrypter { message ->
val result = serviceProvider.cryptoService().encrypt(
roomId = when (message) {
is MessageService.Message.TextMessage -> message.roomId
+ is MessageService.Message.ImageMessage -> message.roomId
},
credentials = credentialsStore.credentials()!!,
when (message) {
@@ -246,6 +278,8 @@ internal class MatrixModules(
)
)
)
+
+ is MessageService.Message.ImageMessage -> TODO()
}
)
@@ -283,6 +317,14 @@ internal class MatrixModules(
store.roomStore(),
store.syncStore(),
store.filterStore(),
+ deviceNotifier = { services ->
+ val encryption = services.deviceService()
+ val crypto = services.cryptoService()
+ DeviceNotifier { userIds, syncToken ->
+ encryption.updateStaleDevices(userIds)
+ crypto.updateOlmSession(userIds, syncToken)
+ }
+ },
messageDecrypter = { serviceProvider ->
val cryptoService = serviceProvider.cryptoService()
MessageDecrypter {
@@ -296,9 +338,9 @@ internal class MatrixModules(
}
},
verificationHandler = { services ->
- logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
val cryptoService = services.cryptoService()
VerificationHandler { apiEvent ->
+ logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
cryptoService.onVerificationEvent(
when (apiEvent) {
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
@@ -308,12 +350,14 @@ internal class MatrixModules(
apiEvent.content.methods,
apiEvent.content.timestampPosix,
)
+
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
apiEvent.sender,
apiEvent.content.fromDevice,
apiEvent.content.transactionId,
apiEvent.content.methods,
)
+
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
apiEvent.sender,
apiEvent.content.fromDevice,
@@ -324,6 +368,7 @@ internal class MatrixModules(
apiEvent.content.short,
apiEvent.content.transactionId,
)
+
is ApiToDeviceEvent.VerificationCancel -> TODO()
is ApiToDeviceEvent.VerificationAccept -> TODO()
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
@@ -331,6 +376,7 @@ internal class MatrixModules(
apiEvent.content.transactionId,
apiEvent.content.key
)
+
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
apiEvent.sender,
apiEvent.content.transactionId,
@@ -341,14 +387,6 @@ internal class MatrixModules(
)
}
},
- deviceNotifier = { services ->
- val encryption = services.deviceService()
- val crypto = services.cryptoService()
- DeviceNotifier { userIds, syncToken ->
- encryption.updateStaleDevices(userIds)
- crypto.updateOlmSession(userIds, syncToken)
- }
- },
oneTimeKeyProducer = { services ->
val cryptoService = services.cryptoService()
MaybeCreateMoreKeys {
@@ -359,6 +397,7 @@ internal class MatrixModules(
val roomService = services.roomService()
object : RoomMembersService {
override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds)
+ override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId)
override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members)
}
},
@@ -367,8 +406,6 @@ internal class MatrixModules(
)
installPushService(credentialsStore)
-
-
}
}
}
@@ -385,32 +422,49 @@ internal class MatrixModules(
internal class DomainModules(
private val matrixModules: MatrixModules,
private val errorTracker: ErrorTracker,
+ private val workModule: WorkModule,
+ private val storeModule: Lazy,
+ private val context: Application,
+ private val dispatchers: CoroutineDispatchers,
) {
- val pushModule by unsafeLazy { PushModule(matrixModules.push, errorTracker) }
- val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run)) }
-}
-
-class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : BackgroundScheduler {
- override fun schedule(key: String, task: BackgroundScheduler.Task) {
- workScheduler.schedule(
- WorkScheduler.WorkTask(
- jobId = 1,
- type = task.type,
- jsonPayload = task.jsonPayload,
- )
+ val pushModule by unsafeLazy {
+ val store = storeModule.value
+ val pushHandler = MatrixPushHandler(
+ workScheduler = workModule.workScheduler(),
+ credentialsStore = store.credentialsStore(),
+ matrixModules.sync,
+ store.roomStore(),
+ )
+ PushModule(
+ errorTracker,
+ pushHandler,
+ context,
+ dispatchers,
+ SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", dispatchers)
)
}
+ val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
}
-class TaskRunnerAdapter(private val matrixTaskRunner: suspend (MatrixTask) -> MatrixTaskRunner.TaskResult) : TaskRunner {
+internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
+ override fun read(uri: String): ImageContentReader.ImageContent {
+ val androidUri = Uri.parse(uri)
+ val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
- override suspend fun run(tasks: List): List {
- return tasks.map {
- when (val result = matrixTaskRunner(MatrixTask(it.task.type, it.task.jsonPayload))) {
- is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
- MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
- }
- }
+ val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+ BitmapFactory.decodeStream(fileStream, null, options)
+
+ return contentResolver.openInputStream(androidUri)?.use { stream ->
+ val output = stream.readBytes()
+ ImageContentReader.ImageContent(
+ height = options.outHeight,
+ width = options.outWidth,
+ size = output.size.toLong(),
+ mimeType = options.outMimeType,
+ fileName = androidUri.lastPathSegment ?: "file",
+ content = output
+ )
+ } ?: throw IllegalArgumentException("Could not process $uri")
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt b/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt
new file mode 100644
index 0000000..ca8bbf9
--- /dev/null
+++ b/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt
@@ -0,0 +1,37 @@
+package app.dapk.st.graph
+
+import app.dapk.st.matrix.push.PushService
+import app.dapk.st.push.PushTokenPayload
+import app.dapk.st.work.TaskRunner
+import io.ktor.client.plugins.*
+import kotlinx.serialization.json.Json
+
+class AppTaskRunner(
+ private val pushService: PushService,
+) {
+
+ suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult {
+ return when (val type = workTask.task.type) {
+ "push_token" -> {
+ runCatching {
+ val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload)
+ pushService.registerPush(payload.token, payload.gatewayUrl)
+ }.fold(
+ onSuccess = { TaskRunner.TaskResult.Success(workTask.source) },
+ onFailure = {
+ val canRetry = if (it is ClientRequestException) {
+ it.response.status.value !in (400 until 500)
+ } else {
+ true
+ }
+ TaskRunner.TaskResult.Failure(workTask.source, canRetry = canRetry)
+ }
+ )
+ }
+
+ else -> throw IllegalArgumentException("Unknown work type: $type")
+ }
+
+ }
+
+}
diff --git a/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt b/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt
new file mode 100644
index 0000000..c35db37
--- /dev/null
+++ b/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt
@@ -0,0 +1,16 @@
+package app.dapk.st.graph
+
+import app.dapk.st.matrix.message.BackgroundScheduler
+import app.dapk.st.work.WorkScheduler
+
+class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : BackgroundScheduler {
+ override fun schedule(key: String, task: BackgroundScheduler.Task) {
+ workScheduler.schedule(
+ WorkScheduler.WorkTask(
+ jobId = 1,
+ type = task.type,
+ jsonPayload = task.jsonPayload,
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/app/dapk/st/graph/DefaultDatabaseDropper.kt b/app/src/main/kotlin/app/dapk/st/graph/DefaultDatabaseDropper.kt
new file mode 100644
index 0000000..6966bfe
--- /dev/null
+++ b/app/src/main/kotlin/app/dapk/st/graph/DefaultDatabaseDropper.kt
@@ -0,0 +1,33 @@
+package app.dapk.st.graph
+
+import app.dapk.st.core.CoroutineDispatchers
+import app.dapk.st.core.withIoContext
+import app.dapk.st.domain.DatabaseDropper
+import com.squareup.sqldelight.android.AndroidSqliteDriver
+
+class DefaultDatabaseDropper(
+ private val coroutineDispatchers: CoroutineDispatchers,
+ private val driver: AndroidSqliteDriver,
+) : DatabaseDropper {
+
+ override suspend fun dropAllTables(deleteCrypto: Boolean) {
+ coroutineDispatchers.withIoContext {
+ val cursor = driver.executeQuery(
+ identifier = null,
+ sql = "SELECT name FROM sqlite_master WHERE type = 'table'",
+ parameters = 0
+ )
+ cursor.use {
+ while (cursor.next()) {
+ cursor.getString(0)?.let {
+ if (!deleteCrypto && it.startsWith("dbCrypto")) {
+ // skip
+ } else {
+ driver.execute(null, "DELETE FROM $it", 0)
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt b/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt
new file mode 100644
index 0000000..5f9f717
--- /dev/null
+++ b/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt
@@ -0,0 +1,24 @@
+package app.dapk.st.graph
+
+import app.dapk.st.matrix.MatrixTaskRunner
+import app.dapk.st.work.TaskRunner
+
+class TaskRunnerAdapter(
+ private val matrixTaskRunner: suspend (MatrixTaskRunner.MatrixTask) -> MatrixTaskRunner.TaskResult,
+ private val appTaskRunner: AppTaskRunner,
+) : TaskRunner {
+
+ override suspend fun run(tasks: List): List {
+ return tasks.map {
+ when {
+ it.task.type.startsWith("matrix") -> {
+ when (val result = matrixTaskRunner(MatrixTaskRunner.MatrixTask(it.task.type, it.task.jsonPayload))) {
+ is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
+ MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
+ }
+ }
+ else -> appTaskRunner.run(it)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
new file mode 100644
index 0000000..e727fee
--- /dev/null
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 7bcba9f..c28a7fb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,7 +9,7 @@ buildscript {
classpath Dependencies.mavenCentral.kotlinGradlePlugin
classpath Dependencies.mavenCentral.sqldelightGradlePlugin
classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin
- classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
+ classpath Dependencies.google.firebaseCrashlyticsPlugin
}
}
@@ -18,14 +18,16 @@ def launchTask = getGradle()
.getTaskRequests()
.toString()
.toLowerCase()
+def isReleaseBuild = launchTask.contains("release")
+ext.isDebugBuild = !isReleaseBuild
subprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = [
- '-Xopt-in=kotlin.contracts.ExperimentalContracts',
- '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
+ '-opt-in=kotlin.contracts.ExperimentalContracts',
+ '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
]
}
}
@@ -52,7 +54,7 @@ ext.applyLibraryPlugins = { project ->
project.apply plugin: 'kotlin-android'
}
-ext.androidSdkVersion = 31
+ext.androidSdkVersion = 32
ext.applyCommonAndroidParameters = { project ->
def android = project.android
@@ -63,14 +65,9 @@ ext.applyCommonAndroidParameters = { project ->
incremental = true
}
android.defaultConfig {
- minSdkVersion 29
+ minSdkVersion 24
targetSdkVersion androidSdkVersion
}
-
- android.buildFeatures.compose = true
- android.composeOptions {
- kotlinCompilerExtensionVersion = Dependencies.google.kotlinCompilerExtensionVersion
- }
}
ext.applyLibraryModuleOptimisations = { project ->
@@ -101,17 +98,26 @@ ext.applyCompose = { project ->
dependencies.implementation Dependencies.google.androidxComposeMaterial
dependencies.implementation Dependencies.google.androidxComposeIconsExtended
dependencies.implementation Dependencies.google.androidxActivityCompose
+
+ def android = project.android
+ android.buildFeatures.compose = true
+ android.composeOptions {
+ kotlinCompilerExtensionVersion = Dependencies.google.kotlinCompilerExtensionVersion
+ }
+}
+
+ext.applyAndroidComposeLibraryModule = { project ->
+ applyAndroidLibraryModule(project)
+ applyCompose(project)
}
ext.applyAndroidLibraryModule = { project ->
applyLibraryPlugins(project)
applyCommonAndroidParameters(project)
applyLibraryModuleOptimisations(project)
- applyCompose(project)
}
ext.applyCrashlyticsIfRelease = { project ->
- def isReleaseBuild = launchTask.contains("release")
if (isReleaseBuild) {
project.apply plugin: 'com.google.firebase.crashlytics'
project.afterEvaluate {
@@ -126,16 +132,17 @@ ext.kotlinTest = { dependencies ->
dependencies.testImplementation Dependencies.mavenCentral.kluent
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10"
- dependencies.testImplementation 'io.mockk:mockk:1.12.2'
- dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
+ dependencies.testImplementation 'io.mockk:mockk:1.12.7'
+ dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
- dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
- dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
+ dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
+ dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
}
ext.kotlinFixtures = { dependencies ->
- dependencies.testFixturesImplementation 'io.mockk:mockk:1.12.2'
+ dependencies.testFixturesImplementation 'io.mockk:mockk:1.12.7'
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
+ dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
}
ext.androidImportFixturesWorkaround = { project, fixtures ->
diff --git a/core/build.gradle b/core/build.gradle
index 9388e48..b573f32 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -4,9 +4,9 @@ plugins {
}
dependencies {
- implementation Dependencies.mavenCentral.kotlinCoroutinesCore
+ api Dependencies.mavenCentral.kotlinCoroutinesCore
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
testFixturesImplementation Dependencies.mavenCentral.kluent
- testFixturesImplementation 'io.mockk:mockk:1.12.2'
- testFixturesImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
+ testFixturesImplementation Dependencies.mavenCentral.mockk
+ testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt b/core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt
new file mode 100644
index 0000000..eaf6813
--- /dev/null
+++ b/core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt
@@ -0,0 +1,4 @@
+package app.dapk.st.core
+
+@JvmInline
+value class AndroidUri(val value: String)
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/Base64.kt b/core/src/main/kotlin/app/dapk/st/core/Base64.kt
new file mode 100644
index 0000000..3ad71f3
--- /dev/null
+++ b/core/src/main/kotlin/app/dapk/st/core/Base64.kt
@@ -0,0 +1,6 @@
+package app.dapk.st.core
+
+interface Base64 {
+ fun encode(input: ByteArray): String
+ fun decode(input: String): ByteArray
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/BuildMeta.kt b/core/src/main/kotlin/app/dapk/st/core/BuildMeta.kt
index 4e965e9..3fd09ac 100644
--- a/core/src/main/kotlin/app/dapk/st/core/BuildMeta.kt
+++ b/core/src/main/kotlin/app/dapk/st/core/BuildMeta.kt
@@ -2,4 +2,5 @@ package app.dapk.st.core
data class BuildMeta(
val versionName: String,
-)
\ No newline at end of file
+ val versionCode: Int,
+)
diff --git a/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt b/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt
index 14e8ca2..5281166 100644
--- a/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt
+++ b/core/src/main/kotlin/app/dapk/st/core/CoroutineDispatchers.kt
@@ -2,7 +2,11 @@ package app.dapk.st.core
import kotlinx.coroutines.*
-data class CoroutineDispatchers(val io: CoroutineDispatcher = Dispatchers.IO, val global: CoroutineScope = GlobalScope)
+data class CoroutineDispatchers(
+ val io: CoroutineDispatcher = Dispatchers.IO,
+ val main: CoroutineDispatcher = Dispatchers.Main,
+ val global: CoroutineScope = GlobalScope,
+)
suspend fun CoroutineDispatchers.withIoContext(
block: suspend CoroutineScope.() -> T
diff --git a/core/src/main/kotlin/app/dapk/st/core/DeviceMeta.kt b/core/src/main/kotlin/app/dapk/st/core/DeviceMeta.kt
new file mode 100644
index 0000000..28529d2
--- /dev/null
+++ b/core/src/main/kotlin/app/dapk/st/core/DeviceMeta.kt
@@ -0,0 +1,5 @@
+package app.dapk.st.core
+
+data class DeviceMeta(
+ val apiVersion: Int
+)
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/LRUCache.kt b/core/src/main/kotlin/app/dapk/st/core/LRUCache.kt
new file mode 100644
index 0000000..373d0ca
--- /dev/null
+++ b/core/src/main/kotlin/app/dapk/st/core/LRUCache.kt
@@ -0,0 +1,27 @@
+package app.dapk.st.core
+
+class LRUCache(val maxSize: Int) {
+
+ private val internalCache = object : LinkedHashMap(0, 0.75f, true) {
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean {
+ return size > maxSize
+ }
+ }
+
+ fun put(key: K, value: V) {
+ internalCache[key] = value
+ }
+
+ fun get(key: K): V? {
+ return internalCache[key]
+ }
+
+ fun getOrPut(key: K, value: () -> V): V {
+ return get(key) ?: value().also { put(key, it) }
+ }
+
+ fun size() = internalCache.size
+
+}
+
+fun LRUCache<*, *>?.isNullOrEmpty() = this == null || this.size() == 0
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/MimeType.kt b/core/src/main/kotlin/app/dapk/st/core/MimeType.kt
new file mode 100644
index 0000000..4d24bf8
--- /dev/null
+++ b/core/src/main/kotlin/app/dapk/st/core/MimeType.kt
@@ -0,0 +1,5 @@
+package app.dapk.st.core
+
+sealed interface MimeType {
+ object Image: MimeType
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/ModuleProvider.kt b/core/src/main/kotlin/app/dapk/st/core/ModuleProvider.kt
index 67f75a7..7dad034 100644
--- a/core/src/main/kotlin/app/dapk/st/core/ModuleProvider.kt
+++ b/core/src/main/kotlin/app/dapk/st/core/ModuleProvider.kt
@@ -4,7 +4,8 @@ import kotlin.reflect.KClass
interface ModuleProvider {
- fun provide(klass: KClass): T
+ fun provide(klass: KClass): T
+ fun reset()
}
interface ProvidableModule
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt b/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt
index 1d09273..64f66b8 100644
--- a/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt
+++ b/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt
@@ -31,10 +31,12 @@ class SingletonFlows(
}
}
+ @Suppress("UNCHECKED_CAST")
fun get(key: String): Flow {
return cache[key]!! as Flow
}
+ @Suppress("UNCHECKED_CAST")
suspend fun update(key: String, value: T) {
(cache[key] as? MutableSharedFlow)?.emit(value)
}
diff --git a/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt b/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt
index 5675590..1c947a7 100644
--- a/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt
+++ b/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt
@@ -21,3 +21,25 @@ inline fun Iterable.firstOrNull(predicate: (T) -> Boolean
}
fun unsafeLazy(initializer: () -> T): Lazy = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)
+
+class ResettableUnsafeLazy(private val initializer: () -> T) : Lazy {
+
+ private var _value: T? = null
+
+ override val value: T
+ get() {
+ return if (_value == null) {
+ initializer().also { _value = it }
+ } else {
+ _value!!
+ }
+ }
+
+ override fun isInitialized(): Boolean {
+ return _value != null
+ }
+
+ fun reset() {
+ _value = null
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/app/dapk/st/core/extensions/MapExtensions.kt b/core/src/main/kotlin/app/dapk/st/core/extensions/MapExtensions.kt
new file mode 100644
index 0000000..5a59d12
--- /dev/null
+++ b/core/src/main/kotlin/app/dapk/st/core/extensions/MapExtensions.kt
@@ -0,0 +1,8 @@
+package app.dapk.st.core.extensions
+
+fun Map?.containsKey(key: K) = this?.containsKey(key) ?: false
+
+fun MutableMap.clearAndPutAll(input: Map) {
+ this.clear()
+ this.putAll(input)
+}
\ No newline at end of file
diff --git a/core/src/testFixtures/kotlin/fixture/CoroutineDispatchersFixture.kt b/core/src/testFixtures/kotlin/fixture/CoroutineDispatchersFixture.kt
new file mode 100644
index 0000000..f7347c2
--- /dev/null
+++ b/core/src/testFixtures/kotlin/fixture/CoroutineDispatchersFixture.kt
@@ -0,0 +1,14 @@
+package fixture
+
+import app.dapk.st.core.CoroutineDispatchers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+
+object CoroutineDispatchersFixture {
+
+ fun aCoroutineDispatchers() = CoroutineDispatchers(
+ Dispatchers.Unconfined,
+ main = Dispatchers.Unconfined,
+ global = CoroutineScope(Dispatchers.Unconfined)
+ )
+}
\ No newline at end of file
diff --git a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt
index 127b31f..f9f3db7 100644
--- a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt
+++ b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt
@@ -1,9 +1,6 @@
package test
-import io.mockk.MockKMatcherScope
-import io.mockk.MockKVerificationScope
-import io.mockk.coJustRun
-import io.mockk.coVerifyAll
+import io.mockk.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import kotlin.coroutines.CoroutineContext
@@ -12,23 +9,30 @@ fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) {
runTest { testBody(ExpectTest(coroutineContext)) }
}
-
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
- private val expects = mutableListOf Unit>()
+ private val expects = mutableListOf Unit>>()
+ private val groups = mutableListOf Unit>()
- override fun verifyExpects() = coVerifyAll { expects.forEach { it.invoke(this@coVerifyAll) } }
-
- override fun T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit) {
- coJustRun { block(this@expectUnit) }.ignore()
- expects.add { block(this@expectUnit) }
+ override fun verifyExpects() {
+ expects.forEach { (times, block) -> coVerify(exactly = times) { block.invoke(this) } }
+ groups.forEach { coVerifyOrder { it.invoke(this) } }
}
+ override fun T.expectUnit(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) {
+ coJustRun { block(this@expectUnit) }.ignore()
+ expects.add(times to { block(this@expectUnit) })
+ }
+
+ override fun T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) {
+ groups.add { block(this@captureExpects) }
+ }
}
private fun Any.ignore() = Unit
interface ExpectTestScope : CoroutineScope {
fun verifyExpects()
- fun T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit)
+ fun T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit)
+ fun T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit)
}
\ No newline at end of file
diff --git a/core/src/testFixtures/kotlin/test/MockkExtensions.kt b/core/src/testFixtures/kotlin/test/MockkExtensions.kt
index bcddce0..2bafc94 100644
--- a/core/src/testFixtures/kotlin/test/MockkExtensions.kt
+++ b/core/src/testFixtures/kotlin/test/MockkExtensions.kt
@@ -1,6 +1,8 @@
package test
import io.mockk.*
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
inline fun T.expect(crossinline block: suspend MockKMatcherScope.(T) -> R) {
coEvery { block(this@expect) } returns mockk(relaxed = true)
@@ -16,11 +18,22 @@ fun MockKStubScope.delegateReturn() = object : Returns {
}
}
+fun MockKStubScope, B>.delegateEmit() = object : Emits {
+ override fun emits(vararg values: T) {
+ answers(ConstantAnswer(flowOf(*values)))
+ }
+}
+
+
fun returns(block: (T) -> Unit) = object : Returns {
override fun returns(value: T) = block(value)
override fun throws(value: Throwable) = throw value
}
+interface Emits {
+ fun emits(vararg values: T)
+}
+
interface Returns {
fun returns(value: T)
fun throws(value: Throwable)
diff --git a/dependencies.gradle b/dependencies.gradle
index 8b77960..8bb353c 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -10,6 +10,13 @@ ext.Dependencies.with {
}
}
+ repositories.maven {
+ url 'https://jitpack.io'
+ content {
+ includeGroup "com.github.UnifiedPush"
+ }
+ }
+
repositories.mavenCentral {
content {
includeGroupByRegex "org\\.jetbrains.*"
@@ -88,45 +95,64 @@ ext.Dependencies.with {
}
}
- def kotlinVer = "1.6.10"
+ def kotlinVer = "1.7.10"
def sqldelightVer = "1.5.3"
- def composeVer = "1.1.0"
+ def composeVer = "1.2.1"
+ def ktorVer = "2.1.0"
google = new DependenciesContainer()
google.with {
- androidGradlePlugin = "com.android.tools.build:gradle:7.1.2"
+ androidGradlePlugin = "com.android.tools.build:gradle:7.2.1"
androidxComposeUi = "androidx.compose.ui:ui:${composeVer}"
androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}"
androidxComposeMaterial = "androidx.compose.material:material:${composeVer}"
androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}"
androidxActivityCompose = "androidx.activity:activity-compose:1.4.0"
- kotlinCompilerExtensionVersion = "1.1.0-rc02"
+ kotlinCompilerExtensionVersion = "1.3.0"
+
+ firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1"
+ jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5"
}
mavenCentral = new DependenciesContainer()
mavenCentral.with {
kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}"
kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}"
- kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
- kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-RC2"
+ kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0"
+ kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
+ kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}"
sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}"
sqldelightAndroid = "com.squareup.sqldelight:android-driver:${sqldelightVer}"
sqldelightInMemory = "com.squareup.sqldelight:sqlite-driver:${sqldelightVer}"
- ktorAndroid = "io.ktor:ktor-client-android:1.6.4"
- ktorCore = "io.ktor:ktor-client-core:1.6.2"
- ktorSerialization = "io.ktor:ktor-client-serialization:1.5.0"
- ktorLogging = "io.ktor:ktor-client-logging-jvm:1.6.2"
- ktorJava = "io.ktor:ktor-client-java:1.6.2"
+ leakCanary = 'com.squareup.leakcanary:leakcanary-android:2.9.1'
+
+ ktorAndroid = "io.ktor:ktor-client-android:${ktorVer}"
+ ktorCore = "io.ktor:ktor-client-core:${ktorVer}"
+ ktorSerialization = "io.ktor:ktor-client-serialization:${ktorVer}"
+ ktorJson = "io.ktor:ktor-serialization-kotlinx-json:${ktorVer}"
+ ktorLogging = "io.ktor:ktor-client-logging-jvm:${ktorVer}"
+ ktorJava = "io.ktor:ktor-client-java:${ktorVer}"
+ ktorContentNegotiation = "io.ktor:ktor-client-content-negotiation:${ktorVer}"
+
+ coil = "io.coil-kt:coil-compose:2.2.0"
+ accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.25.1"
junit = "junit:junit:4.13.2"
kluent = "org.amshove.kluent:kluent:1.68"
+ mockk = 'io.mockk:mockk:1.12.7'
- matrixOlm = "org.matrix.android:olm-sdk:3.2.10"
+ matrixOlm = "org.matrix.android:olm-sdk:3.2.12"
}
+
+ jitPack = new DependenciesContainer()
+ jitPack.with {
+ unifiedPush = "com.github.UnifiedPush:android-connector:2.0.1"
+ }
+
}
class DependenciesContainer extends GroovyObjectSupport {
diff --git a/design-library/build.gradle b/design-library/build.gradle
index f15ea08..578435a 100644
--- a/design-library/build.gradle
+++ b/design-library/build.gradle
@@ -1,7 +1,7 @@
-applyAndroidLibraryModule(project)
+applyAndroidComposeLibraryModule(project)
dependencies {
implementation project(":core")
- implementation("io.coil-kt:coil-compose:1.4.0")
- implementation "com.google.accompanist:accompanist-systemuicontroller:0.24.1-alpha"
+ implementation Dependencies.mavenCentral.coil
+ implementation Dependencies.mavenCentral.accompanistSystemuicontroller
}
\ No newline at end of file
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Empty.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Empty.kt
new file mode 100644
index 0000000..3b13536
--- /dev/null
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Empty.kt
@@ -0,0 +1,18 @@
+package app.dapk.st.design.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@Composable
+fun GenericEmpty() {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text("Nothing to see here...")
+ }
+ }
+}
\ No newline at end of file
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt
new file mode 100644
index 0000000..3242945
--- /dev/null
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt
@@ -0,0 +1,22 @@
+package app.dapk.st.design.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@Composable
+fun GenericError(retryAction: () -> Unit) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text("Something went wrong...")
+ Button(onClick = { retryAction() }) {
+ Text("Retry")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Icon.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Icon.kt
index 8db9a78..93f9529 100644
--- a/design-library/src/main/kotlin/app/dapk/st/design/components/Icon.kt
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Icon.kt
@@ -10,12 +10,14 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
-import coil.compose.rememberImagePainter
+import coil.compose.rememberAsyncImagePainter
+import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
@OptIn(ExperimentalUnitApi::class)
@@ -25,7 +27,8 @@ fun BoxScope.CircleishAvatar(avatarUrl: String?, fallbackLabel: String, size: Dp
null -> {
val colors = SmallTalkTheme.extendedColors.getMissingImageColor(fallbackLabel)
Box(
- Modifier.align(Alignment.Center)
+ Modifier
+ .align(Alignment.Center)
.background(color = colors.first, shape = CircleShape)
.size(size),
contentAlignment = Alignment.Center
@@ -40,14 +43,16 @@ fun BoxScope.CircleishAvatar(avatarUrl: String?, fallbackLabel: String, size: Dp
}
else -> {
Image(
- painter = rememberImagePainter(
- data = avatarUrl,
- builder = {
- transformations(CircleCropTransformation())
- }
+ painter = rememberAsyncImagePainter(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(avatarUrl)
+ .transformations(CircleCropTransformation())
+ .build()
),
contentDescription = null,
- modifier = Modifier.size(size).align(Alignment.Center)
+ modifier = Modifier
+ .size(size)
+ .align(Alignment.Center)
)
}
}
@@ -56,7 +61,11 @@ fun BoxScope.CircleishAvatar(avatarUrl: String?, fallbackLabel: String, size: Dp
@Composable
fun MissingAvatarIcon(displayName: String, displayImageSize: Dp) {
val colors = SmallTalkTheme.extendedColors.getMissingImageColor(displayName)
- Box(Modifier.background(color = colors.first, shape = CircleShape).size(displayImageSize), contentAlignment = Alignment.Center) {
+ Box(
+ Modifier
+ .background(color = colors.first, shape = CircleShape)
+ .size(displayImageSize), contentAlignment = Alignment.Center
+ ) {
Text(
text = (displayName).first().toString().uppercase(),
color = colors.second
@@ -67,11 +76,11 @@ fun MissingAvatarIcon(displayName: String, displayImageSize: Dp) {
@Composable
fun MessengerUrlIcon(avatarUrl: String, displayImageSize: Dp) {
Image(
- painter = rememberImagePainter(
- data = avatarUrl,
- builder = {
- transformations(CircleCropTransformation())
- }
+ painter = rememberAsyncImagePainter(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(avatarUrl)
+ .transformations(CircleCropTransformation())
+ .build()
),
contentDescription = null,
modifier = Modifier.size(displayImageSize)
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt
index 89753c5..826694b 100644
--- a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt
@@ -27,14 +27,13 @@ fun Spider(currentPage: SpiderPage, onNavigate: (SpiderPage?
}
Column {
- Toolbar(
- onNavigate = navigateAndPopStack,
- title = currentPage.label
- )
-
- currentPage.parent?.let {
- BackHandler(onBack = navigateAndPopStack)
+ if (currentPage.hasToolbar) {
+ Toolbar(
+ onNavigate = navigateAndPopStack,
+ title = currentPage.label
+ )
}
+ BackHandler(onBack = navigateAndPopStack)
computedWeb[currentPage.route]!!.invoke(currentPage.state)
}
}
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt
index 60567c7..c79b436 100644
--- a/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Toolbar.kt
@@ -1,27 +1,38 @@
package app.dapk.st.design.components
-import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@Composable
-fun ColumnScope.Toolbar(onNavigate: () -> Unit, title: String? = null, actions: @Composable RowScope.() -> Unit = {}) {
+fun Toolbar(
+ onNavigate: (() -> Unit)? = null,
+ title: String? = null,
+ offset: (Density.() -> IntOffset)? = null,
+ actions: @Composable RowScope.() -> Unit = {}
+) {
+ val navigationIcon = foo(onNavigate)
+
TopAppBar(
- modifier = Modifier.height(72.dp),
- backgroundColor = Color.Transparent,
- navigationIcon = {
- IconButton(onClick = { onNavigate() }) {
- Icon(Icons.Default.ArrowBack, contentDescription = null)
+ modifier = Modifier.height(72.dp).run {
+ if (offset == null) {
+ this
+ } else {
+ this.offset(offset)
}
},
+ backgroundColor = MaterialTheme.colors.background,
+ navigationIcon = navigationIcon,
title = title?.let {
{ Text(it, maxLines = 2) }
} ?: {},
@@ -29,4 +40,18 @@ fun ColumnScope.Toolbar(onNavigate: () -> Unit, title: String? = null, actions:
elevation = 0.dp
)
Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black.copy(alpha = 0.2f), thickness = 0.5.dp)
+}
+
+
+private fun foo(onNavigate: (() -> Unit)?): (@Composable () -> Unit)? {
+ return onNavigate?.let {
+ { NavigationIcon(it) }
+ }
+}
+
+@Composable
+private fun NavigationIcon(onNavigate: () -> Unit) {
+ IconButton(onClick = { onNavigate.invoke() }) {
+ Icon(Icons.Default.ArrowBack, contentDescription = null)
+ }
}
\ No newline at end of file
diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt
index d76316e..6fdc9fa 100644
--- a/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt
+++ b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt
@@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
-fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null) {
+fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null, body: @Composable () -> Unit = {}) {
val modifier = Modifier.padding(horizontal = 24.dp)
Column(
Modifier
@@ -31,6 +31,7 @@ fun TextRow(title: String, content: String? = null, includeDivider: Boolean = tr
Text(text = content, fontSize = 18.sp)
}
}
+ body()
Spacer(modifier = Modifier.height(24.dp))
}
if (includeDivider) {
diff --git a/domains/android/compose-core/build.gradle b/domains/android/compose-core/build.gradle
new file mode 100644
index 0000000..c79d69b
--- /dev/null
+++ b/domains/android/compose-core/build.gradle
@@ -0,0 +1,7 @@
+applyAndroidComposeLibraryModule(project)
+
+dependencies {
+ implementation project(":core")
+ implementation project(":features:navigator")
+ api project(":domains:android:core")
+}
diff --git a/domains/android/core/src/main/AndroidManifest.xml b/domains/android/compose-core/src/main/AndroidManifest.xml
similarity index 100%
rename from domains/android/core/src/main/AndroidManifest.xml
rename to domains/android/compose-core/src/main/AndroidManifest.xml
diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt
similarity index 100%
rename from domains/android/core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt
rename to domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt
diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt
similarity index 95%
rename from domains/android/core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt
rename to domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt
index 4166d6b..a06b4b7 100644
--- a/domains/android/core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt
+++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt
@@ -41,6 +41,9 @@ fun LifecycleEffect(onStart: () -> Unit = {}, onStop: () -> Unit = {}) {
when (event) {
Lifecycle.Event.ON_START -> onStart()
Lifecycle.Event.ON_STOP -> onStop()
+ else -> {
+ // ignored
+ }
}
}
diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt
similarity index 100%
rename from domains/android/core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt
rename to domains/android/compose-core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt
diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt
similarity index 100%
rename from domains/android/core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt
rename to domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt
diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/components/Components.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/components/Components.kt
similarity index 100%
rename from domains/android/core/src/main/kotlin/app/dapk/st/core/components/Components.kt
rename to domains/android/compose-core/src/main/kotlin/app/dapk/st/core/components/Components.kt
diff --git a/domains/android/core/build.gradle b/domains/android/core/build.gradle
index 896c7c4..6a11a06 100644
--- a/domains/android/core/build.gradle
+++ b/domains/android/core/build.gradle
@@ -1,6 +1,6 @@
-applyAndroidLibraryModule(project)
+plugins { id 'kotlin' }
dependencies {
+ compileOnly project(":domains:android:stub")
implementation project(":core")
- implementation project(":features:navigator")
}
diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/ContextExtensions.kt b/domains/android/core/src/main/kotlin/app/dapk/st/core/ContextExtensions.kt
index 0d426b0..c36ac65 100644
--- a/domains/android/core/src/main/kotlin/app/dapk/st/core/ContextExtensions.kt
+++ b/domains/android/core/src/main/kotlin/app/dapk/st/core/ContextExtensions.kt
@@ -2,5 +2,6 @@ package app.dapk.st.core
import android.content.Context
-inline fun Context.module() =
- (this.applicationContext as ModuleProvider).provide(T::class)
\ No newline at end of file
+inline fun Context.module() = (this.applicationContext as ModuleProvider).provide(T::class)
+
+fun Context.resetModules() = (this.applicationContext as ModuleProvider).reset()
\ No newline at end of file
diff --git a/domains/android/core/src/main/kotlin/app/dapk/st/core/DeviceMetaExtensions.kt b/domains/android/core/src/main/kotlin/app/dapk/st/core/DeviceMetaExtensions.kt
new file mode 100644
index 0000000..3cc7e00
--- /dev/null
+++ b/domains/android/core/src/main/kotlin/app/dapk/st/core/DeviceMetaExtensions.kt
@@ -0,0 +1,19 @@
+package app.dapk.st.core
+
+import android.os.Build
+
+fun DeviceMeta.isAtLeastO(block: () -> T, fallback: () -> T = { throw IllegalStateException("not handled") }): T {
+ return if (this.apiVersion >= Build.VERSION_CODES.O) block() else fallback()
+}
+
+fun DeviceMeta.onAtLeastO(block: () -> Unit) {
+ if (this.apiVersion >= Build.VERSION_CODES.O) block()
+}
+
+inline fun DeviceMeta.whenPOrHigher(block: () -> T, fallback: () -> T) = whenXOrHigher(Build.VERSION_CODES.P, block, fallback)
+inline fun DeviceMeta.whenOOrHigher(block: () -> T, fallback: () -> T) = whenXOrHigher(Build.VERSION_CODES.O, block, fallback)
+
+inline fun DeviceMeta.whenXOrHigher(version: Int, block: () -> T, fallback: () -> T): T {
+ return if (this.apiVersion >= version) block() else fallback()
+}
+
diff --git a/domains/android/imageloader/build.gradle b/domains/android/imageloader/build.gradle
index 3ed97a3..45821be 100644
--- a/domains/android/imageloader/build.gradle
+++ b/domains/android/imageloader/build.gradle
@@ -2,5 +2,5 @@ applyAndroidLibraryModule(project)
dependencies {
implementation project(":core")
- implementation "io.coil-kt:coil:1.4.0"
+ implementation Dependencies.mavenCentral.coil
}
diff --git a/domains/android/imageloader/src/main/kotlin/app/dapk/st/imageloader/ImageLoader.kt b/domains/android/imageloader/src/main/kotlin/app/dapk/st/imageloader/ImageLoader.kt
index f84db5c..06cd47a 100644
--- a/domains/android/imageloader/src/main/kotlin/app/dapk/st/imageloader/ImageLoader.kt
+++ b/domains/android/imageloader/src/main/kotlin/app/dapk/st/imageloader/ImageLoader.kt
@@ -1,12 +1,13 @@
package app.dapk.st.imageloader
import android.content.Context
+import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.widget.ImageView
-import androidx.core.graphics.drawable.toBitmap
import coil.imageLoader
import coil.request.ImageRequest
+import coil.request.ImageResult
import coil.transform.CircleCropTransformation
import coil.transform.Transformation
import coil.load as coilLoad
@@ -14,7 +15,6 @@ import coil.load as coilLoad
interface ImageLoader {
suspend fun load(url: String, transformation: Transformation? = null): Drawable?
-
}
interface IconLoader {
@@ -31,19 +31,24 @@ class CachedIcons(private val imageLoader: ImageLoader) : IconLoader {
override suspend fun load(url: String): Icon? {
return cache.getOrPut(url) {
- imageLoader.load(url, transformation = circleCrop)?.toBitmap()?.let {
+ imageLoader.load(url, transformation = circleCrop)?.asBitmap()?.let {
Icon.createWithBitmap(it)
}
}
}
}
+private fun Drawable.asBitmap() = (this as? BitmapDrawable)?.bitmap
internal class CoilImageLoader(private val context: Context) : ImageLoader {
private val coil = context.imageLoader
override suspend fun load(url: String, transformation: Transformation?): Drawable? {
+ return internalLoad(url, transformation).drawable
+ }
+
+ private suspend fun internalLoad(url: String, transformation: Transformation?): ImageResult {
val request = ImageRequest.Builder(context)
.data(url)
.let {
@@ -53,7 +58,7 @@ internal class CoilImageLoader(private val context: Context) : ImageLoader {
}
}
.build()
- return coil.execute(request).drawable
+ return coil.execute(request)
}
}
diff --git a/domains/android/push/build.gradle b/domains/android/push/build.gradle
index 4f21b7a..dbcfe03 100644
--- a/domains/android/push/build.gradle
+++ b/domains/android/push/build.gradle
@@ -1,8 +1,13 @@
applyAndroidLibraryModule(project)
+apply plugin: "org.jetbrains.kotlin.plugin.serialization"
dependencies {
implementation project(':core')
+ implementation project(':domains:android:core')
+ implementation project(':domains:store')
implementation project(':matrix:services:push')
implementation platform('com.google.firebase:firebase-bom:29.0.3')
implementation 'com.google.firebase:firebase-messaging'
+ implementation Dependencies.mavenCentral.kotlinSerializationJson
+ implementation Dependencies.jitPack.unifiedPush
}
diff --git a/domains/android/push/src/main/AndroidManifest.xml b/domains/android/push/src/main/AndroidManifest.xml
index a675a3a..61024b7 100644
--- a/domains/android/push/src/main/AndroidManifest.xml
+++ b/domains/android/push/src/main/AndroidManifest.xml
@@ -1,2 +1,24 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushHandler.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushHandler.kt
new file mode 100644
index 0000000..5b7fa75
--- /dev/null
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushHandler.kt
@@ -0,0 +1,17 @@
+package app.dapk.st.push
+
+import app.dapk.st.matrix.common.EventId
+import app.dapk.st.matrix.common.RoomId
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+interface PushHandler {
+ fun onNewToken(payload: PushTokenPayload)
+ fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
+}
+
+@Serializable
+data class PushTokenPayload(
+ @SerialName("token") val token: String,
+ @SerialName("gateway_url") val gatewayUrl: String,
+)
\ No newline at end of file
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt
index 5978b95..5b923c7 100644
--- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt
@@ -1,16 +1,40 @@
package app.dapk.st.push
+import android.content.Context
+import app.dapk.st.core.CoroutineDispatchers
+import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.extensions.ErrorTracker
-import app.dapk.st.matrix.push.PushService
+import app.dapk.st.core.extensions.unsafeLazy
+import app.dapk.st.domain.Preferences
+import app.dapk.st.domain.push.PushTokenRegistrarPreferences
+import app.dapk.st.push.firebase.FirebasePushTokenRegistrar
+import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
class PushModule(
- private val pushService: PushService,
private val errorTracker: ErrorTracker,
-) {
+ private val pushHandler: PushHandler,
+ private val context: Context,
+ private val dispatchers: CoroutineDispatchers,
+ private val preferences: Preferences,
+) : ProvidableModule {
- fun registerFirebasePushTokenUseCase() = RegisterFirebasePushTokenUseCase(
- pushService,
- errorTracker,
- )
+ private val registrars by unsafeLazy {
+ PushTokenRegistrars(
+ context,
+ FirebasePushTokenRegistrar(
+ errorTracker,
+ context,
+ pushHandler,
+ ),
+ UnifiedPushRegistrar(context),
+ PushTokenRegistrarPreferences(preferences)
+ )
+ }
-}
\ No newline at end of file
+ fun pushTokenRegistrars() = registrars
+
+ fun pushTokenRegistrar(): PushTokenRegistrar = pushTokenRegistrars()
+ fun pushHandler() = pushHandler
+ fun dispatcher() = dispatchers
+
+}
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrar.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrar.kt
new file mode 100644
index 0000000..6cd6a1e
--- /dev/null
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrar.kt
@@ -0,0 +1,6 @@
+package app.dapk.st.push
+
+interface PushTokenRegistrar {
+ suspend fun registerCurrentToken()
+ fun unregister()
+}
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt
new file mode 100644
index 0000000..7ce0658
--- /dev/null
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt
@@ -0,0 +1,76 @@
+package app.dapk.st.push
+
+import android.content.Context
+import app.dapk.st.domain.push.PushTokenRegistrarPreferences
+import app.dapk.st.push.firebase.FirebasePushTokenRegistrar
+import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
+import org.unifiedpush.android.connector.UnifiedPush
+
+private val FIREBASE_OPTION = Registrar("Google - Firebase (FCM)")
+private val NONE = Registrar("None")
+
+class PushTokenRegistrars(
+ private val context: Context,
+ private val firebasePushTokenRegistrar: FirebasePushTokenRegistrar,
+ private val unifiedPushRegistrar: UnifiedPushRegistrar,
+ private val pushTokenStore: PushTokenRegistrarPreferences,
+) : PushTokenRegistrar {
+
+ private var selection: Registrar? = null
+
+ fun options(): List {
+ return listOf(NONE, FIREBASE_OPTION) + UnifiedPush.getDistributors(context).map { Registrar(it) }
+ }
+
+ suspend fun currentSelection() = selection ?: (pushTokenStore.currentSelection()?.let { Registrar(it) } ?: FIREBASE_OPTION).also { selection = it }
+
+ suspend fun makeSelection(option: Registrar) {
+ selection = option
+ pushTokenStore.store(option.id)
+ when (option) {
+ NONE -> {
+ firebasePushTokenRegistrar.unregister()
+ unifiedPushRegistrar.unregister()
+ }
+
+ FIREBASE_OPTION -> {
+ unifiedPushRegistrar.unregister()
+ firebasePushTokenRegistrar.registerCurrentToken()
+ }
+
+ else -> {
+ firebasePushTokenRegistrar.unregister()
+ unifiedPushRegistrar.registerSelection(option)
+ }
+ }
+ }
+
+ override suspend fun registerCurrentToken() {
+ when (selection) {
+ FIREBASE_OPTION -> firebasePushTokenRegistrar.registerCurrentToken()
+ NONE -> {
+ // do nothing
+ }
+
+ else -> unifiedPushRegistrar.registerCurrentToken()
+ }
+ }
+
+ override fun unregister() {
+ when (selection) {
+ FIREBASE_OPTION -> firebasePushTokenRegistrar.unregister()
+ NONE -> {
+ runCatching {
+ firebasePushTokenRegistrar.unregister()
+ unifiedPushRegistrar.unregister()
+ }
+ }
+
+ else -> unifiedPushRegistrar.unregister()
+ }
+ }
+
+}
+
+@JvmInline
+value class Registrar(val id: String)
\ No newline at end of file
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/RegisterFirebasePushTokenUseCase.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/RegisterFirebasePushTokenUseCase.kt
deleted file mode 100644
index 4ef213b..0000000
--- a/domains/android/push/src/main/kotlin/app/dapk/st/push/RegisterFirebasePushTokenUseCase.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package app.dapk.st.push
-
-import app.dapk.st.core.AppLogTag
-import app.dapk.st.core.extensions.CrashScope
-import app.dapk.st.core.extensions.ErrorTracker
-import app.dapk.st.core.log
-import app.dapk.st.matrix.push.PushService
-import com.google.firebase.messaging.FirebaseMessaging
-
-class RegisterFirebasePushTokenUseCase(
- private val pushService: PushService,
- override val errorTracker: ErrorTracker,
-) : CrashScope {
-
- suspend fun registerCurrentToken() {
- kotlin.runCatching {
- FirebaseMessaging.getInstance().token().also {
- pushService.registerPush(it)
- }
- }
- .trackFailure()
- .onSuccess {
- log(AppLogTag.PUSH, "registered new push token")
- }
- }
-
-}
\ No newline at end of file
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/FirebaseMessagingExtensions.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebaseMessagingExtensions.kt
similarity index 86%
rename from domains/android/push/src/main/kotlin/app/dapk/st/push/FirebaseMessagingExtensions.kt
rename to domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebaseMessagingExtensions.kt
index 78a8033..9ff0ac7 100644
--- a/domains/android/push/src/main/kotlin/app/dapk/st/push/FirebaseMessagingExtensions.kt
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebaseMessagingExtensions.kt
@@ -1,4 +1,4 @@
-package app.dapk.st.push
+package app.dapk.st.push.firebase
import com.google.firebase.messaging.FirebaseMessaging
import kotlin.coroutines.resume
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushService.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushService.kt
new file mode 100644
index 0000000..e1d79db
--- /dev/null
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushService.kt
@@ -0,0 +1,36 @@
+package app.dapk.st.push.firebase
+
+import app.dapk.st.core.AppLogTag
+import app.dapk.st.core.extensions.unsafeLazy
+import app.dapk.st.core.log
+import app.dapk.st.core.module
+import app.dapk.st.matrix.common.EventId
+import app.dapk.st.matrix.common.RoomId
+import app.dapk.st.push.PushModule
+import app.dapk.st.push.PushTokenPayload
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+
+private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify"
+
+class FirebasePushService : FirebaseMessagingService() {
+
+ private val handler by unsafeLazy { module().pushHandler() }
+
+ override fun onNewToken(token: String) {
+ log(AppLogTag.PUSH, "FCM onNewToken")
+ handler.onNewToken(
+ PushTokenPayload(
+ token = token,
+ gatewayUrl = SYGNAL_GATEWAY,
+ )
+ )
+ }
+
+ override fun onMessageReceived(message: RemoteMessage) {
+ log(AppLogTag.PUSH, "FCM onMessage")
+ val eventId = message.data["event_id"]?.let { EventId(it) }
+ val roomId = message.data["room_id"]?.let { RoomId(it) }
+ handler.onMessageReceived(eventId, roomId)
+ }
+}
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushTokenRegistrar.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushTokenRegistrar.kt
new file mode 100644
index 0000000..2c9321a
--- /dev/null
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/firebase/FirebasePushTokenRegistrar.kt
@@ -0,0 +1,61 @@
+package app.dapk.st.push.firebase
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import app.dapk.st.core.AppLogTag
+import app.dapk.st.core.extensions.CrashScope
+import app.dapk.st.core.extensions.ErrorTracker
+import app.dapk.st.core.log
+import app.dapk.st.push.PushHandler
+import app.dapk.st.push.PushTokenPayload
+import app.dapk.st.push.PushTokenRegistrar
+import app.dapk.st.push.unifiedpush.UnifiedPushMessageReceiver
+import com.google.firebase.messaging.FirebaseMessaging
+
+private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify"
+
+class FirebasePushTokenRegistrar(
+ override val errorTracker: ErrorTracker,
+ private val context: Context,
+ private val pushHandler: PushHandler,
+) : PushTokenRegistrar, CrashScope {
+
+ override suspend fun registerCurrentToken() {
+ log(AppLogTag.PUSH, "FCM - register current token")
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, FirebasePushService::class.java),
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP,
+ )
+
+ kotlin.runCatching {
+ FirebaseMessaging.getInstance().token().also {
+ pushHandler.onNewToken(
+ PushTokenPayload(
+ token = it,
+ gatewayUrl = SYGNAL_GATEWAY,
+ )
+ )
+ }
+ }
+ .trackFailure()
+ .onSuccess {
+ log(AppLogTag.PUSH, "registered new push token")
+ }
+ }
+
+ override fun unregister() {
+ log(AppLogTag.PUSH, "FCM - unregister")
+ FirebaseMessaging.getInstance().deleteToken()
+ context.stopService(Intent(context, FirebasePushService::class.java))
+
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, FirebasePushService::class.java),
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP,
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt
new file mode 100644
index 0000000..011b0dc
--- /dev/null
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt
@@ -0,0 +1,77 @@
+package app.dapk.st.push.unifiedpush
+
+import android.content.Context
+import app.dapk.st.core.AppLogTag
+import app.dapk.st.core.log
+import app.dapk.st.core.module
+import app.dapk.st.matrix.common.EventId
+import app.dapk.st.matrix.common.RoomId
+import app.dapk.st.push.PushModule
+import app.dapk.st.push.PushTokenPayload
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import org.unifiedpush.android.connector.MessagingReceiver
+import java.net.URL
+
+private val json = Json { ignoreUnknownKeys = true }
+
+private const val FALLBACK_UNIFIED_PUSH_GATEWAY = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
+
+class UnifiedPushMessageReceiver : MessagingReceiver() {
+
+ private val scope = CoroutineScope(SupervisorJob())
+
+ override fun onMessage(context: Context, message: ByteArray, instance: String) {
+ log(AppLogTag.PUSH, "UnifiedPush onMessage, $message")
+ val module = context.module()
+ val handler = module.pushHandler()
+ scope.launch {
+ withContext(module.dispatcher().io) {
+ val payload = json.decodeFromString(UnifiedPushMessagePayload.serializer(), String(message))
+ handler.onMessageReceived(payload.notification.eventId?.let { EventId(it) }, payload.notification.roomId?.let { RoomId(it) })
+ }
+ }
+ }
+
+ override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
+ log(AppLogTag.PUSH, "UnifiedPush onNewEndpoint $endpoint")
+ val module = context.module()
+ val handler = module.pushHandler()
+ scope.launch {
+ withContext(module.dispatcher().io) {
+ val matrixEndpoint = URL(endpoint).let { URL("${it.protocol}://${it.host}/_matrix/push/v1/notify") }
+ val content = runCatching { matrixEndpoint.openStream().use { String(it.readBytes()) } }.getOrNull() ?: ""
+ val gatewayUrl = when {
+ content.contains("\"gateway\":\"matrix\"") -> matrixEndpoint.toString()
+ else -> FALLBACK_UNIFIED_PUSH_GATEWAY
+ }
+ handler.onNewToken(PushTokenPayload(token = endpoint, gatewayUrl = gatewayUrl))
+ }
+ }
+ }
+
+ override fun onRegistrationFailed(context: Context, instance: String) {
+ log(AppLogTag.PUSH, "UnifiedPush onRegistrationFailed")
+ }
+
+ override fun onUnregistered(context: Context, instance: String) {
+ log(AppLogTag.PUSH, "UnifiedPush onUnregistered")
+ }
+
+ @Serializable
+ private data class UnifiedPushMessagePayload(
+ @SerialName("notification") val notification: Notification,
+ ) {
+
+ @Serializable
+ data class Notification(
+ @SerialName("event_id") val eventId: String?,
+ @SerialName("room_id") val roomId: String?,
+ )
+ }
+}
\ No newline at end of file
diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt
new file mode 100644
index 0000000..cbb5ad5
--- /dev/null
+++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt
@@ -0,0 +1,47 @@
+package app.dapk.st.push.unifiedpush
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import app.dapk.st.core.AppLogTag
+import app.dapk.st.core.log
+import app.dapk.st.push.PushTokenRegistrar
+import app.dapk.st.push.Registrar
+import org.unifiedpush.android.connector.UnifiedPush
+
+class UnifiedPushRegistrar(
+ private val context: Context,
+) : PushTokenRegistrar {
+
+ fun registerSelection(registrar: Registrar) {
+ log(AppLogTag.PUSH, "UnifiedPush - register: $registrar")
+ UnifiedPush.saveDistributor(context, registrar.id)
+ registerApp()
+ }
+
+ override suspend fun registerCurrentToken() {
+ log(AppLogTag.PUSH, "UnifiedPush - register current token")
+ if (UnifiedPush.getDistributor(context).isNotEmpty()) {
+ registerApp()
+ }
+ }
+
+ private fun registerApp() {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, UnifiedPushMessageReceiver::class.java),
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP,
+ )
+ UnifiedPush.registerApp(context)
+ }
+
+ override fun unregister() {
+ UnifiedPush.unregisterApp(context)
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(context, UnifiedPushMessageReceiver::class.java),
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP,
+ )
+ }
+
+}
diff --git a/domains/android/stub/build.gradle b/domains/android/stub/build.gradle
index 8ee9c3d..4a7ad4a 100644
--- a/domains/android/stub/build.gradle
+++ b/domains/android/stub/build.gradle
@@ -13,6 +13,8 @@ if (localProperties.exists()) {
dependencies {
def androidVer = androidSdkVersion
+ api files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
+
kotlinFixtures(it)
testFixturesImplementation testFixtures(project(":core"))
testFixturesImplementation files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeContext.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeContext.kt
new file mode 100644
index 0000000..623c98c
--- /dev/null
+++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeContext.kt
@@ -0,0 +1,8 @@
+package fake
+
+import android.content.Context
+import io.mockk.mockk
+
+class FakeContext {
+ val instance = mockk()
+}
\ No newline at end of file
diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeInboxStyle.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeInboxStyle.kt
new file mode 100644
index 0000000..910cfa7
--- /dev/null
+++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeInboxStyle.kt
@@ -0,0 +1,22 @@
+package fake
+
+import android.app.Notification
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+
+class FakeInboxStyle {
+ private val _summary = slot()
+
+ val instance = mockk()
+ val lines = mutableListOf()
+ val summary: String
+ get() = _summary.captured
+
+ fun captureInteractions() {
+ every { instance.addLine(capture(lines)) } returns instance
+ every { instance.setSummaryText(capture(_summary)) } returns instance
+ }
+
+
+}
\ No newline at end of file
diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeMessagingStyle.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeMessagingStyle.kt
new file mode 100644
index 0000000..d76b96a
--- /dev/null
+++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeMessagingStyle.kt
@@ -0,0 +1,14 @@
+package fake
+
+import android.app.Notification
+import android.app.Person
+import io.mockk.every
+import io.mockk.mockk
+
+class FakeMessagingStyle {
+ var user: Person? = null
+ val instance = mockk()
+
+}
+
+fun aFakeMessagingStyle() = FakeMessagingStyle().instance
\ No newline at end of file
diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotification.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotification.kt
new file mode 100644
index 0000000..a07820a
--- /dev/null
+++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotification.kt
@@ -0,0 +1,12 @@
+package fake
+
+import android.app.Notification
+import io.mockk.mockk
+
+class FakeNotification {
+
+ val instance = mockk()
+
+}
+
+fun aFakeNotification() = FakeNotification().instance
\ No newline at end of file
diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationBuilder.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationBuilder.kt
new file mode 100644
index 0000000..67aa3a0
--- /dev/null
+++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationBuilder.kt
@@ -0,0 +1,8 @@
+package fake
+
+import android.app.Notification
+import io.mockk.mockk
+
+class FakeNotificationBuilder {
+ val instance = mockk(relaxed = true)
+}
\ No newline at end of file
diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationManager.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationManager.kt
new file mode 100644
index 0000000..b8573e3
--- /dev/null
+++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeNotificationManager.kt
@@ -0,0 +1,14 @@
+package fake
+
+import android.app.NotificationManager
+import io.mockk.mockk
+import io.mockk.verify
+
+class FakeNotificationManager {
+
+ val instance = mockk()
+
+ fun verifyCancelled(tag: String, id: Int) {
+ verify { instance.cancel(tag, id) }
+ }
+}
\ No newline at end of file
diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakePersonBuilder.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakePersonBuilder.kt
new file mode 100644
index 0000000..e254d7d
--- /dev/null
+++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakePersonBuilder.kt
@@ -0,0 +1,8 @@
+package fake
+
+import android.app.Person
+import io.mockk.mockk
+
+class FakePersonBuilder {
+ val instance = mockk(relaxed = true)
+}
\ No newline at end of file
diff --git a/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt b/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt
index 543df16..c1bfc7a 100644
--- a/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt
+++ b/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt
@@ -1,5 +1,5 @@
@file:JvmName("SnapshotStateKt")
-
+@file:Suppress("UNUSED")
package androidx.compose.runtime
import kotlin.reflect.KProperty
diff --git a/domains/android/viewmodel/build.gradle b/domains/android/viewmodel/build.gradle
index dac308a..c3cce48 100644
--- a/domains/android/viewmodel/build.gradle
+++ b/domains/android/viewmodel/build.gradle
@@ -9,7 +9,7 @@ dependencies {
kotlinFixtures(it)
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
- testFixturesImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
+ testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
testFixturesImplementation testFixtures(project(":core"))
testFixturesCompileOnly project(":domains:android:viewmodel-stub")
}
\ No newline at end of file
diff --git a/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt b/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt
index 9cdadde..018c6cd 100644
--- a/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt
+++ b/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt
@@ -6,6 +6,7 @@ import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import test.ExpectTest
+@Suppress("UNCHECKED_CAST")
class ViewModelTest {
var instance: TestMutableState? = null
diff --git a/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt b/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt
index a1dff08..e60c0ac 100644
--- a/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt
+++ b/domains/android/work/src/main/kotlin/app/dapk/st/work/TaskRunner.kt
@@ -8,15 +8,15 @@ interface TaskRunner {
suspend fun run(tasks: List): List
data class RunnableWorkTask(
- val source: JobWorkItem,
+ val source: JobWorkItem?,
val task: WorkTask
)
sealed interface TaskResult {
- val source: JobWorkItem
+ val source: JobWorkItem?
- data class Success(override val source: JobWorkItem) : TaskResult
- data class Failure(override val source: JobWorkItem, val canRetry: Boolean) : TaskResult
+ data class Success(override val source: JobWorkItem?) : TaskResult
+ data class Failure(override val source: JobWorkItem?, val canRetry: Boolean) : TaskResult
}
}
\ No newline at end of file
diff --git a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt
index e712235..edb4b38 100644
--- a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt
+++ b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkAndroidService.kt
@@ -3,6 +3,7 @@ package app.dapk.st.work
import android.app.job.JobParameters
import android.app.job.JobService
import android.app.job.JobWorkItem
+import android.os.Build
import app.dapk.st.core.extensions.Scope
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.core.module
@@ -24,11 +25,15 @@ class WorkAndroidService : JobService() {
when (it) {
is TaskRunner.TaskResult.Failure -> {
if (!it.canRetry) {
- params.completeWork(it.source)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ params.completeWork(it.source!!)
+ }
}
}
is TaskRunner.TaskResult.Success -> {
- params.completeWork(it.source)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ params.completeWork(it.source!!)
+ }
}
}
}
@@ -40,24 +45,37 @@ class WorkAndroidService : JobService() {
}
private fun JobParameters.collectAllTasks(): List {
- var work: JobWorkItem?
- val tasks = mutableListOf()
- do {
- work = this.dequeueWork()
- work?.intent?.also { intent ->
- tasks.add(
- RunnableWorkTask(
- source = work,
- task = WorkTask(
- jobId = this.jobId,
- type = intent.getStringExtra("task-type")!!,
- jsonPayload = intent.getStringExtra("task-payload")!!,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ var work: JobWorkItem?
+ val tasks = mutableListOf()
+ do {
+ work = this.dequeueWork()
+ work?.intent?.also { intent ->
+ tasks.add(
+ RunnableWorkTask(
+ source = work,
+ task = WorkTask(
+ jobId = this.jobId,
+ type = intent.getStringExtra("task-type")!!,
+ jsonPayload = intent.getStringExtra("task-payload")!!,
+ )
)
)
+ }
+ } while (work != null)
+ return tasks
+ } else {
+ return listOf(
+ RunnableWorkTask(
+ source = null,
+ task = WorkTask(
+ jobId = this.jobId,
+ type = this.extras.getString("task-type")!!,
+ jsonPayload = this.extras.getString("task-payload")!!,
+ )
)
- }
- } while (work != null)
- return tasks
+ )
+ }
}
override fun onStopJob(params: JobParameters): Boolean {
diff --git a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt
index b4a70ef..ce93d61 100644
--- a/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt
+++ b/domains/android/work/src/main/kotlin/app/dapk/st/work/WorkSchedulingJobScheduler.kt
@@ -6,6 +6,7 @@ import android.app.job.JobWorkItem
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.os.Build
internal class WorkSchedulingJobScheduler(
private val context: Context,
@@ -23,12 +24,17 @@ internal class WorkSchedulingJobScheduler(
.setRequiresDeviceIdle(false)
.build()
- val item = JobWorkItem(
- Intent()
- .putExtra("task-type", task.type)
- .putExtra("task-payload", task.jsonPayload)
- )
-
- jobScheduler.enqueue(job, item)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val item = JobWorkItem(
+ Intent()
+ .putExtra("task-type", task.type)
+ .putExtra("task-payload", task.jsonPayload)
+ )
+ jobScheduler.enqueue(job, item)
+ } else {
+ job.extras.putString("task-type", task.type)
+ job.extras.putString("task-payload", task.jsonPayload)
+ jobScheduler.schedule(job)
+ }
}
}
diff --git a/domains/olm-stub/build.gradle b/domains/olm-stub/build.gradle
index 5717824..b35101f 100644
--- a/domains/olm-stub/build.gradle
+++ b/domains/olm-stub/build.gradle
@@ -3,5 +3,5 @@ plugins {
}
dependencies {
- compileOnly 'org.json:json:20211205'
+ compileOnly 'org.json:json:20220320'
}
\ No newline at end of file
diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt
index 36869c1..c924186 100644
--- a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt
+++ b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmPersistenceWrapper.kt
@@ -1,5 +1,6 @@
package app.dapk.st.olm
+import app.dapk.st.core.Base64
import app.dapk.st.domain.OlmPersistence
import app.dapk.st.domain.SerializedObject
import app.dapk.st.matrix.common.Curve25519
@@ -10,10 +11,10 @@ import org.matrix.olm.OlmInboundGroupSession
import org.matrix.olm.OlmOutboundGroupSession
import org.matrix.olm.OlmSession
import java.io.*
-import java.util.*
class OlmPersistenceWrapper(
private val olmPersistence: OlmPersistence,
+ private val base64: Base64,
) : OlmStore {
override suspend fun read(): OlmAccount? {
@@ -49,21 +50,21 @@ class OlmPersistenceWrapper(
override suspend fun readInbound(sessionId: SessionId): OlmInboundGroupSession? {
return olmPersistence.readInbound(sessionId)?.value?.deserialize()
}
-}
-private fun T.serialize(): String {
- val baos = ByteArrayOutputStream()
- ObjectOutputStream(baos).use {
- it.writeObject(this)
+ private fun T.serialize(): String {
+ val baos = ByteArrayOutputStream()
+ ObjectOutputStream(baos).use {
+ it.writeObject(this)
+ }
+ return base64.encode(baos.toByteArray())
}
- return Base64.getEncoder().encode(baos.toByteArray()).toString(Charsets.UTF_8)
-}
-@Suppress("UNCHECKED_CAST")
-private fun String.deserialize(): T {
- val decoded = Base64.getDecoder().decode(this)
- val baos = ByteArrayInputStream(decoded)
- return ObjectInputStream(baos).use {
- it.readObject() as T
+ @Suppress("UNCHECKED_CAST")
+ private fun String.deserialize(): T {
+ val decoded = base64.decode(this)
+ val baos = ByteArrayInputStream(decoded)
+ return ObjectInputStream(baos).use {
+ it.readObject() as T
+ }
}
}
diff --git a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt
index ceee2ba..6a5531a 100644
--- a/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt
+++ b/domains/olm/src/main/kotlin/app/dapk/st/olm/OlmWrapper.kt
@@ -254,7 +254,7 @@ class OlmWrapper(
return readSession.firstNotNullOfOrNull { (_, session) ->
kotlin.runCatching {
- when (type.toInt()) {
+ when (type) {
OlmMessage.MESSAGE_TYPE_PRE_KEY -> {
if (session.matchesInboundSession(body.value)) {
logger.matrixLog(CRYPTO, "matched inbound session, attempting decrypt")
@@ -270,6 +270,8 @@ class OlmWrapper(
session.decryptMessage(olmMessage)?.let { JsonString(it) }?.also {
logger.crypto("alt flow identity: $senderKey : ${session.sessionIdentifier()}")
olmStore.persistSession(senderKey, SessionId(session.sessionIdentifier()), session)
+ }.also {
+ session.releaseSession()
}
}
}
@@ -287,6 +289,8 @@ class OlmWrapper(
}.ifNull {
logger.matrixLog(CRYPTO, "failed to decrypt olm session")
DecryptionResult.Failed(errors.joinToString { it.message ?: "N/A" })
+ }.also {
+ readSession.forEach { it.second.releaseSession() }
}
}
@@ -310,7 +314,9 @@ class OlmWrapper(
errorTracker.track(it)
DecryptionResult.Failed(it.message ?: "Unknown")
}
- )
+ ).also {
+ megolmSession.releaseSession()
+ }
}
}
}
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt
new file mode 100644
index 0000000..5570a73
--- /dev/null
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt
@@ -0,0 +1,19 @@
+package app.dapk.st.domain
+
+class ApplicationPreferences(
+ private val preferences: Preferences,
+) {
+
+ suspend fun readVersion(): ApplicationVersion? {
+ return preferences.readString("version")?.let { ApplicationVersion(it.toInt()) }
+ }
+
+ suspend fun setVersion(version: ApplicationVersion) {
+ return preferences.store("version", version.value.toString())
+ }
+
+}
+
+@JvmInline
+value class ApplicationVersion(val value: Int)
+
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt
index 101b39f..4b3b20e 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/MemberPersistence.kt
@@ -35,4 +35,12 @@ class MemberPersistence(
.map { Json.decodeFromString(RoomMember.serializer(), it) }
}
}
+
+ override suspend fun query(roomId: RoomId, limit: Int): List {
+ return coroutineDispatchers.withIoContext {
+ database.roomMemberQueries.selectMembersByRoom(roomId.value, limit.toLong())
+ .executeAsList()
+ .map { Json.decodeFromString(RoomMember.serializer(), it) }
+ }
+ }
}
\ No newline at end of file
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt
index c478116..15cbaf1 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt
@@ -7,6 +7,7 @@ import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.domain.eventlog.EventLogPersistence
import app.dapk.st.domain.localecho.LocalEchoPersistence
import app.dapk.st.domain.profile.ProfilePersistence
+import app.dapk.st.domain.push.PushTokenRegistrarPreferences
import app.dapk.st.domain.sync.OverviewPersistence
import app.dapk.st.domain.sync.RoomPersistence
import app.dapk.st.matrix.common.CredentialsStore
@@ -34,6 +35,10 @@ class StoreModule(
fun filterStore(): FilterStore = FilterPreferences(preferences)
val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) }
+ fun pushStore() = PushTokenRegistrarPreferences(preferences)
+
+ fun applicationStore() = ApplicationPreferences(preferences)
+
fun olmStore() = OlmPersistence(database, credentialsStore())
fun knownDevicesStore() = DevicePersistence(database, KnownDevicesCache(), coroutineDispatchers)
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt
index 2be221e..b4d5346 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt
@@ -1,5 +1,7 @@
package app.dapk.st.domain
+import app.dapk.st.core.AppLogTag
+import app.dapk.st.core.log
import app.dapk.st.matrix.common.SyncToken
import app.dapk.st.matrix.sync.SyncStore
import app.dapk.st.matrix.sync.SyncStore.SyncKey
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt
index bb28e31..3ae9a1b 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt
@@ -33,11 +33,13 @@ class LocalEchoPersistence(
inMemoryEchos.value = echos.groupBy {
when (val message = it.message) {
is MessageService.Message.TextMessage -> message.roomId
+ is MessageService.Message.ImageMessage -> message.roomId
}
}.mapValues {
it.value.associateBy {
when (val message = it.message) {
is MessageService.Message.TextMessage -> message.localId
+ is MessageService.Message.ImageMessage -> message.localId
}
}
}
@@ -56,6 +58,7 @@ class LocalEchoPersistence(
database.transaction {
when (message) {
is MessageService.Message.TextMessage -> database.localEchoQueries.delete(message.localId)
+ is MessageService.Message.ImageMessage -> database.localEchoQueries.delete(message.localId)
}
}
} catch (error: Exception) {
@@ -84,6 +87,14 @@ class LocalEchoPersistence(
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
)
)
+
+ is MessageService.Message.ImageMessage -> database.localEchoQueries.insert(
+ DbLocalEcho(
+ message.localId,
+ message.roomId.value,
+ Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
+ )
+ )
}
}
}
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/push/PushTokenRegistrarPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/push/PushTokenRegistrarPreferences.kt
new file mode 100644
index 0000000..38110ef
--- /dev/null
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/push/PushTokenRegistrarPreferences.kt
@@ -0,0 +1,16 @@
+package app.dapk.st.domain.push
+
+import app.dapk.st.domain.Preferences
+
+private const val SELECTION_KEY = "push_token_selection"
+
+class PushTokenRegistrarPreferences(
+ private val preferences: Preferences,
+) {
+
+ suspend fun currentSelection() = preferences.readString(SELECTION_KEY)
+
+ suspend fun store(registrar: String) {
+ preferences.store(SELECTION_KEY, registrar)
+ }
+}
\ No newline at end of file
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt
index 7c6a9c6..b9bb5f8 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/OverviewPersistence.kt
@@ -11,10 +11,8 @@ import app.dapk.st.matrix.sync.RoomInvite
import app.dapk.st.matrix.sync.RoomOverview
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
private val json = Json
@@ -31,11 +29,22 @@ internal class OverviewPersistence(
.map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
}
+ override suspend fun removeRooms(roomsToRemove: List) {
+ dispatchers.withIoContext {
+ database.transaction {
+ roomsToRemove.forEach {
+ database.inviteStateQueries.remove(it.value)
+ database.overviewStateQueries.remove(it.value)
+ }
+ }
+ }
+ }
+
override suspend fun persistInvites(invites: List) {
dispatchers.withIoContext {
database.inviteStateQueries.transaction {
invites.forEach {
- database.inviteStateQueries.insert(it.roomId.value)
+ database.inviteStateQueries.insert(it.roomId.value, json.encodeToString(RoomInvite.serializer(), it))
}
}
}
@@ -45,7 +54,15 @@ internal class OverviewPersistence(
return database.inviteStateQueries.selectAll()
.asFlow()
.mapToList()
- .map { it.map { RoomInvite(RoomId(it)) } }
+ .map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } }
+ }
+
+ override suspend fun removeInvites(invites: List) {
+ dispatchers.withIoContext {
+ database.inviteStateQueries.transaction {
+ invites.forEach { database.inviteStateQueries.remove(it.value) }
+ }
+ }
}
override suspend fun persist(overviewState: OverviewState) {
@@ -59,7 +76,7 @@ internal class OverviewPersistence(
}
override suspend fun retrieve(): OverviewState {
- return withContext(Dispatchers.IO) {
+ return dispatchers.withIoContext {
val overviews = database.overviewStateQueries.selectAll().executeAsList()
overviews.map { json.decodeFromString(RoomOverview.serializer(), it.blob) }
}
diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt
index 0ed472b..451effd 100644
--- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt
+++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt
@@ -37,6 +37,13 @@ internal class RoomPersistence(
}
}
+ override suspend fun remove(rooms: List) {
+ coroutineDispatchers
+ database.roomEventQueries.transaction {
+ rooms.forEach { database.roomEventQueries.remove(it.value) }
+ }
+ }
+
override fun latest(roomId: RoomId): Flow {
val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map {
json.decodeFromString(RoomOverview.serializer(), it)
@@ -75,7 +82,7 @@ internal class RoomPersistence(
}
}
- override suspend fun observeUnread(): Flow