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