initial commit
This commit is contained in:
commit
c78d24a458
|
@ -0,0 +1,13 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
.idea/*.xml
|
||||||
|
.DS_Store
|
||||||
|
**/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
/benchmark-out
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
|
@ -0,0 +1 @@
|
||||||
|
SmallTalk
|
|
@ -0,0 +1,127 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="Groovy">
|
||||||
|
<option name="RIGHT_MARGIN" value="160" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<option name="RIGHT_MARGIN" value="160" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
|
@ -0,0 +1,48 @@
|
||||||
|
# SmallTalk [![codecov](https://codecov.io/gh/ouchadam/small-talk/branch/main/graph/badge.svg?token=ETFSLZ9FCI)](https://codecov.io/gh/ouchadam/small-talk)
|
||||||
|
|
||||||
|
`SmallTalk` is a minimal, modern, friends and family focused Android messenger. Heavily inspired by Whatsapp and Signal, powered by Matrix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Project mantra
|
||||||
|
- Tiny app size - currently 1.72mb~ when provided via app bundle.
|
||||||
|
- Focused on reliability and stability.
|
||||||
|
- Bare-bones feature set.
|
||||||
|
|
||||||
|
##### _*Google play only with automatic crash reporting enabled_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Feature list
|
||||||
|
|
||||||
|
- Login with username/password (home servers must serve `${domain}.well-known/matrix/client`)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
#### Planned
|
||||||
|
|
||||||
|
- Device verification (technically supported but has no UI)
|
||||||
|
- Invitations (technically supported but has no UI)
|
||||||
|
- Room history
|
||||||
|
- Message media
|
||||||
|
- Cross signing
|
||||||
|
- Google drive backups
|
||||||
|
- Markdown subset (bold, italic, blocks)
|
||||||
|
- Changing user name/avatar
|
||||||
|
- Room settings and information
|
||||||
|
- Exporting E2E room keys
|
||||||
|
- Local search
|
||||||
|
- Registration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Technical details
|
||||||
|
|
||||||
|
- Built on Jetpack compose and kotlin multiplatform libraries ktor and sqldelight (although the project is not currently setup to be multiplatform until needed).
|
||||||
|
- 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.
|
|
@ -0,0 +1,83 @@
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'kotlin-android'
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCommonAndroidParameters(project)
|
||||||
|
applyCrashlyticsIfRelease(project)
|
||||||
|
|
||||||
|
android {
|
||||||
|
ndkVersion "25.0.8141415"
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "app.dapk.st"
|
||||||
|
versionCode 1
|
||||||
|
versionName "0.0.1-alpha1"
|
||||||
|
resConfigs "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle {
|
||||||
|
abi.enableSplit true
|
||||||
|
density.enableSplit true
|
||||||
|
language.enableSplit true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
matchingFallbacks = ['release']
|
||||||
|
signingConfig.storeFile rootProject.file("tools/debug.keystore")
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||||
|
'proguard/app.pro',
|
||||||
|
"proguard/serializationx.pro",
|
||||||
|
"proguard/olm.pro"
|
||||||
|
signingConfig = buildTypes.debug.signingConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
resources.excludes += "DebugProbesKt.bin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":features:home")
|
||||||
|
implementation project(":features:directory")
|
||||||
|
implementation project(":features:login")
|
||||||
|
implementation project(":features:settings")
|
||||||
|
implementation project(":features:notifications")
|
||||||
|
implementation project(":features:messenger")
|
||||||
|
implementation project(":features:profile")
|
||||||
|
implementation project(":features:navigator")
|
||||||
|
|
||||||
|
implementation project(':domains:store')
|
||||||
|
implementation project(":domains:android:core")
|
||||||
|
implementation project(":domains:android:tracking")
|
||||||
|
implementation project(":domains:android:push")
|
||||||
|
implementation project(":domains:android:work")
|
||||||
|
implementation project(":domains:android:imageloader")
|
||||||
|
implementation project(":domains:olm")
|
||||||
|
|
||||||
|
implementation project(":matrix:matrix")
|
||||||
|
implementation project(":matrix:matrix-http-ktor")
|
||||||
|
implementation project(":matrix:services:auth")
|
||||||
|
implementation project(":matrix:services:sync")
|
||||||
|
implementation project(":matrix:services:room")
|
||||||
|
implementation project(":matrix:services:push")
|
||||||
|
implementation project(":matrix:services:message")
|
||||||
|
implementation project(":matrix:services:device")
|
||||||
|
implementation project(":matrix:services:crypto")
|
||||||
|
implementation project(":matrix:services:profile")
|
||||||
|
|
||||||
|
implementation project(":core")
|
||||||
|
|
||||||
|
implementation Dependencies.google.androidxComposeUi
|
||||||
|
implementation Dependencies.mavenCentral.ktorAndroid
|
||||||
|
implementation Dependencies.mavenCentral.sqldelightAndroid
|
||||||
|
implementation Dependencies.mavenCentral.matrixOlm
|
||||||
|
|
||||||
|
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||||
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
-assumenosideeffects class android.util.Log {
|
||||||
|
v(...);
|
||||||
|
d(...);
|
||||||
|
i(...);
|
||||||
|
w(...);
|
||||||
|
e(...);
|
||||||
|
println(...);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
-keepnames class ** { *; }
|
|
@ -0,0 +1 @@
|
||||||
|
-keepclassmembers class org.matrix.olm.** { *; }
|
|
@ -0,0 +1,16 @@
|
||||||
|
-if @kotlinx.serialization.Serializable class **
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
static <1>$Companion Companion;
|
||||||
|
}
|
||||||
|
|
||||||
|
-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
static **$* *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class <1>$<3> {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
public static <1> INSTANCE;
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="app.dapk.st">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<meta-data
|
||||||
|
android:name="firebase_crashlytics_collection_enabled"
|
||||||
|
android:value="false" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<string tools:ignore="UnusedResources,TypographyDashes" name="com.crashlytics.android.build_id" translatable="false">00000000000000000000000000000000</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="default_web_client_id" translatable="false">390541134533-h9utldf4jb22qd5b6cs2cl8dkoohobjo.apps.googleusercontent.com</string>
|
||||||
|
<string name="gcm_defaultSenderId" translatable="false">390541134533</string>
|
||||||
|
<string name="google_api_key" translatable="false">AIzaSyDS_TVmK-thJeXtLpobR1wwbhqJ1hISVik</string>
|
||||||
|
<string name="google_app_id" translatable="false">1:390541134533:android:3f75d35c4dba1a287b3eac</string>
|
||||||
|
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyDS_TVmK-thJeXtLpobR1wwbhqJ1hISVik</string>
|
||||||
|
<string name="google_storage_bucket" translatable="false">helium-6f01a.appspot.com</string>
|
||||||
|
<string name="project_id" translatable="false">helium-6f01a</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="app.dapk.st">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name="app.dapk.st.SmallTalkApplication"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/dapk_app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/DapkTheme">
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name="app.dapk.st.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:targetActivity="app.dapk.st.home.MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,39 @@
|
||||||
|
package app.dapk.st
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.withIoContext
|
||||||
|
import app.dapk.st.domain.Preferences
|
||||||
|
|
||||||
|
internal class SharedPreferencesDelegate(
|
||||||
|
context: Context,
|
||||||
|
fileName: String,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : Preferences {
|
||||||
|
|
||||||
|
private val preferences by lazy { context.getSharedPreferences(fileName, Context.MODE_PRIVATE) }
|
||||||
|
|
||||||
|
override suspend fun store(key: String, value: String) {
|
||||||
|
coroutineDispatchers.withIoContext {
|
||||||
|
preferences.edit().putString(key, value).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun readString(key: String): String? {
|
||||||
|
return coroutineDispatchers.withIoContext {
|
||||||
|
preferences.getString(key, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(key: String) {
|
||||||
|
coroutineDispatchers.withIoContext {
|
||||||
|
preferences.edit().remove(key).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
coroutineDispatchers.withIoContext {
|
||||||
|
preferences.edit().clear().apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package app.dapk.st
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
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.Scope
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import app.dapk.st.directory.DirectoryModule
|
||||||
|
import app.dapk.st.messenger.MessengerModule
|
||||||
|
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.notifications.NotificationsModule
|
||||||
|
import app.dapk.st.profile.ProfileModule
|
||||||
|
import app.dapk.st.settings.SettingsModule
|
||||||
|
import app.dapk.st.work.TaskRunnerModule
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
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 applicationScope = Scope(Dispatchers.IO)
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
val notificationsModule = featureModules.notificationsModule
|
||||||
|
val storeModule = appModule.storeModule.value
|
||||||
|
val eventLogStore = storeModule.eventLogStore()
|
||||||
|
|
||||||
|
val logger: (String, String) -> Unit = { tag, message ->
|
||||||
|
Log.e(tag, message)
|
||||||
|
GlobalScope.launch {
|
||||||
|
eventLogStore.insert(tag, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachAppLogger(logger)
|
||||||
|
_appLogger = logger
|
||||||
|
|
||||||
|
applicationScope.launch {
|
||||||
|
notificationsModule.firebasePushTokenUseCase().registerCurrentToken()
|
||||||
|
storeModule.localEchoStore.preload()
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationScope.launch {
|
||||||
|
val notificationsUseCase = notificationsModule.notificationsUseCase()
|
||||||
|
notificationsUseCase.listenForNotificationChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
|
||||||
|
override fun <T : ProvidableModule> provide(klass: KClass<T>): T {
|
||||||
|
return when (klass) {
|
||||||
|
DirectoryModule::class -> featureModules.directoryModule
|
||||||
|
LoginModule::class -> featureModules.loginModule
|
||||||
|
HomeModule::class -> featureModules.homeModule
|
||||||
|
SettingsModule::class -> featureModules.settingsModule
|
||||||
|
ProfileModule::class -> featureModules.profileModule
|
||||||
|
NotificationsModule::class -> featureModules.notificationsModule
|
||||||
|
MessengerModule::class -> featureModules.messengerModule
|
||||||
|
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
|
||||||
|
CoreAndroidModule::class -> appModule.coreAndroidModule
|
||||||
|
else -> throw IllegalArgumentException("Unknown: $klass")
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,405 @@
|
||||||
|
package app.dapk.st.graph
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
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.extensions.ErrorTracker
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import app.dapk.st.directory.DirectoryModule
|
||||||
|
import app.dapk.st.domain.StoreModule
|
||||||
|
import app.dapk.st.home.HomeModule
|
||||||
|
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.*
|
||||||
|
import app.dapk.st.matrix.crypto.RoomMembersProvider
|
||||||
|
import app.dapk.st.matrix.crypto.Verification
|
||||||
|
import app.dapk.st.matrix.crypto.cryptoService
|
||||||
|
import app.dapk.st.matrix.crypto.installCryptoService
|
||||||
|
import app.dapk.st.matrix.device.deviceService
|
||||||
|
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.push.installPushService
|
||||||
|
import app.dapk.st.matrix.push.pushService
|
||||||
|
import app.dapk.st.matrix.room.*
|
||||||
|
import app.dapk.st.matrix.sync.*
|
||||||
|
import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent
|
||||||
|
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.notifications.NotificationsModule
|
||||||
|
import app.dapk.st.olm.DeviceKeyFactory
|
||||||
|
import app.dapk.st.olm.OlmPersistenceWrapper
|
||||||
|
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.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 trackingModule by unsafeLazy {
|
||||||
|
TrackingModule(
|
||||||
|
isCrashTrackingEnabled = !BuildConfig.DEBUG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
|
||||||
|
private val database = DapkDb(driver)
|
||||||
|
|
||||||
|
private val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
||||||
|
|
||||||
|
val storeModule = unsafeLazy {
|
||||||
|
StoreModule(
|
||||||
|
database = database,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
val featureModules = FeatureModules(
|
||||||
|
storeModule,
|
||||||
|
matrixModules,
|
||||||
|
domainModules,
|
||||||
|
trackingModule,
|
||||||
|
imageLoaderModule,
|
||||||
|
context,
|
||||||
|
buildMeta,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class FeatureModules internal constructor(
|
||||||
|
private val storeModule: Lazy<StoreModule>,
|
||||||
|
private val matrixModules: MatrixModules,
|
||||||
|
private val domainModules: DomainModules,
|
||||||
|
private val trackingModule: TrackingModule,
|
||||||
|
imageLoaderModule: ImageLoaderModule,
|
||||||
|
context: Context,
|
||||||
|
buildMeta: BuildMeta,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val directoryModule by unsafeLazy {
|
||||||
|
DirectoryModule(
|
||||||
|
syncService = matrixModules.sync,
|
||||||
|
messageService = matrixModules.message,
|
||||||
|
context = context,
|
||||||
|
credentialsStore = storeModule.value.credentialsStore(),
|
||||||
|
roomStore = storeModule.value.roomStore(),
|
||||||
|
roomService = matrixModules.room,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val loginModule by unsafeLazy {
|
||||||
|
LoginModule(
|
||||||
|
matrixModules.auth,
|
||||||
|
domainModules.pushModule,
|
||||||
|
matrixModules.profile,
|
||||||
|
trackingModule.errorTracker
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val messengerModule by unsafeLazy {
|
||||||
|
MessengerModule(
|
||||||
|
matrixModules.sync,
|
||||||
|
matrixModules.message,
|
||||||
|
matrixModules.room,
|
||||||
|
storeModule.value.credentialsStore(),
|
||||||
|
storeModule.value.roomStore(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile) }
|
||||||
|
val settingsModule by unsafeLazy { SettingsModule(storeModule.value, matrixModules.crypto, matrixModules.sync, context.contentResolver, buildMeta) }
|
||||||
|
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile) }
|
||||||
|
val notificationsModule by unsafeLazy {
|
||||||
|
NotificationsModule(
|
||||||
|
matrixModules.push,
|
||||||
|
matrixModules.sync,
|
||||||
|
storeModule.value.credentialsStore(),
|
||||||
|
domainModules.pushModule.registerFirebasePushTokenUseCase(),
|
||||||
|
imageLoaderModule.iconLoader(),
|
||||||
|
storeModule.value.roomStore(),
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MatrixModules(
|
||||||
|
private val storeModule: Lazy<StoreModule>,
|
||||||
|
private val trackingModule: TrackingModule,
|
||||||
|
private val workModule: WorkModule,
|
||||||
|
private val logger: MatrixLogger,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val matrix by unsafeLazy {
|
||||||
|
val store = storeModule.value
|
||||||
|
val credentialsStore = store.credentialsStore()
|
||||||
|
MatrixClient(
|
||||||
|
KtorMatrixHttpClientFactory(
|
||||||
|
credentialsStore,
|
||||||
|
includeLogging = true
|
||||||
|
),
|
||||||
|
logger
|
||||||
|
).also {
|
||||||
|
it.install {
|
||||||
|
installAuthService(credentialsStore)
|
||||||
|
installEncryptionService(store.knownDevicesStore())
|
||||||
|
|
||||||
|
val olmAccountStore = OlmPersistenceWrapper(store.olmStore())
|
||||||
|
val singletonFlows = SingletonFlows()
|
||||||
|
val olm = OlmWrapper(
|
||||||
|
olmStore = olmAccountStore,
|
||||||
|
singletonFlows = singletonFlows,
|
||||||
|
jsonCanonicalizer = JsonCanonicalizer(),
|
||||||
|
deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()),
|
||||||
|
errorTracker = trackingModule.errorTracker,
|
||||||
|
logger = logger,
|
||||||
|
clock = Clock.systemUTC(),
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
installCryptoService(
|
||||||
|
credentialsStore,
|
||||||
|
olm,
|
||||||
|
roomMembersProvider = { services ->
|
||||||
|
RoomMembersProvider {
|
||||||
|
services.roomService().joinedMembers(it).map { it.userId }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler())) { serviceProvider ->
|
||||||
|
MessageEncrypter { message ->
|
||||||
|
val result = serviceProvider.cryptoService().encrypt(
|
||||||
|
roomId = when (message) {
|
||||||
|
is MessageService.Message.TextMessage -> message.roomId
|
||||||
|
},
|
||||||
|
credentials = credentialsStore.credentials()!!,
|
||||||
|
when (message) {
|
||||||
|
is MessageService.Message.TextMessage -> JsonString(
|
||||||
|
MatrixHttpClient.jsonWithDefaults.encodeToString(
|
||||||
|
ApiMessage.TextMessage.serializer(),
|
||||||
|
ApiMessage.TextMessage(
|
||||||
|
ApiMessage.TextMessage.TextContent(
|
||||||
|
message.content.body,
|
||||||
|
message.content.type,
|
||||||
|
), message.roomId, type = EventType.ROOM_MESSAGE.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
MessageEncrypter.EncryptedMessagePayload(
|
||||||
|
result.algorithmName,
|
||||||
|
result.senderKey,
|
||||||
|
result.cipherText,
|
||||||
|
result.sessionId,
|
||||||
|
result.deviceId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installRoomService(
|
||||||
|
storeModule.value.memberStore(),
|
||||||
|
roomMessenger = {
|
||||||
|
val messageService = it.messageService()
|
||||||
|
object : RoomMessenger {
|
||||||
|
override suspend fun enableEncryption(roomId: RoomId) {
|
||||||
|
messageService.sendEventMessage(
|
||||||
|
roomId, MessageService.EventMessage.Encryption(
|
||||||
|
algorithm = AlgorithmName("m.megolm.v1.aes-sha2")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
installProfileService(storeModule.value.profileStore(), singletonFlows, credentialsStore)
|
||||||
|
|
||||||
|
installSyncService(
|
||||||
|
credentialsStore,
|
||||||
|
store.overviewStore(),
|
||||||
|
store.roomStore(),
|
||||||
|
store.syncStore(),
|
||||||
|
store.filterStore(),
|
||||||
|
messageDecrypter = { serviceProvider ->
|
||||||
|
val cryptoService = serviceProvider.cryptoService()
|
||||||
|
MessageDecrypter {
|
||||||
|
cryptoService.decrypt(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keySharer = { serviceProvider ->
|
||||||
|
val cryptoService = serviceProvider.cryptoService()
|
||||||
|
KeySharer { sharedRoomKeys ->
|
||||||
|
cryptoService.importRoomKeys(sharedRoomKeys)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verificationHandler = { services ->
|
||||||
|
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
|
||||||
|
val cryptoService = services.cryptoService()
|
||||||
|
VerificationHandler { apiEvent ->
|
||||||
|
cryptoService.onVerificationEvent(
|
||||||
|
when (apiEvent) {
|
||||||
|
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
|
||||||
|
apiEvent.sender,
|
||||||
|
apiEvent.content.fromDevice,
|
||||||
|
apiEvent.content.transactionId,
|
||||||
|
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,
|
||||||
|
apiEvent.content.method,
|
||||||
|
apiEvent.content.protocols,
|
||||||
|
apiEvent.content.hashes,
|
||||||
|
apiEvent.content.codes,
|
||||||
|
apiEvent.content.short,
|
||||||
|
apiEvent.content.transactionId,
|
||||||
|
)
|
||||||
|
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
||||||
|
is ApiToDeviceEvent.VerificationAccept -> TODO()
|
||||||
|
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
||||||
|
apiEvent.sender,
|
||||||
|
apiEvent.content.transactionId,
|
||||||
|
apiEvent.content.key
|
||||||
|
)
|
||||||
|
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
||||||
|
apiEvent.sender,
|
||||||
|
apiEvent.content.transactionId,
|
||||||
|
apiEvent.content.keys,
|
||||||
|
apiEvent.content.mac,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
cryptoService.maybeCreateMoreKeys(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
roomMembersService = { services ->
|
||||||
|
val roomService = services.roomService()
|
||||||
|
object : RoomMembersService {
|
||||||
|
override suspend fun find(roomId: RoomId, userIds: List<UserId>) = roomService.findMembers(roomId, userIds)
|
||||||
|
override suspend fun insert(roomId: RoomId, members: List<RoomMember>) = roomService.insertMembers(roomId, members)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorTracker = trackingModule.errorTracker,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
|
||||||
|
installPushService(credentialsStore)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val auth by unsafeLazy { matrix.authService() }
|
||||||
|
val push by unsafeLazy { matrix.pushService() }
|
||||||
|
val sync by unsafeLazy { matrix.syncService() }
|
||||||
|
val message by unsafeLazy { matrix.messageService() }
|
||||||
|
val room by unsafeLazy { matrix.roomService() }
|
||||||
|
val profile by unsafeLazy { matrix.profileService() }
|
||||||
|
val crypto by unsafeLazy { matrix.cryptoService() }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DomainModules(
|
||||||
|
private val matrixModules: MatrixModules,
|
||||||
|
private val errorTracker: ErrorTracker,
|
||||||
|
) {
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskRunnerAdapter(private val matrixTaskRunner: suspend (MatrixTask) -> MatrixTaskRunner.TaskResult) : TaskRunner {
|
||||||
|
|
||||||
|
override suspend fun run(tasks: List<TaskRunner.RunnableWorkTask>): List<TaskRunner.TaskResult> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="strict" />
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<string name="dapk_app_name">SmallTalk</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="DapkTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:forceDarkAllowed">false</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="default_web_client_id" translatable="false">390541134533-h9utldf4jb22qd5b6cs2cl8dkoohobjo.apps.googleusercontent.com</string>
|
||||||
|
<string name="gcm_defaultSenderId" translatable="false">390541134533</string>
|
||||||
|
<string name="google_api_key" translatable="false">AIzaSyDS_TVmK-thJeXtLpobR1wwbhqJ1hISVik</string>
|
||||||
|
<string name="google_app_id" translatable="false">1:390541134533:android:3f75d35c4dba1a287b3eac</string>
|
||||||
|
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyDS_TVmK-thJeXtLpobR1wwbhqJ1hISVik</string>
|
||||||
|
<string name="google_storage_bucket" translatable="false">helium-6f01a.appspot.com</string>
|
||||||
|
<string name="project_id" translatable="false">helium-6f01a</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,141 @@
|
||||||
|
buildscript {
|
||||||
|
apply from: "dependencies.gradle"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
Dependencies._repositories.call(it)
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath Dependencies.google.androidGradlePlugin
|
||||||
|
classpath Dependencies.mavenCentral.kotlinGradlePlugin
|
||||||
|
classpath Dependencies.mavenCentral.sqldelightGradlePlugin
|
||||||
|
classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin
|
||||||
|
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def launchTask = getGradle()
|
||||||
|
.getStartParameter()
|
||||||
|
.getTaskRequests()
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
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',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.applyMatrixServiceModule = { project ->
|
||||||
|
project.apply plugin: 'kotlin'
|
||||||
|
project.apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
|
||||||
|
|
||||||
|
def dependencies = project.dependencies
|
||||||
|
|
||||||
|
dependencies.api project.project(":matrix:matrix")
|
||||||
|
dependencies.api project.project(":matrix:common")
|
||||||
|
dependencies.implementation project.project(":matrix:matrix-http")
|
||||||
|
dependencies.implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.applyLibraryPlugins = { project ->
|
||||||
|
project.apply plugin: 'com.android.library'
|
||||||
|
project.apply plugin: 'kotlin-android'
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.applyCommonAndroidParameters = { project ->
|
||||||
|
def android = project.android
|
||||||
|
android.compileSdk 31
|
||||||
|
android.compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
incremental = true
|
||||||
|
}
|
||||||
|
android.defaultConfig {
|
||||||
|
minSdkVersion 29
|
||||||
|
targetSdkVersion 31
|
||||||
|
}
|
||||||
|
|
||||||
|
android.buildFeatures.compose = true
|
||||||
|
android.composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = Dependencies.google.kotlinCompilerExtensionVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.applyLibraryModuleOptimisations = { project ->
|
||||||
|
project.android {
|
||||||
|
variantFilter { variant ->
|
||||||
|
if (variant.name == "debug") {
|
||||||
|
variant.ignore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = false
|
||||||
|
dataBinding = false
|
||||||
|
aidl = false
|
||||||
|
renderScript = false
|
||||||
|
resValues = false
|
||||||
|
shaders = false
|
||||||
|
viewBinding = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.applyCompose = { project ->
|
||||||
|
def dependencies = project.dependencies
|
||||||
|
|
||||||
|
dependencies.implementation Dependencies.google.androidxComposeUi
|
||||||
|
dependencies.implementation Dependencies.google.androidxComposeFoundation
|
||||||
|
dependencies.implementation Dependencies.google.androidxComposeMaterial
|
||||||
|
dependencies.implementation Dependencies.google.androidxComposeIconsExtended
|
||||||
|
dependencies.implementation Dependencies.google.androidxActivityCompose
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
project.tasks.withType(com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask).configureEach {
|
||||||
|
it.googleServicesResourceRoot.set(project.file("src/release/res/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
||||||
|
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.kotlinFixtures = { dependencies ->
|
||||||
|
dependencies.testFixturesImplementation 'io.mockk:mockk:1.12.2'
|
||||||
|
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (launchTask.contains("codeCoverageReport".toLowerCase())) {
|
||||||
|
apply from: 'tools/coverage.gradle'
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
plugins {
|
||||||
|
id 'kotlin'
|
||||||
|
id 'java-test-fixtures'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
|
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
|
testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||||
|
testFixturesImplementation 'io.mockk:mockk:1.12.2'
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
data class BuildMeta(
|
||||||
|
val versionName: String,
|
||||||
|
)
|
|
@ -0,0 +1,15 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
data class CoroutineDispatchers(val io: CoroutineDispatcher = Dispatchers.IO, val global: CoroutineScope = GlobalScope)
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineDispatchers.withIoContext(
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
) = withContext(this.io, block)
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineDispatchers.withIoContextAsync(
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
): Deferred<T> = withContext(this.io) {
|
||||||
|
async { block() }
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
enum class AppLogTag(val key: String) {
|
||||||
|
NOTIFICATION("notification"),
|
||||||
|
PERFORMANCE("performance"),
|
||||||
|
PUSH("push"),
|
||||||
|
ERROR_NON_FATAL("error - non fatal"),
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias AppLogger = (tag: String, message: String) -> Unit
|
||||||
|
|
||||||
|
private var appLoggerInstance: AppLogger? = null
|
||||||
|
|
||||||
|
fun attachAppLogger(logger: AppLogger) {
|
||||||
|
appLoggerInstance = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
fun log(tag: AppLogTag, message: Any) {
|
||||||
|
appLoggerInstance?.invoke(tag.key, message.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> logP(area: String, block: suspend () -> T): T {
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
return block().also {
|
||||||
|
val timeTaken = System.currentTimeMillis() - start
|
||||||
|
log(AppLogTag.PERFORMANCE, "$area: took $timeTaken ms")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
sealed interface Lce<T> {
|
||||||
|
class Loading<T> : Lce<T>
|
||||||
|
data class Error<T>(val cause: Throwable) : Lce<T>
|
||||||
|
data class Content<T>(val value: T) : Lce<T>
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
interface ModuleProvider {
|
||||||
|
|
||||||
|
fun <T: ProvidableModule> provide(klass: KClass<T>): T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProvidableModule
|
|
@ -0,0 +1,47 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class SingletonFlows {
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private val cache = mutableMapOf<String, MutableSharedFlow<*>>()
|
||||||
|
private val started = ConcurrentHashMap<String, Boolean?>()
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
suspend fun <T> getOrPut(key: String, onStart: suspend () -> T): Flow<T> {
|
||||||
|
return when (val flow = cache[key]) {
|
||||||
|
null -> mutex.withLock {
|
||||||
|
cache.getOrPut(key) {
|
||||||
|
MutableSharedFlow<T>(replay = 1).also {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
async {
|
||||||
|
it.emit(onStart())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as Flow<T>
|
||||||
|
}
|
||||||
|
else -> flow as Flow<T>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> get(key: String): Flow<T> {
|
||||||
|
return cache[key]!! as Flow<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> update(key: String, value: T) {
|
||||||
|
(cache[key] as? MutableSharedFlow<T>)?.emit(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(key: String) {
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
|
interface ErrorTracker {
|
||||||
|
fun track(throwable: Throwable, extra: String = "")
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CrashScope {
|
||||||
|
val errorTracker: ErrorTracker
|
||||||
|
fun <T> Result<T>.trackFailure() = this.onFailure { errorTracker.track(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> ErrorTracker.nullAndTrack(throwable: Throwable, extra: String = ""): T? {
|
||||||
|
this.track(throwable, extra)
|
||||||
|
return null
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
|
import kotlinx.coroutines.InternalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
|
|
||||||
|
@OptIn(InternalCoroutinesApi::class)
|
||||||
|
suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolean): T? {
|
||||||
|
var counter = 0
|
||||||
|
|
||||||
|
var result: T? = null
|
||||||
|
this
|
||||||
|
.takeWhile {
|
||||||
|
counter++
|
||||||
|
!predicate(it) || counter < (count + 1)
|
||||||
|
}
|
||||||
|
.filter { predicate(it) }
|
||||||
|
.collect {
|
||||||
|
result = it
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
|
inline fun <T> T?.ifNull(block: () -> T): T = this ?: block()
|
||||||
|
inline fun <T> ifOrNull(condition: Boolean, block: () -> T): T? = if (condition) block() else null
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
inline fun <T, T1 : T, T2 : T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean, predicate2: (T) -> Boolean): Pair<T1, T2>? {
|
||||||
|
var firstValue: T1? = null
|
||||||
|
var secondValue: T2? = null
|
||||||
|
|
||||||
|
for (element in this) {
|
||||||
|
if (firstValue == null && predicate(element)) {
|
||||||
|
firstValue = element as T1
|
||||||
|
}
|
||||||
|
if (secondValue == null && predicate2(element)) {
|
||||||
|
secondValue = element as T2
|
||||||
|
}
|
||||||
|
if (firstValue != null && secondValue != null) return firstValue to secondValue
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)
|
|
@ -0,0 +1,11 @@
|
||||||
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
|
||||||
|
fun <T> Lce<T>.takeIfContent(): T? {
|
||||||
|
return when (this) {
|
||||||
|
is Lce.Content -> this.value
|
||||||
|
is Lce.Error -> null
|
||||||
|
is Lce.Loading -> null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
|
inline fun <T, R> List<T>.ifNotEmpty(transform: (List<T>) -> List<R>) = if (this.isEmpty()) emptyList() else transform(this)
|
|
@ -0,0 +1,30 @@
|
||||||
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
class Scope(
|
||||||
|
dispatcher: CoroutineDispatcher
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val coroutineScope = CoroutineScope(dispatcher + job)
|
||||||
|
|
||||||
|
fun launch(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
|
): Job {
|
||||||
|
return coroutineScope.launch(context, start, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeErrorTracker : ErrorTracker by mockk(relaxed = true)
|
|
@ -0,0 +1,15 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import io.mockk.*
|
||||||
|
|
||||||
|
inline fun <T : Any, reified R> T.expect(crossinline block: suspend MockKMatcherScope.(T) -> R) {
|
||||||
|
coEvery { block(this@expect) } returns mockk(relaxed = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T, B> MockKStubScope<T, B>.delegateReturn(): Returns<T> = Returns { value ->
|
||||||
|
answers(ConstantAnswer(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface Returns<T> {
|
||||||
|
fun returns(value: T)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package test
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
|
||||||
|
class TestSharedFlow<T>(
|
||||||
|
private val instance: MutableSharedFlow<T> = MutableSharedFlow()
|
||||||
|
) : MutableSharedFlow<T> by instance {
|
||||||
|
|
||||||
|
private val values = mutableListOf<T>()
|
||||||
|
|
||||||
|
override suspend fun emit(value: T) {
|
||||||
|
values.add(value)
|
||||||
|
instance.emit(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun tryEmit(value: T): Boolean {
|
||||||
|
values.add(value)
|
||||||
|
return instance.tryEmit(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertNoValues() {
|
||||||
|
values shouldBeEqualTo emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertValues(vararg expected: T) {
|
||||||
|
this.values shouldBeEqualTo expected.toList()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
ext.Dependencies = new DependenciesContainer()
|
||||||
|
|
||||||
|
ext.Dependencies.with {
|
||||||
|
_repositories = { repositories ->
|
||||||
|
repositories.google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "com\\.android.*"
|
||||||
|
includeGroupByRegex "com\\.google.*"
|
||||||
|
includeGroupByRegex "androidx\\..*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories.mavenCentral {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "org\\.jetbrains.*"
|
||||||
|
includeGroupByRegex "com\\.google.*"
|
||||||
|
includeGroupByRegex "com\\.squareup.*"
|
||||||
|
includeGroupByRegex "com\\.android.*"
|
||||||
|
includeGroupByRegex "org\\.apache.*"
|
||||||
|
includeGroupByRegex "org\\.json.*"
|
||||||
|
includeGroupByRegex "org\\.codehaus.*"
|
||||||
|
includeGroupByRegex "org\\.jdom.*"
|
||||||
|
includeGroupByRegex "com\\.fasterxml.*"
|
||||||
|
includeGroupByRegex "com\\.sun.*"
|
||||||
|
includeGroupByRegex "org\\.ow2.*"
|
||||||
|
includeGroupByRegex "org\\.eclipse.*"
|
||||||
|
includeGroup "app.cash.turbine"
|
||||||
|
includeGroup "de.undercouch"
|
||||||
|
includeGroup "de.danielbechler"
|
||||||
|
includeGroup "com.github.gundy"
|
||||||
|
includeGroup "com.sun.activation"
|
||||||
|
includeGroup "com.thoughtworks.qdox"
|
||||||
|
includeGroup "com.annimon"
|
||||||
|
includeGroup "com.github.javaparser"
|
||||||
|
includeGroup "com.beust"
|
||||||
|
includeGroup "org.bouncycastle"
|
||||||
|
includeGroup "org.bitbucket.b_c"
|
||||||
|
includeGroup "org.checkerframework"
|
||||||
|
includeGroup "org.amshove.kluent"
|
||||||
|
includeGroup "org.jvnet.staxex"
|
||||||
|
includeGroup "org.glassfish"
|
||||||
|
includeGroup "org.glassfish.jaxb"
|
||||||
|
includeGroup "org.antlr"
|
||||||
|
includeGroup "org.tensorflow"
|
||||||
|
includeGroup "org.xerial"
|
||||||
|
includeGroup "org.slf4j"
|
||||||
|
includeGroup "org.freemarker"
|
||||||
|
includeGroup "org.threeten"
|
||||||
|
includeGroup "org.hamcrest"
|
||||||
|
includeGroup "org.matrix.android"
|
||||||
|
includeGroup "org.sonatype.oss"
|
||||||
|
includeGroup "org.junit.jupiter"
|
||||||
|
includeGroup "org.junit.platform"
|
||||||
|
includeGroup "org.junit"
|
||||||
|
includeGroup "org.junit.jupiter"
|
||||||
|
includeGroup "org.jsoup"
|
||||||
|
includeGroup "org.jacoco"
|
||||||
|
includeGroup "org.testng"
|
||||||
|
includeGroup "org.opentest4j"
|
||||||
|
includeGroup "org.apiguardian"
|
||||||
|
includeGroup "org.webjars"
|
||||||
|
includeGroup "org.objenesis"
|
||||||
|
includeGroup "commons-io"
|
||||||
|
includeGroup "commons-logging"
|
||||||
|
includeGroup "commons-codec"
|
||||||
|
includeGroup "net.java.dev.jna"
|
||||||
|
includeGroup "net.sf.jopt-simple"
|
||||||
|
includeGroup "net.sf.kxml"
|
||||||
|
includeGroup "net.bytebuddy"
|
||||||
|
includeGroup "net.java"
|
||||||
|
includeGroup "it.unimi.dsi"
|
||||||
|
includeGroup "io.grpc"
|
||||||
|
includeGroup "io.netty"
|
||||||
|
includeGroup "io.opencensus"
|
||||||
|
includeGroup "io.ktor"
|
||||||
|
includeGroup "io.coil-kt"
|
||||||
|
includeGroup "io.mockk"
|
||||||
|
includeGroup "info.picocli"
|
||||||
|
includeGroup "us.fatehi"
|
||||||
|
includeGroup "jakarta.xml.bind"
|
||||||
|
includeGroup "jakarta.activation"
|
||||||
|
includeGroup "javax.inject"
|
||||||
|
includeGroup "junit"
|
||||||
|
includeGroup "jline"
|
||||||
|
includeGroup "xerces"
|
||||||
|
includeGroup "xml-apis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def kotlinVer = "1.6.10"
|
||||||
|
def sqldelightVer = "1.5.3"
|
||||||
|
def composeVer = "1.1.0"
|
||||||
|
|
||||||
|
google = new DependenciesContainer()
|
||||||
|
google.with {
|
||||||
|
androidGradlePlugin = "com.android.tools.build:gradle:7.1.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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
|
||||||
|
junit = "junit:junit:4.13.2"
|
||||||
|
kluent = "org.amshove.kluent:kluent:1.68"
|
||||||
|
|
||||||
|
matrixOlm = "org.matrix.android:olm-sdk:3.2.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DependenciesContainer extends GroovyObjectSupport {
|
||||||
|
|
||||||
|
private final Map<String, Object> storage = new HashMap<String, Object>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Object getProperty(String name) {
|
||||||
|
return storage.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setProperty(String name, Object newValue) {
|
||||||
|
storage.put(name, newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":core")
|
||||||
|
implementation("io.coil-kt:coil-compose:1.4.0")
|
||||||
|
implementation "com.google.accompanist:accompanist-systemuicontroller:0.24.1-alpha"
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="app.dapk.st.design"/>
|
|
@ -0,0 +1,9 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
fun Configuration.percentOfHeight(float: Float): Dp {
|
||||||
|
return (this.screenHeightDp * float).dp
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.transform.CircleCropTransformation
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUnitApi::class)
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.CircleishAvatar(avatarUrl: String?, fallbackLabel: String, size: Dp) {
|
||||||
|
when (avatarUrl) {
|
||||||
|
null -> {
|
||||||
|
val colors = SmallTalkTheme.extendedColors.getMissingImageColor(fallbackLabel)
|
||||||
|
Box(
|
||||||
|
Modifier.align(Alignment.Center)
|
||||||
|
.background(color = colors.first, shape = CircleShape)
|
||||||
|
.size(size),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = fallbackLabel.uppercase().first().toString(),
|
||||||
|
color = colors.second,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = TextUnit(size.value * 0.5f, TextUnitType.Sp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Image(
|
||||||
|
painter = rememberImagePainter(
|
||||||
|
data = avatarUrl,
|
||||||
|
builder = {
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(size).align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
Text(
|
||||||
|
text = (displayName).first().toString().uppercase(),
|
||||||
|
color = colors.second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessengerUrlIcon(avatarUrl: String, displayImageSize: Dp) {
|
||||||
|
Image(
|
||||||
|
painter = rememberImagePainter(
|
||||||
|
data = avatarUrl,
|
||||||
|
builder = {
|
||||||
|
transformations(CircleCropTransformation())
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(displayImageSize)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.DropdownMenu
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OverflowMenu(content: @Composable () -> Unit) {
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false },
|
||||||
|
offset = DpOffset(0.dp, (-72).dp)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
IconButton(onClick = {
|
||||||
|
showMenu = !showMenu
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.MoreVert,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?) -> Unit, graph: SpiderScope.() -> Unit) {
|
||||||
|
val pageCache = remember { mutableMapOf<Route<*>, SpiderPage<out T>>() }
|
||||||
|
pageCache[currentPage.route] = currentPage
|
||||||
|
|
||||||
|
val computedWeb = remember(true) {
|
||||||
|
mutableMapOf<Route<*>, @Composable (T) -> Unit>().also { computedWeb ->
|
||||||
|
val scope = object : SpiderScope {
|
||||||
|
override fun <T> item(route: Route<T>, content: @Composable (T) -> Unit) {
|
||||||
|
computedWeb[route] = { content(it as T) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.invoke(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val navigateAndPopStack = {
|
||||||
|
pageCache.remove(currentPage.route)
|
||||||
|
onNavigate(pageCache[currentPage.parent])
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Toolbar(
|
||||||
|
onNavigate = navigateAndPopStack,
|
||||||
|
title = currentPage.label
|
||||||
|
)
|
||||||
|
|
||||||
|
currentPage.parent?.let {
|
||||||
|
BackHandler(onBack = navigateAndPopStack)
|
||||||
|
}
|
||||||
|
computedWeb[currentPage.route]!!.invoke(currentPage.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface SpiderScope {
|
||||||
|
fun <T> item(route: Route<T>, content: @Composable (T) -> Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SpiderPage<T>(
|
||||||
|
val route: Route<T>,
|
||||||
|
val label: String,
|
||||||
|
val parent: Route<*>?,
|
||||||
|
val state: T,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class Route<S>(val value: String)
|
|
@ -0,0 +1,71 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.darkColors
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
private object Palette {
|
||||||
|
val brandPrimary = Color(0xFFb41cca)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DARK_COLOURS = darkColors(
|
||||||
|
primary = Palette.brandPrimary,
|
||||||
|
onPrimary = Color(0xDDFFFFFF),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LIGHT_COLOURS = DARK_COLOURS
|
||||||
|
|
||||||
|
private val DARK_EXTENDED = ExtendedColors(
|
||||||
|
selfBubble = DARK_COLOURS.primary,
|
||||||
|
onSelfBubble = DARK_COLOURS.onPrimary,
|
||||||
|
othersBubble = Color(0x20EDEDED),
|
||||||
|
onOthersBubble = Color(0xFF000000),
|
||||||
|
missingImageColors = listOf(
|
||||||
|
Color(0xFFf7c7f7) to Color(0xFFdf20de),
|
||||||
|
Color(0xFFe5d7f6) to Color(0xFF7b30cf),
|
||||||
|
Color(0xFFf6c8cb) to Color(0xFFda2535),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
private val LIGHT_EXTENDED = DARK_EXTENDED
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ExtendedColors(
|
||||||
|
val selfBubble: Color,
|
||||||
|
val onSelfBubble: Color,
|
||||||
|
val othersBubble: Color,
|
||||||
|
val onOthersBubble: Color,
|
||||||
|
val missingImageColors: List<Pair<Color, Color>>,
|
||||||
|
) {
|
||||||
|
fun getMissingImageColor(key: String): Pair<Color, Color> {
|
||||||
|
return missingImageColors[key.hashCode().absoluteValue % (missingImageColors.size)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val LocalExtendedColors = staticCompositionLocalOf { LIGHT_EXTENDED }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SmallTalkTheme(content: @Composable () -> Unit) {
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val systemInDarkTheme = isSystemInDarkTheme()
|
||||||
|
MaterialTheme(
|
||||||
|
colors = if (systemInDarkTheme) DARK_COLOURS else LIGHT_COLOURS,
|
||||||
|
) {
|
||||||
|
val backgroundColor = MaterialTheme.colors.background
|
||||||
|
SideEffect {
|
||||||
|
systemUiController.setSystemBarsColor(backgroundColor)
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalExtendedColors provides if (systemInDarkTheme) DARK_EXTENDED else LIGHT_EXTENDED) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SmallTalkTheme {
|
||||||
|
val extendedColors: ExtendedColors
|
||||||
|
@Composable
|
||||||
|
get() = LocalExtendedColors.current
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
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.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.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.Toolbar(onNavigate: () -> Unit, title: String? = null, actions: @Composable RowScope.() -> Unit = {}) {
|
||||||
|
TopAppBar(
|
||||||
|
modifier = Modifier.height(72.dp),
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onNavigate() }) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = title?.let {
|
||||||
|
{ Text(it, maxLines = 2) }
|
||||||
|
} ?: {},
|
||||||
|
actions = actions,
|
||||||
|
elevation = 0.dp
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black.copy(alpha = 0.2f), thickness = 0.5.dp)
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.Divider
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
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) {
|
||||||
|
val modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = onClick != null) { onClick?.invoke() }) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Column(modifier) {
|
||||||
|
when (content) {
|
||||||
|
null -> {
|
||||||
|
Text(text = title, fontSize = 18.sp)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Text(text = title, fontSize = 12.sp)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
Text(text = content, fontSize = 18.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
if (includeDivider) {
|
||||||
|
Divider(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconRow(icon: ImageVector, title: String, onClick: (() -> Unit)? = null) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = onClick != null) { onClick?.invoke() }
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(icon, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(text = title, fontSize = 18.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsTextRow(title: String, subtitle: String?, onClick: (() -> Unit)?) {
|
||||||
|
TextRow(title = title, subtitle, includeDivider = false, onClick)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":core")
|
||||||
|
implementation project(":features:navigator")
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="app.dapk.st.core"/>
|
|
@ -0,0 +1,19 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelLazy
|
||||||
|
import androidx.lifecycle.ViewModelProvider.*
|
||||||
|
|
||||||
|
inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
|
||||||
|
noinline factory: () -> VM
|
||||||
|
): Lazy<VM> {
|
||||||
|
val factoryPromise = object : Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>) = when (modelClass) {
|
||||||
|
VM::class.java -> factory() as T
|
||||||
|
else -> throw Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ViewModelLazy(VM::class, { viewModelStore }, { factoryPromise })
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StartObserving(block: StartScope.() -> Unit) {
|
||||||
|
LaunchedEffect(true) {
|
||||||
|
block(StartScope(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartScope(private val scope: CoroutineScope) {
|
||||||
|
|
||||||
|
fun <T> SharedFlow<T>.launch(onEach: suspend (T) -> Unit) {
|
||||||
|
this.onEach(onEach).launchIn(scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EffectScope {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnceEffect(key: Any, sideEffect: () -> Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LifecycleEffect(onStart: () -> Unit = {}, onStop: () -> Unit = {}) {
|
||||||
|
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||||
|
DisposableEffect(lifecycleOwner.value) {
|
||||||
|
val lifecycleObserver = LifecycleEventObserver { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_START -> onStart()
|
||||||
|
Lifecycle.Event.ON_STOP -> onStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleOwner.value.lifecycle.addObserver(lifecycleObserver)
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
lifecycleOwner.value.lifecycle.removeObserver(lifecycleObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
inline fun <reified T : ProvidableModule> Context.module() =
|
||||||
|
(this.applicationContext as ModuleProvider).provide(T::class)
|
|
@ -0,0 +1,9 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import app.dapk.st.navigator.IntentFactory
|
||||||
|
|
||||||
|
class CoreAndroidModule(private val intentFactory: IntentFactory): ProvidableModule {
|
||||||
|
|
||||||
|
fun intentFactory() = intentFactory
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import app.dapk.st.navigator.navigator
|
||||||
|
|
||||||
|
abstract class DapkActivity : ComponentActivity(), EffectScope {
|
||||||
|
|
||||||
|
private val coreAndroidModule by unsafeLazy { module<CoreAndroidModule>() }
|
||||||
|
private val remembers = mutableMapOf<Any, Any>()
|
||||||
|
protected val navigator by navigator { coreAndroidModule.intentFactory() }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun OnceEffect(key: Any, sideEffect: () -> Unit) {
|
||||||
|
val triggerSideEffect = remembers.containsKey(key).not()
|
||||||
|
if (triggerSideEffect) {
|
||||||
|
remembers[key] = Unit
|
||||||
|
SideEffect {
|
||||||
|
sideEffect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
||||||
|
abstract class DapkViewModel<S, VE>(
|
||||||
|
initialState: S
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
protected val _events = MutableSharedFlow<VE>(extraBufferCapacity = 1)
|
||||||
|
val events: SharedFlow<VE> = _events
|
||||||
|
var state by mutableStateOf<S>(initialState)
|
||||||
|
protected set
|
||||||
|
|
||||||
|
fun updateState(reducer: S.() -> S) {
|
||||||
|
state = reducer(state)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package app.dapk.st.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Header(label: String) {
|
||||||
|
Box(Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp)) {
|
||||||
|
Text(text = label.uppercase(), fontWeight = FontWeight.Bold, fontSize = 12.sp, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CenteredLoading() {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator(Modifier.wrapContentSize())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":core")
|
||||||
|
implementation "io.coil-kt:coil:1.4.0"
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="app.dapk.st.imageloader"/>
|
|
@ -0,0 +1,62 @@
|
||||||
|
package app.dapk.st.imageloader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.transform.CircleCropTransformation
|
||||||
|
import coil.transform.Transformation
|
||||||
|
import coil.load as coilLoad
|
||||||
|
|
||||||
|
interface ImageLoader {
|
||||||
|
|
||||||
|
suspend fun load(url: String, transformation: Transformation? = null): Drawable?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IconLoader {
|
||||||
|
|
||||||
|
suspend fun load(url: String): Icon?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CachedIcons(private val imageLoader: ImageLoader) : IconLoader {
|
||||||
|
|
||||||
|
private val circleCrop = CircleCropTransformation()
|
||||||
|
private val cache = mutableMapOf<String, Icon?>()
|
||||||
|
|
||||||
|
override suspend fun load(url: String): Icon? {
|
||||||
|
return cache.getOrPut(url) {
|
||||||
|
imageLoader.load(url, transformation = circleCrop)?.toBitmap()?.let {
|
||||||
|
Icon.createWithBitmap(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class CoilImageLoader(private val context: Context) : ImageLoader {
|
||||||
|
|
||||||
|
private val coil = context.imageLoader
|
||||||
|
|
||||||
|
override suspend fun load(url: String, transformation: Transformation?): Drawable? {
|
||||||
|
val request = ImageRequest.Builder(context)
|
||||||
|
.data(url)
|
||||||
|
.let {
|
||||||
|
when (transformation) {
|
||||||
|
null -> it
|
||||||
|
else -> it.transformations(transformation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return coil.execute(request).drawable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ImageView.load(url: String) {
|
||||||
|
this.coilLoad(url)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package app.dapk.st.imageloader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
|
||||||
|
class ImageLoaderModule(
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val imageLoader by unsafeLazy { CoilImageLoader(context) }
|
||||||
|
|
||||||
|
private val cachedIcons by unsafeLazy { CachedIcons(imageLoader) }
|
||||||
|
|
||||||
|
fun iconLoader(): IconLoader = cachedIcons
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':core')
|
||||||
|
implementation project(':matrix:services:push')
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||||
|
implementation 'com.google.firebase:firebase-messaging'
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="app.dapk.st.push"/>
|
|
@ -0,0 +1,18 @@
|
||||||
|
package app.dapk.st.push
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
suspend fun FirebaseMessaging.token() = suspendCoroutine<String> { continuation ->
|
||||||
|
this.token.addOnCompleteListener { task ->
|
||||||
|
when {
|
||||||
|
task.isSuccessful -> continuation.resume(task.result!!)
|
||||||
|
task.isCanceled -> continuation.resumeWith(Result.failure(CancelledTokenFetchingException()))
|
||||||
|
else -> continuation.resumeWith(Result.failure(task.exception ?: UnknownTokenFetchingFailedException()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CancelledTokenFetchingException : Throwable()
|
||||||
|
private class UnknownTokenFetchingFailedException : Throwable()
|
|
@ -0,0 +1,16 @@
|
||||||
|
package app.dapk.st.push
|
||||||
|
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
|
import app.dapk.st.matrix.push.PushService
|
||||||
|
|
||||||
|
class PushModule(
|
||||||
|
private val pushService: PushService,
|
||||||
|
private val errorTracker: ErrorTracker,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun registerFirebasePushTokenUseCase() = RegisterFirebasePushTokenUseCase(
|
||||||
|
pushService,
|
||||||
|
errorTracker,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':core')
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||||
|
implementation 'com.google.firebase:firebase-crashlytics'
|
||||||
|
|
||||||
|
// is it worth the 400kb size increase?
|
||||||
|
// implementation 'com.google.firebase:firebase-analytics'
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="app.dapk.st.tracking"/>
|
|
@ -0,0 +1,19 @@
|
||||||
|
package app.dapk.st.tracking
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import app.dapk.st.core.AppLogTag
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
|
import app.dapk.st.core.log
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
|
||||||
|
class CrashlyticsCrashTracker(
|
||||||
|
private val firebaseCrashlytics: FirebaseCrashlytics,
|
||||||
|
) : ErrorTracker {
|
||||||
|
|
||||||
|
override fun track(throwable: Throwable, extra: String) {
|
||||||
|
Log.e("ST", throwable.message, throwable)
|
||||||
|
log(AppLogTag.ERROR_NON_FATAL, "${throwable.message ?: "N/A"} extra=$extra")
|
||||||
|
firebaseCrashlytics.recordException(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package app.dapk.st.tracking
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
|
||||||
|
class TrackingModule(
|
||||||
|
private val isCrashTrackingEnabled: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val errorTracker: ErrorTracker by unsafeLazy {
|
||||||
|
when (isCrashTrackingEnabled) {
|
||||||
|
true -> CrashlyticsCrashTracker(FirebaseCrashlytics.getInstance())
|
||||||
|
false -> object : ErrorTracker {
|
||||||
|
override fun track(throwable: Throwable, extra: String) {
|
||||||
|
Log.e("error", throwable.message, throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':core')
|
||||||
|
implementation project(':domains:android:core')
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.dapk.st.work">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<service android:name=".WorkAndroidService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,22 @@
|
||||||
|
package app.dapk.st.work
|
||||||
|
|
||||||
|
import android.app.job.JobWorkItem
|
||||||
|
import app.dapk.st.work.WorkScheduler.WorkTask
|
||||||
|
|
||||||
|
interface TaskRunner {
|
||||||
|
|
||||||
|
suspend fun run(tasks: List<RunnableWorkTask>): List<TaskResult>
|
||||||
|
|
||||||
|
data class RunnableWorkTask(
|
||||||
|
val source: JobWorkItem,
|
||||||
|
val task: WorkTask
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface TaskResult {
|
||||||
|
val source: JobWorkItem
|
||||||
|
|
||||||
|
data class Success(override val source: JobWorkItem) : TaskResult
|
||||||
|
data class Failure(override val source: JobWorkItem, val canRetry: Boolean) : TaskResult
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package app.dapk.st.work
|
||||||
|
|
||||||
|
import android.app.job.JobParameters
|
||||||
|
import android.app.job.JobService
|
||||||
|
import android.app.job.JobWorkItem
|
||||||
|
import app.dapk.st.core.extensions.Scope
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import app.dapk.st.core.module
|
||||||
|
import app.dapk.st.work.TaskRunner.RunnableWorkTask
|
||||||
|
import app.dapk.st.work.WorkScheduler.WorkTask
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
|
||||||
|
class WorkAndroidService : JobService() {
|
||||||
|
|
||||||
|
private val module by unsafeLazy { module<TaskRunnerModule>() }
|
||||||
|
private val serviceScope = Scope(Dispatchers.IO)
|
||||||
|
private var currentJob: Job? = null
|
||||||
|
|
||||||
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
|
currentJob = serviceScope.launch {
|
||||||
|
val results = module.taskRunner().run(params.collectAllTasks())
|
||||||
|
results.forEach {
|
||||||
|
when (it) {
|
||||||
|
is TaskRunner.TaskResult.Failure -> {
|
||||||
|
if (!it.canRetry) {
|
||||||
|
params.completeWork(it.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TaskRunner.TaskResult.Success -> {
|
||||||
|
params.completeWork(it.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val shouldReschedule = results.any { it is TaskRunner.TaskResult.Failure && it.canRetry }
|
||||||
|
jobFinished(params, shouldReschedule)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JobParameters.collectAllTasks(): List<RunnableWorkTask> {
|
||||||
|
var work: JobWorkItem?
|
||||||
|
val tasks = mutableListOf<RunnableWorkTask>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopJob(params: JobParameters): Boolean {
|
||||||
|
currentJob?.cancel()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
serviceScope.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package app.dapk.st.work
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.dapk.st.core.ProvidableModule
|
||||||
|
|
||||||
|
class WorkModule(private val context: Context) {
|
||||||
|
fun workScheduler(): WorkScheduler = WorkSchedulingJobScheduler(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskRunnerModule(private val taskRunner: TaskRunner) : ProvidableModule {
|
||||||
|
fun taskRunner() = taskRunner
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package app.dapk.st.work
|
||||||
|
|
||||||
|
interface WorkScheduler {
|
||||||
|
|
||||||
|
fun schedule(task: WorkTask)
|
||||||
|
|
||||||
|
data class WorkTask(val jobId: Int, val type: String, val jsonPayload: String)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package app.dapk.st.work
|
||||||
|
|
||||||
|
import android.app.job.JobInfo
|
||||||
|
import android.app.job.JobScheduler
|
||||||
|
import android.app.job.JobWorkItem
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
internal class WorkSchedulingJobScheduler(
|
||||||
|
private val context: Context,
|
||||||
|
) : WorkScheduler {
|
||||||
|
|
||||||
|
private val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||||
|
|
||||||
|
override fun schedule(task: WorkScheduler.WorkTask) {
|
||||||
|
val job = JobInfo.Builder(100, ComponentName(context, WorkAndroidService::class.java))
|
||||||
|
.setMinimumLatency(1)
|
||||||
|
.setOverrideDeadline(1)
|
||||||
|
.setBackoffCriteria(1000L, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
|
||||||
|
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||||
|
.setRequiresCharging(false)
|
||||||
|
.setRequiresDeviceIdle(false)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val item = JobWorkItem(
|
||||||
|
Intent()
|
||||||
|
.putExtra("task-type", task.type)
|
||||||
|
.putExtra("task-payload", task.jsonPayload)
|
||||||
|
)
|
||||||
|
|
||||||
|
jobScheduler.enqueue(job, item)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
plugins {
|
||||||
|
id 'kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly 'org.json:json:20211205'
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class OlmAccount implements Serializable {
|
||||||
|
public static final String JSON_KEY_ONE_TIME_KEY = "curve25519";
|
||||||
|
public static final String JSON_KEY_IDENTITY_KEY = "curve25519";
|
||||||
|
public static final String JSON_KEY_FINGER_PRINT_KEY = "ed25519";
|
||||||
|
|
||||||
|
public OlmAccount() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
long getOlmAccountId() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseAccount() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReleased() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> identityKeys() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long maxOneTimeKeys() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generateOneTimeKeys(int aNumberOfKeys) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Map<String, String>> oneTimeKeys() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeOneTimeKeys(OlmSession aSession) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markOneTimeKeysAsPublished() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String signMessage(String aMessage) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] pickle(byte[] aKey, StringBuffer aErrorMsg) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unpickle(byte[] aSerializedData, byte[] aKey) throws Exception {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generateFallbackKey() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Map<String, String>> fallbackKey() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forgetFallbackKey() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class OlmException extends IOException {
|
||||||
|
public static final int EXCEPTION_CODE_INIT_ACCOUNT_CREATION = 10;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_SERIALIZATION = 100;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_DESERIALIZATION = 101;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_IDENTITY_KEYS = 102;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_GENERATE_ONE_TIME_KEYS = 103;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_ONE_TIME_KEYS = 104;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_REMOVE_ONE_TIME_KEYS = 105;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_MARK_ONE_KEYS_AS_PUBLISHED = 106;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_SIGN_MESSAGE = 107;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_GENERATE_FALLBACK_KEY = 108;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_FALLBACK_KEY = 109;
|
||||||
|
public static final int EXCEPTION_CODE_ACCOUNT_FORGET_FALLBACK_KEY = 110;
|
||||||
|
public static final int EXCEPTION_CODE_CREATE_INBOUND_GROUP_SESSION = 200;
|
||||||
|
public static final int EXCEPTION_CODE_INIT_INBOUND_GROUP_SESSION = 201;
|
||||||
|
public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_IDENTIFIER = 202;
|
||||||
|
public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_DECRYPT_SESSION = 203;
|
||||||
|
public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_FIRST_KNOWN_INDEX = 204;
|
||||||
|
public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_IS_VERIFIED = 205;
|
||||||
|
public static final int EXCEPTION_CODE_INBOUND_GROUP_SESSION_EXPORT = 206;
|
||||||
|
public static final int EXCEPTION_CODE_CREATE_OUTBOUND_GROUP_SESSION = 300;
|
||||||
|
public static final int EXCEPTION_CODE_INIT_OUTBOUND_GROUP_SESSION = 301;
|
||||||
|
public static final int EXCEPTION_CODE_OUTBOUND_GROUP_SESSION_IDENTIFIER = 302;
|
||||||
|
public static final int EXCEPTION_CODE_OUTBOUND_GROUP_SESSION_KEY = 303;
|
||||||
|
public static final int EXCEPTION_CODE_OUTBOUND_GROUP_ENCRYPT_MESSAGE = 304;
|
||||||
|
public static final int EXCEPTION_CODE_INIT_SESSION_CREATION = 400;
|
||||||
|
public static final int EXCEPTION_CODE_SESSION_INIT_OUTBOUND_SESSION = 401;
|
||||||
|
public static final int EXCEPTION_CODE_SESSION_INIT_INBOUND_SESSION = 402;
|
||||||
|
public static final int EXCEPTION_CODE_SESSION_INIT_INBOUND_SESSION_FROM = 403;
|
||||||
|
public static final int EXCEPTION_CODE_SESSION_ENCRYPT_MESSAGE = 404;
|
||||||
|
public static final int EXCEPTION_CODE_SESSION_DECRYPT_MESSAGE = 405;
|
||||||
|
public static final int EXCEPTION_CODE_SESSION_SESSION_IDENTIFIER = 406;
|
||||||
|
public static final int EXCEPTION_CODE_UTILITY_CREATION = 500;
|
||||||
|
public static final int EXCEPTION_CODE_UTILITY_VERIFY_SIGNATURE = 501;
|
||||||
|
public static final int EXCEPTION_CODE_PK_ENCRYPTION_CREATION = 600;
|
||||||
|
public static final int EXCEPTION_CODE_PK_ENCRYPTION_SET_RECIPIENT_KEY = 601;
|
||||||
|
public static final int EXCEPTION_CODE_PK_ENCRYPTION_ENCRYPT = 602;
|
||||||
|
public static final int EXCEPTION_CODE_PK_DECRYPTION_CREATION = 700;
|
||||||
|
public static final int EXCEPTION_CODE_PK_DECRYPTION_GENERATE_KEY = 701;
|
||||||
|
public static final int EXCEPTION_CODE_PK_DECRYPTION_DECRYPT = 702;
|
||||||
|
public static final int EXCEPTION_CODE_PK_DECRYPTION_SET_PRIVATE_KEY = 703;
|
||||||
|
public static final int EXCEPTION_CODE_PK_DECRYPTION_PRIVATE_KEY = 704;
|
||||||
|
public static final int EXCEPTION_CODE_PK_SIGNING_CREATION = 800;
|
||||||
|
public static final int EXCEPTION_CODE_PK_SIGNING_GENERATE_SEED = 801;
|
||||||
|
public static final int EXCEPTION_CODE_PK_SIGNING_INIT_WITH_SEED = 802;
|
||||||
|
public static final int EXCEPTION_CODE_PK_SIGNING_SIGN = 803;
|
||||||
|
public static final int EXCEPTION_CODE_SAS_CREATION = 900;
|
||||||
|
public static final int EXCEPTION_CODE_SAS_ERROR = 901;
|
||||||
|
public static final int EXCEPTION_CODE_SAS_MISSING_THEIR_PKEY = 902;
|
||||||
|
public static final int EXCEPTION_CODE_SAS_GENERATE_SHORT_CODE = 903;
|
||||||
|
public static final String EXCEPTION_MSG_INVALID_PARAMS_DESERIALIZATION = "invalid de-serialized parameters";
|
||||||
|
private final int mCode;
|
||||||
|
private final String mMessage;
|
||||||
|
|
||||||
|
public OlmException(int aExceptionCode, String aExceptionMessage) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExceptionCode() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class OlmInboundGroupSession implements Serializable {
|
||||||
|
|
||||||
|
public OlmInboundGroupSession(String aSessionKey) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OlmInboundGroupSession importSession(String exported) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseSession() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReleased() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sessionIdentifier() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFirstKnownIndex() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVerified() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String export(long messageIndex) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public OlmInboundGroupSession.DecryptMessageResult decryptMessage(String aEncryptedMsg) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DecryptMessageResult {
|
||||||
|
public String mDecryptedMessage;
|
||||||
|
public long mIndex;
|
||||||
|
|
||||||
|
public DecryptMessageResult() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
public class OlmManager {
|
||||||
|
public OlmManager() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOlmLibVersion() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public native String getOlmLibVersionJni();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
public class OlmMessage {
|
||||||
|
public static final int MESSAGE_TYPE_PRE_KEY = 0;
|
||||||
|
public static final int MESSAGE_TYPE_MESSAGE = 1;
|
||||||
|
public String mCipherText;
|
||||||
|
public long mType;
|
||||||
|
|
||||||
|
public OlmMessage() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class OlmOutboundGroupSession implements Serializable {
|
||||||
|
|
||||||
|
public OlmOutboundGroupSession() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseSession() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReleased() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sessionIdentifier() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int messageIndex() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sessionKey() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String encryptMessage(String aClearMsg) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
public class OlmSAS {
|
||||||
|
|
||||||
|
public OlmSAS() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPublicKey() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTheirPublicKey(String otherPkey) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] generateShortCode(String info, int byteNumber) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String calculateMac(String message, String info) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String calculateMacLongKdf(String message, String info) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseSas() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class OlmSession implements Serializable {
|
||||||
|
|
||||||
|
public OlmSession() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
long getOlmSessionId() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseSession() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReleased() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initOutboundSession(OlmAccount aAccount, String aTheirIdentityKey, String aTheirOneTimeKey) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initInboundSession(OlmAccount aAccount, String aPreKeyMsg) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initInboundSessionFrom(OlmAccount aAccount, String aTheirIdentityKey, String aPreKeyMsg) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sessionIdentifier() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchesInboundSession(String aOneTimeKeyMsg) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matchesInboundSessionFrom(String aTheirIdentityKey, String aOneTimeKeyMsg) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public OlmMessage encryptMessage(String aClearMsg) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String decryptMessage(OlmMessage aEncryptedMsg) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] serialize(byte[] aKey, StringBuffer aErrorMsg) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void deserialize(byte[] aSerializedData, byte[] aKey) throws Exception {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package org.matrix.olm;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class OlmUtility {
|
||||||
|
public static final int RANDOM_KEY_SIZE = 32;
|
||||||
|
|
||||||
|
public OlmUtility() throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseUtility() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyEd25519Signature(String aSignature, String aFingerprintKey, String aMessage) throws OlmException {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sha256(String aMessageToHash) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getRandomKey() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReleased() {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, String> toStringMap(JSONObject jsonObject) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, Map<String, String>> toStringMapMap(JSONObject jsonObject) {
|
||||||
|
throw new RuntimeException("stub");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
plugins {
|
||||||
|
id 'kotlin'
|
||||||
|
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||||
|
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
|
|
||||||
|
implementation project(":core")
|
||||||
|
implementation project(":domains:store")
|
||||||
|
implementation project(":matrix:services:crypto")
|
||||||
|
implementation project(":matrix:services:device")
|
||||||
|
compileOnly project(":domains:olm-stub")
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package app.dapk.st.olm
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.DeviceId
|
||||||
|
import app.dapk.st.matrix.common.Ed25519
|
||||||
|
import app.dapk.st.matrix.common.UserId
|
||||||
|
import app.dapk.st.matrix.crypto.Olm
|
||||||
|
import org.matrix.olm.OlmSAS
|
||||||
|
import org.matrix.olm.OlmUtility
|
||||||
|
|
||||||
|
internal class DefaultSasSession(private val selfFingerprint: Ed25519) : Olm.SasSession {
|
||||||
|
|
||||||
|
private val olmSAS = OlmSAS()
|
||||||
|
|
||||||
|
override fun publicKey(): String {
|
||||||
|
return olmSAS.publicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun generateCommitment(hash: String, startJsonString: String): String {
|
||||||
|
val utility = OlmUtility()
|
||||||
|
return utility.sha256(olmSAS.publicKey + startJsonString).also {
|
||||||
|
utility.releaseUtility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun calculateMac(
|
||||||
|
selfUserId: UserId,
|
||||||
|
selfDeviceId: DeviceId,
|
||||||
|
otherUserId: UserId,
|
||||||
|
otherDeviceId: DeviceId,
|
||||||
|
transactionId: String
|
||||||
|
): Olm.MacResult {
|
||||||
|
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" +
|
||||||
|
selfUserId.value +
|
||||||
|
selfDeviceId.value +
|
||||||
|
otherUserId.value +
|
||||||
|
otherDeviceId.value +
|
||||||
|
transactionId
|
||||||
|
val deviceKeyId = "ed25519:${selfDeviceId.value}"
|
||||||
|
val macMap = mapOf(
|
||||||
|
deviceKeyId to olmSAS.calculateMac(selfFingerprint.value, baseInfo + deviceKeyId)
|
||||||
|
)
|
||||||
|
val keys = olmSAS.calculateMac(macMap.keys.sorted().joinToString(separator = ","), baseInfo + "KEY_IDS")
|
||||||
|
return Olm.MacResult(macMap, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTheirPublicKey(key: String) {
|
||||||
|
olmSAS.setTheirPublicKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun release() {
|
||||||
|
olmSAS.releaseSas()
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue