initial commit

This commit is contained in:
Adam Brown 2022-02-24 21:55:56 +00:00
commit c78d24a458
383 changed files with 16781 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
.idea/*.xml
.DS_Store
**/build
/captures
.externalNativeBuild
.cxx
local.properties
/benchmark-out

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
SmallTalk

View File

@ -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>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

48
README.md Normal file
View File

@ -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.

83
app/build.gradle Normal file
View File

@ -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'
}

8
app/proguard/app.pro Normal file
View File

@ -0,0 +1,8 @@
-assumenosideeffects class android.util.Log {
v(...);
d(...);
i(...);
w(...);
e(...);
println(...);
}

1
app/proguard/clear.pro Normal file
View File

@ -0,0 +1 @@
-keepnames class ** { *; }

1
app/proguard/olm.pro Normal file
View File

@ -0,0 +1 @@
-keepclassmembers class org.matrix.olm.** { *; }

View File

@ -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(...);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:shrinkMode="strict" />

View File

@ -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>

View File

@ -0,0 +1,3 @@
<resources>
<string name="dapk_app_name">SmallTalk</string>
</resources>

View File

@ -0,0 +1,7 @@
<resources>
<style name="DapkTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
</style>
</resources>

View File

@ -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>

141
build.gradle Normal file
View File

@ -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'
}

11
core/build.gradle Normal file
View File

@ -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'
}

View File

@ -0,0 +1,5 @@
package app.dapk.st.core
data class BuildMeta(
val versionName: String,
)

View File

@ -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() }
}

View File

@ -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")
}
}

View File

@ -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>
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -0,0 +1,6 @@
package fake
import app.dapk.st.core.extensions.ErrorTracker
import io.mockk.mockk
class FakeErrorTracker : ErrorTracker by mockk(relaxed = true)

View File

@ -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)
}

View File

@ -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()
}
}

147
dependencies.gradle Normal file
View File

@ -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);
}
}

View File

@ -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"
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.design"/>

View File

@ -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
}

View File

@ -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)
)
}

View File

@ -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,
)
}
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,6 @@
applyAndroidLibraryModule(project)
dependencies {
implementation project(":core")
implementation project(":features:navigator")
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.core"/>

View File

@ -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 })
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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()
}
}
}
}

View File

@ -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)
}
}

View File

@ -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())
}
}

View File

@ -0,0 +1,6 @@
applyAndroidLibraryModule(project)
dependencies {
implementation project(":core")
implementation "io.coil-kt:coil:1.4.0"
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.imageloader"/>

View File

@ -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)
}

View File

@ -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
}

View File

@ -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'
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.push"/>

View File

@ -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()

View File

@ -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,
)
}

View File

@ -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")
}
}
}

View File

@ -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'
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="app.dapk.st.tracking"/>

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -0,0 +1,6 @@
applyAndroidLibraryModule(project)
dependencies {
implementation project(':core')
implementation project(':domains:android:core')
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -0,0 +1,7 @@
plugins {
id 'kotlin'
}
dependencies {
compileOnly 'org.json:json:20211205'
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}
}

View File

@ -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();
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

15
domains/olm/build.gradle Normal file
View File

@ -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")
}

View File

@ -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