Merge pull request #103 from ouchadam/release-candidate
[Auto] Release Candidate
This commit is contained in:
commit
c4b3729908
|
@ -0,0 +1,7 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gradle
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 3
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -17,19 +17,11 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
|
|
||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
|
- uses: gradle/gradle-build-action@v2
|
||||||
|
|
||||||
- name: Assemble debug variant
|
- name: Assemble debug variant
|
||||||
run: ./gradlew assembleDebug --no-daemon
|
run: ./gradlew assembleDebug --no-daemon
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: Check Size
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-size:
|
||||||
|
name: Check Size
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
distribution: 'adopt'
|
||||||
|
java-version: '11'
|
||||||
|
- uses: gradle/gradle-build-action@v2
|
||||||
|
|
||||||
|
- name: Fetch bundletool
|
||||||
|
run: |
|
||||||
|
curl -s -L https://github.com/google/bundletool/releases/download/1.9.0/bundletool-all-1.9.0.jar --create-dirs -o bin/bundletool.jar
|
||||||
|
chmod +x bin/bundletool.jar
|
||||||
|
echo "#!/bin/bash" >> bin/bundletool
|
||||||
|
echo 'java -jar $(dirname "$0")/bundletool.jar "$@"' >> bin/bundletool
|
||||||
|
chmod +x bin/bundletool
|
||||||
|
echo "$(pwd)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Save Size
|
||||||
|
env:
|
||||||
|
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ./apk_size
|
||||||
|
echo $(./tools/check-size.sh | tail -1 | cut -d ',' -f2-) > ./apk_size/size.txt
|
||||||
|
echo $PULL_REQUEST_NUMBER > ./apk_size/pr_number.txt
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: apk-size
|
||||||
|
path: |
|
||||||
|
apk_size/size.txt
|
||||||
|
apk_size/pr_number.txt
|
||||||
|
retention-days: 5
|
|
@ -0,0 +1,44 @@
|
||||||
|
name: Comment APK Size
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [ "Check Size" ]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment-size:
|
||||||
|
name: Comment Size
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
${{ github.event.workflow_run.event == 'pull_request' &&
|
||||||
|
github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: dawidd6/action-download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: apk-size
|
||||||
|
workflow: ${{ github.event.workflow_run.workflow_id }}
|
||||||
|
|
||||||
|
- name: Check release size
|
||||||
|
run: |
|
||||||
|
ls -R
|
||||||
|
echo "::set-output name=APK_SIZE::$(cat size.txt)"
|
||||||
|
echo "::set-output name=PR_NUMBER::$(cat pr_number.txt)"
|
||||||
|
id: size
|
||||||
|
|
||||||
|
- name: Find Comment
|
||||||
|
uses: peter-evans/find-comment@v1
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
body-includes: APK Size
|
||||||
|
- name: Publish size to PR
|
||||||
|
uses: peter-evans/create-or-update-comment@v1
|
||||||
|
with:
|
||||||
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
issue-number: ${{ steps.size.outputs.PR_NUMBER }}
|
||||||
|
body: |
|
||||||
|
APK Size: ${{ steps.size.outputs.APK_SIZE }}
|
||||||
|
edit-mode: replace
|
|
@ -17,29 +17,26 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
|
|
||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '11'
|
||||||
|
- uses: gradle/gradle-build-action@v2
|
||||||
|
|
||||||
|
- name: Create pip requirements
|
||||||
|
run: |
|
||||||
|
echo "matrix-synapse==v1.60.0" > requirements.txt
|
||||||
|
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
- name: Start synapse server
|
- name: Start synapse server
|
||||||
run: |
|
run: |
|
||||||
pip install matrix-synapse
|
pip install -r requirements.txt
|
||||||
curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh \
|
curl -sL https://gist.githubusercontent.com/ouchadam/e3ad09ec382bd91a66d88ab575ea7c31/raw/run.sh \
|
||||||
| bash -s -- --no-rate-limit
|
| bash -s -- --no-rate-limit
|
||||||
|
|
||||||
- name: Run all unit tests
|
- name: Run all unit tests
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
name: Update Gradle Wrapper
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-gradle-wrapper:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Update Gradle Wrapper
|
||||||
|
uses: gradle-update/update-gradle-wrapper-action@v1
|
|
@ -0,0 +1,29 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
24
README.md
24
README.md
|
@ -1,13 +1,18 @@
|
||||||
# SmallTalk [![Assemble](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml/badge.svg)](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml) [![codecov](https://codecov.io/gh/ouchadam/small-talk/branch/main/graph/badge.svg?token=ETFSLZ9FCI)](https://codecov.io/gh/ouchadam/small-talk) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
|
# SmallTalk [![Assemble](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml/badge.svg)](https://github.com/ouchadam/small-talk/actions/workflows/assemble.yml) [![codecov](https://codecov.io/gh/ouchadam/small-talk/branch/main/graph/badge.svg?token=ETFSLZ9FCI)](https://codecov.io/gh/ouchadam/small-talk) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/github/v/release/ouchadam/small-talk?include_prereleases) ![](https://img.shields.io/badge/%5Bmatrix%5D%20-%23small--talk%3Aiswell.cool-blueviolet)
|
||||||
|
|
||||||
`SmallTalk` is a minimal, modern, friends and family focused Android messenger. Heavily inspired by Whatsapp and Signal, powered by Matrix.
|
`SmallTalk` is a minimal, modern, friends and family focused Android messenger. Heavily inspired by Whatsapp and Signal, powered by Matrix.
|
||||||
|
|
||||||
|
|
||||||
![header](https://github.com/ouchadam/small-talk/blob/main/.github/readme/header.png?raw=true)
|
![header](https://github.com/ouchadam/small-talk/blob/main/.github/readme/header.png?raw=true)
|
||||||
|
[<img align="right" height="70" src="https://github.com/ouchadam/small-talk/blob/main/.github/readme/google-play-badge.png?raw=tru"></a>](https://play.google.com/store/apps/details?id=app.dapk.st)
|
||||||
|
|
||||||
---
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
Project mantra
|
|
||||||
- Tiny app size - currently 1.72mb~ when provided via app bundle.
|
### Project mantra
|
||||||
|
- Tiny app size - currently 1.80mb~ when provided via app bundle.
|
||||||
- Focused on reliability and stability.
|
- Focused on reliability and stability.
|
||||||
- Bare-bones feature set.
|
- Bare-bones feature set.
|
||||||
|
|
||||||
|
@ -15,16 +20,17 @@ Project mantra
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Feature list
|
### Feature list
|
||||||
|
|
||||||
- Login with username/password (home servers must serve `${domain}.well-known/matrix/client`)
|
- Login with Matrix ID/Password
|
||||||
- Combined Room and DM interface
|
- Combined Room and DM interface
|
||||||
- End to end encryption
|
- End to end encryption
|
||||||
- Message bubbles, supporting text, replies and edits
|
- Message bubbles, supporting text, replies and edits
|
||||||
- Push notifications (DMs always notify, Rooms notify once)
|
- Push notifications (DMs always notify, Rooms notify once)
|
||||||
- Importing of E2E room keys from Element clients
|
- Importing of E2E room keys from Element clients
|
||||||
|
- [UnifiedPush](https://unifiedpush.org/)
|
||||||
|
|
||||||
#### Planned
|
### Planned
|
||||||
|
|
||||||
- Device verification (technically supported but has no UI)
|
- Device verification (technically supported but has no UI)
|
||||||
- Invitations (technically supported but has no UI)
|
- Invitations (technically supported but has no UI)
|
||||||
|
@ -48,3 +54,7 @@ Project mantra
|
||||||
- Heavily optimised build script, clean _cacheless_ builds are sub 10 seconds with a warmed up gradle daemon.
|
- 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.
|
- Avoids code generation where possible in favour of build speed, this mainly means manual DI.
|
||||||
- A pure kotlin test harness to allow for critical flow assertions [Smoke Tests](https://github.com/ouchadam/small-talk/blob/main/test-harness/src/test/kotlin/SmokeTest.kt), currently Linux x86-64 only.
|
- A pure kotlin test harness to allow for critical flow assertions [Smoke Tests](https://github.com/ouchadam/small-talk/blob/main/test-harness/src/test/kotlin/SmokeTest.kt), currently Linux x86-64 only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Join the conversation @ https://matrix.to/#/#small-talk:iswell.cool
|
||||||
|
|
|
@ -10,10 +10,21 @@ android {
|
||||||
ndkVersion "25.0.8141415"
|
ndkVersion "25.0.8141415"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.dapk.st"
|
applicationId "app.dapk.st"
|
||||||
versionCode 2
|
def versionJson = new groovy.json.JsonSlurper().parseText(rootProject.file('version.json').text)
|
||||||
versionName "0.0.1-alpha1"
|
versionCode versionJson.code
|
||||||
|
versionName versionJson.name
|
||||||
|
|
||||||
|
if (isDebugBuild) {
|
||||||
|
resConfigs "en", "xxhdpi"
|
||||||
|
variantFilter { variant ->
|
||||||
|
if (variant.buildType.name == "release") {
|
||||||
|
setIgnore(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
resConfigs "en"
|
resConfigs "en"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bundle {
|
bundle {
|
||||||
abi.enableSplit true
|
abi.enableSplit true
|
||||||
|
@ -23,6 +34,7 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
|
versionNameSuffix = " [debug]"
|
||||||
matchingFallbacks = ['release']
|
matchingFallbacks = ['release']
|
||||||
signingConfig.storeFile rootProject.file("tools/debug.keystore")
|
signingConfig.storeFile rootProject.file("tools/debug.keystore")
|
||||||
}
|
}
|
||||||
|
@ -33,16 +45,24 @@ android {
|
||||||
'proguard/app.pro',
|
'proguard/app.pro',
|
||||||
"proguard/serializationx.pro",
|
"proguard/serializationx.pro",
|
||||||
"proguard/olm.pro"
|
"proguard/olm.pro"
|
||||||
|
|
||||||
|
// actual releases are signed with a different config
|
||||||
signingConfig = buildTypes.debug.signingConfig
|
signingConfig = buildTypes.debug.signingConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
resources.excludes += "DebugProbesKt.bin"
|
resources.excludes += "DebugProbesKt.bin"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring Dependencies.google.jdkLibs
|
||||||
|
|
||||||
implementation project(":features:home")
|
implementation project(":features:home")
|
||||||
implementation project(":features:directory")
|
implementation project(":features:directory")
|
||||||
implementation project(":features:login")
|
implementation project(":features:login")
|
||||||
|
@ -51,8 +71,10 @@ dependencies {
|
||||||
implementation project(":features:messenger")
|
implementation project(":features:messenger")
|
||||||
implementation project(":features:profile")
|
implementation project(":features:profile")
|
||||||
implementation project(":features:navigator")
|
implementation project(":features:navigator")
|
||||||
|
implementation project(":features:share-entry")
|
||||||
|
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:core")
|
implementation project(":domains:android:core")
|
||||||
implementation project(":domains:android:tracking")
|
implementation project(":domains:android:tracking")
|
||||||
implementation project(":domains:android:push")
|
implementation project(":domains:android:push")
|
||||||
|
@ -79,5 +101,5 @@ dependencies {
|
||||||
implementation Dependencies.mavenCentral.matrixOlm
|
implementation Dependencies.mavenCentral.matrixOlm
|
||||||
|
|
||||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
|
debugImplementation Dependencies.mavenCentral.leakCanary
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,11 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
|
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -33,7 +33,7 @@ internal class SharedPreferencesDelegate(
|
||||||
|
|
||||||
override suspend fun clear() {
|
override suspend fun clear() {
|
||||||
coroutineDispatchers.withIoContext {
|
coroutineDispatchers.withIoContext {
|
||||||
preferences.edit().clear().apply()
|
preferences.edit().clear().commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,26 +1,29 @@
|
||||||
package app.dapk.st
|
package app.dapk.st
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import app.dapk.st.core.CoreAndroidModule
|
import app.dapk.st.core.CoreAndroidModule
|
||||||
import app.dapk.st.core.ModuleProvider
|
import app.dapk.st.core.ModuleProvider
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.core.attachAppLogger
|
import app.dapk.st.core.attachAppLogger
|
||||||
|
import app.dapk.st.core.extensions.ResettableUnsafeLazy
|
||||||
import app.dapk.st.core.extensions.Scope
|
import app.dapk.st.core.extensions.Scope
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
|
||||||
import app.dapk.st.directory.DirectoryModule
|
import app.dapk.st.directory.DirectoryModule
|
||||||
import app.dapk.st.messenger.MessengerModule
|
import app.dapk.st.domain.StoreModule
|
||||||
import app.dapk.st.graph.AppModule
|
import app.dapk.st.graph.AppModule
|
||||||
import app.dapk.st.graph.FeatureModules
|
|
||||||
import app.dapk.st.home.HomeModule
|
import app.dapk.st.home.HomeModule
|
||||||
import app.dapk.st.login.LoginModule
|
import app.dapk.st.login.LoginModule
|
||||||
|
import app.dapk.st.messenger.MessengerModule
|
||||||
import app.dapk.st.notifications.NotificationsModule
|
import app.dapk.st.notifications.NotificationsModule
|
||||||
import app.dapk.st.profile.ProfileModule
|
import app.dapk.st.profile.ProfileModule
|
||||||
|
import app.dapk.st.push.firebase.FirebasePushService
|
||||||
|
import app.dapk.st.push.PushModule
|
||||||
import app.dapk.st.settings.SettingsModule
|
import app.dapk.st.settings.SettingsModule
|
||||||
|
import app.dapk.st.share.ShareEntryModule
|
||||||
import app.dapk.st.work.TaskRunnerModule
|
import app.dapk.st.work.TaskRunnerModule
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
class SmallTalkApplication : Application(), ModuleProvider {
|
class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
|
@ -28,8 +31,10 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
private val appLogger: (String, String) -> Unit = { tag, message -> _appLogger?.invoke(tag, message) }
|
private val appLogger: (String, String) -> Unit = { tag, message -> _appLogger?.invoke(tag, message) }
|
||||||
private var _appLogger: ((String, String) -> Unit)? = null
|
private var _appLogger: ((String, String) -> Unit)? = null
|
||||||
|
|
||||||
private val appModule: AppModule by unsafeLazy { AppModule(this, appLogger) }
|
private val lazyAppModule = ResettableUnsafeLazy { AppModule(this, appLogger) }
|
||||||
private val featureModules: FeatureModules by unsafeLazy { appModule.featureModules }
|
private val lazyFeatureModules = ResettableUnsafeLazy { appModule.featureModules }
|
||||||
|
private val appModule by lazyAppModule
|
||||||
|
private val featureModules by lazyFeatureModules
|
||||||
private val applicationScope = Scope(Dispatchers.IO)
|
private val applicationScope = Scope(Dispatchers.IO)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
@ -40,15 +45,17 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
|
|
||||||
val logger: (String, String) -> Unit = { tag, message ->
|
val logger: (String, String) -> Unit = { tag, message ->
|
||||||
Log.e(tag, message)
|
Log.e(tag, message)
|
||||||
GlobalScope.launch {
|
applicationScope.launch { eventLogStore.insert(tag, message) }
|
||||||
eventLogStore.insert(tag, message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
attachAppLogger(logger)
|
attachAppLogger(logger)
|
||||||
_appLogger = logger
|
_appLogger = logger
|
||||||
|
|
||||||
|
onApplicationLaunch(notificationsModule, storeModule)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
notificationsModule.firebasePushTokenUseCase().registerCurrentToken()
|
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
||||||
storeModule.localEchoStore.preload()
|
storeModule.localEchoStore.preload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,10 +74,24 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
SettingsModule::class -> featureModules.settingsModule
|
SettingsModule::class -> featureModules.settingsModule
|
||||||
ProfileModule::class -> featureModules.profileModule
|
ProfileModule::class -> featureModules.profileModule
|
||||||
NotificationsModule::class -> featureModules.notificationsModule
|
NotificationsModule::class -> featureModules.notificationsModule
|
||||||
|
PushModule::class -> featureModules.pushModule
|
||||||
MessengerModule::class -> featureModules.messengerModule
|
MessengerModule::class -> featureModules.messengerModule
|
||||||
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
|
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
|
||||||
CoreAndroidModule::class -> appModule.coreAndroidModule
|
CoreAndroidModule::class -> appModule.coreAndroidModule
|
||||||
|
ShareEntryModule::class -> featureModules.shareEntryModule
|
||||||
else -> throw IllegalArgumentException("Unknown: $klass")
|
else -> throw IllegalArgumentException("Unknown: $klass")
|
||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
featureModules.pushModule.pushTokenRegistrar().unregister()
|
||||||
|
appModule.coroutineDispatchers.io.cancel()
|
||||||
|
applicationScope.cancel()
|
||||||
|
lazyAppModule.reset()
|
||||||
|
lazyFeatureModules.reset()
|
||||||
|
|
||||||
|
val notificationsModule = featureModules.notificationsModule
|
||||||
|
val storeModule = appModule.storeModule.value
|
||||||
|
onApplicationLaunch(notificationsModule, storeModule)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package app.dapk.st.graph
|
||||||
|
|
||||||
|
import app.dapk.st.core.Base64
|
||||||
|
|
||||||
|
class AndroidBase64 : Base64 {
|
||||||
|
override fun encode(input: ByteArray): String {
|
||||||
|
return android.util.Base64.encodeToString(input, android.util.Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decode(input: String): ByteArray {
|
||||||
|
return android.util.Base64.decode(input, android.util.Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,17 @@
|
||||||
package app.dapk.st.graph
|
package app.dapk.st.graph
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import app.dapk.db.DapkDb
|
import app.dapk.db.DapkDb
|
||||||
import app.dapk.st.BuildConfig
|
import app.dapk.st.BuildConfig
|
||||||
import app.dapk.st.SharedPreferencesDelegate
|
import app.dapk.st.SharedPreferencesDelegate
|
||||||
import app.dapk.st.core.BuildMeta
|
import app.dapk.st.core.*
|
||||||
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.ErrorTracker
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.directory.DirectoryModule
|
import app.dapk.st.directory.DirectoryModule
|
||||||
|
@ -20,8 +21,6 @@ import app.dapk.st.home.MainActivity
|
||||||
import app.dapk.st.imageloader.ImageLoaderModule
|
import app.dapk.st.imageloader.ImageLoaderModule
|
||||||
import app.dapk.st.login.LoginModule
|
import app.dapk.st.login.LoginModule
|
||||||
import app.dapk.st.matrix.MatrixClient
|
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.authService
|
||||||
import app.dapk.st.matrix.auth.installAuthService
|
import app.dapk.st.matrix.auth.installAuthService
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.*
|
||||||
|
@ -34,7 +33,11 @@ import app.dapk.st.matrix.device.installEncryptionService
|
||||||
import app.dapk.st.matrix.device.internal.ApiMessage
|
import app.dapk.st.matrix.device.internal.ApiMessage
|
||||||
import app.dapk.st.matrix.http.MatrixHttpClient
|
import app.dapk.st.matrix.http.MatrixHttpClient
|
||||||
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
||||||
import app.dapk.st.matrix.message.*
|
import app.dapk.st.matrix.message.MessageEncrypter
|
||||||
|
import app.dapk.st.matrix.message.MessageService
|
||||||
|
import app.dapk.st.matrix.message.installMessageService
|
||||||
|
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||||
|
import app.dapk.st.matrix.message.messageService
|
||||||
import app.dapk.st.matrix.push.installPushService
|
import app.dapk.st.matrix.push.installPushService
|
||||||
import app.dapk.st.matrix.push.pushService
|
import app.dapk.st.matrix.push.pushService
|
||||||
import app.dapk.st.matrix.room.*
|
import app.dapk.st.matrix.room.*
|
||||||
|
@ -44,6 +47,8 @@ import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
|
||||||
import app.dapk.st.messenger.MessengerActivity
|
import app.dapk.st.messenger.MessengerActivity
|
||||||
import app.dapk.st.messenger.MessengerModule
|
import app.dapk.st.messenger.MessengerModule
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
|
import app.dapk.st.notifications.MatrixPushHandler
|
||||||
import app.dapk.st.notifications.NotificationsModule
|
import app.dapk.st.notifications.NotificationsModule
|
||||||
import app.dapk.st.olm.DeviceKeyFactory
|
import app.dapk.st.olm.DeviceKeyFactory
|
||||||
import app.dapk.st.olm.OlmPersistenceWrapper
|
import app.dapk.st.olm.OlmPersistenceWrapper
|
||||||
|
@ -51,18 +56,17 @@ import app.dapk.st.olm.OlmWrapper
|
||||||
import app.dapk.st.profile.ProfileModule
|
import app.dapk.st.profile.ProfileModule
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
import app.dapk.st.settings.SettingsModule
|
import app.dapk.st.settings.SettingsModule
|
||||||
|
import app.dapk.st.share.ShareEntryModule
|
||||||
import app.dapk.st.tracking.TrackingModule
|
import app.dapk.st.tracking.TrackingModule
|
||||||
import app.dapk.st.work.TaskRunner
|
|
||||||
import app.dapk.st.work.TaskRunnerModule
|
import app.dapk.st.work.TaskRunnerModule
|
||||||
import app.dapk.st.work.WorkModule
|
import app.dapk.st.work.WorkModule
|
||||||
import app.dapk.st.work.WorkScheduler
|
|
||||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
|
||||||
internal class AppModule(context: Application, logger: MatrixLogger) {
|
internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||||
|
|
||||||
private val buildMeta = BuildMeta(BuildConfig.VERSION_NAME)
|
private val buildMeta = BuildMeta(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
|
||||||
private val trackingModule by unsafeLazy {
|
private val trackingModule by unsafeLazy {
|
||||||
TrackingModule(
|
TrackingModule(
|
||||||
isCrashTrackingEnabled = !BuildConfig.DEBUG
|
isCrashTrackingEnabled = !BuildConfig.DEBUG
|
||||||
|
@ -71,8 +75,8 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||||
|
|
||||||
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
|
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
|
||||||
private val database = DapkDb(driver)
|
private val database = DapkDb(driver)
|
||||||
|
private val clock = Clock.systemUTC()
|
||||||
private val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
||||||
|
|
||||||
val storeModule = unsafeLazy {
|
val storeModule = unsafeLazy {
|
||||||
StoreModule(
|
StoreModule(
|
||||||
|
@ -80,31 +84,41 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||||
preferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", coroutineDispatchers),
|
preferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", coroutineDispatchers),
|
||||||
errorTracker = trackingModule.errorTracker,
|
errorTracker = trackingModule.errorTracker,
|
||||||
credentialPreferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-credentials-preferences", coroutineDispatchers),
|
credentialPreferences = SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-credentials-preferences", coroutineDispatchers),
|
||||||
databaseDropper = { includeCryptoAccount ->
|
databaseDropper = DefaultDatabaseDropper(coroutineDispatchers, driver),
|
||||||
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
|
coroutineDispatchers = coroutineDispatchers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private val workModule = WorkModule(context)
|
private val workModule = WorkModule(context)
|
||||||
private val imageLoaderModule = ImageLoaderModule(context)
|
private val imageLoaderModule = ImageLoaderModule(context)
|
||||||
|
|
||||||
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers)
|
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver)
|
||||||
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker)
|
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers)
|
||||||
|
|
||||||
val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory {
|
val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory {
|
||||||
override fun home(activity: Activity) = Intent(activity, MainActivity::class.java)
|
override fun notificationOpenApp(context: Context) = PendingIntent.getActivity(
|
||||||
override fun messenger(activity: Activity, roomId: RoomId) = MessengerActivity.newInstance(activity, roomId)
|
context,
|
||||||
override fun messengerShortcut(activity: Activity, roomId: RoomId) = MessengerActivity.newShortcutInstance(activity, roomId)
|
1000,
|
||||||
|
home(context)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun notificationOpenMessage(context: Context, roomId: RoomId) = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
roomId.hashCode(),
|
||||||
|
messenger(context, roomId)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun home(context: Context) = Intent(context, MainActivity::class.java)
|
||||||
|
override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId)
|
||||||
|
override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId)
|
||||||
|
override fun messengerAttachments(context: Context, roomId: RoomId, attachments: List<MessageAttachment>) = MessengerActivity.newMessageAttachment(
|
||||||
|
context,
|
||||||
|
roomId,
|
||||||
|
attachments
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
val featureModules = FeatureModules(
|
val featureModules = FeatureModules(
|
||||||
|
@ -112,10 +126,12 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||||
matrixModules,
|
matrixModules,
|
||||||
domainModules,
|
domainModules,
|
||||||
trackingModule,
|
trackingModule,
|
||||||
|
coreAndroidModule,
|
||||||
imageLoaderModule,
|
imageLoaderModule,
|
||||||
context,
|
context,
|
||||||
buildMeta,
|
buildMeta,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers,
|
||||||
|
clock,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,10 +140,12 @@ internal class FeatureModules internal constructor(
|
||||||
private val matrixModules: MatrixModules,
|
private val matrixModules: MatrixModules,
|
||||||
private val domainModules: DomainModules,
|
private val domainModules: DomainModules,
|
||||||
private val trackingModule: TrackingModule,
|
private val trackingModule: TrackingModule,
|
||||||
|
private val coreAndroidModule: CoreAndroidModule,
|
||||||
imageLoaderModule: ImageLoaderModule,
|
imageLoaderModule: ImageLoaderModule,
|
||||||
context: Context,
|
context: Context,
|
||||||
buildMeta: BuildMeta,
|
buildMeta: BuildMeta,
|
||||||
coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
clock: Clock,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val directoryModule by unsafeLazy {
|
val directoryModule by unsafeLazy {
|
||||||
|
@ -155,12 +173,14 @@ internal class FeatureModules internal constructor(
|
||||||
matrixModules.room,
|
matrixModules.room,
|
||||||
storeModule.value.credentialsStore(),
|
storeModule.value.credentialsStore(),
|
||||||
storeModule.value.roomStore(),
|
storeModule.value.roomStore(),
|
||||||
|
clock
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile) }
|
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, buildMeta) }
|
||||||
val settingsModule by unsafeLazy {
|
val settingsModule by unsafeLazy {
|
||||||
SettingsModule(
|
SettingsModule(
|
||||||
storeModule.value,
|
storeModule.value,
|
||||||
|
pushModule,
|
||||||
matrixModules.crypto,
|
matrixModules.crypto,
|
||||||
matrixModules.sync,
|
matrixModules.sync,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
|
@ -168,19 +188,26 @@ internal class FeatureModules internal constructor(
|
||||||
coroutineDispatchers
|
coroutineDispatchers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync) }
|
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) }
|
||||||
val notificationsModule by unsafeLazy {
|
val notificationsModule by unsafeLazy {
|
||||||
NotificationsModule(
|
NotificationsModule(
|
||||||
matrixModules.push,
|
|
||||||
matrixModules.sync,
|
|
||||||
storeModule.value.credentialsStore(),
|
|
||||||
domainModules.pushModule.registerFirebasePushTokenUseCase(),
|
|
||||||
imageLoaderModule.iconLoader(),
|
imageLoaderModule.iconLoader(),
|
||||||
storeModule.value.roomStore(),
|
storeModule.value.roomStore(),
|
||||||
context,
|
context,
|
||||||
|
intentFactory = coreAndroidModule.intentFactory(),
|
||||||
|
dispatchers = coroutineDispatchers,
|
||||||
|
deviceMeta = DeviceMeta(Build.VERSION.SDK_INT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val shareEntryModule by unsafeLazy {
|
||||||
|
ShareEntryModule(matrixModules.sync, matrixModules.room)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pushModule by unsafeLazy {
|
||||||
|
domainModules.pushModule
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MatrixModules(
|
internal class MatrixModules(
|
||||||
|
@ -189,6 +216,7 @@ internal class MatrixModules(
|
||||||
private val workModule: WorkModule,
|
private val workModule: WorkModule,
|
||||||
private val logger: MatrixLogger,
|
private val logger: MatrixLogger,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val matrix by unsafeLazy {
|
val matrix by unsafeLazy {
|
||||||
|
@ -205,7 +233,8 @@ internal class MatrixModules(
|
||||||
installAuthService(credentialsStore)
|
installAuthService(credentialsStore)
|
||||||
installEncryptionService(store.knownDevicesStore())
|
installEncryptionService(store.knownDevicesStore())
|
||||||
|
|
||||||
val olmAccountStore = OlmPersistenceWrapper(store.olmStore())
|
val base64 = AndroidBase64()
|
||||||
|
val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64)
|
||||||
val singletonFlows = SingletonFlows(coroutineDispatchers)
|
val singletonFlows = SingletonFlows(coroutineDispatchers)
|
||||||
val olm = OlmWrapper(
|
val olm = OlmWrapper(
|
||||||
olmStore = olmAccountStore,
|
olmStore = olmAccountStore,
|
||||||
|
@ -225,13 +254,16 @@ internal class MatrixModules(
|
||||||
services.roomService().joinedMembers(it).map { it.userId }
|
services.roomService().joinedMembers(it).map { it.userId }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
base64 = base64,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
)
|
)
|
||||||
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler())) { serviceProvider ->
|
val imageContentReader = AndroidImageContentReader(contentResolver)
|
||||||
|
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), imageContentReader) { serviceProvider ->
|
||||||
MessageEncrypter { message ->
|
MessageEncrypter { message ->
|
||||||
val result = serviceProvider.cryptoService().encrypt(
|
val result = serviceProvider.cryptoService().encrypt(
|
||||||
roomId = when (message) {
|
roomId = when (message) {
|
||||||
is MessageService.Message.TextMessage -> message.roomId
|
is MessageService.Message.TextMessage -> message.roomId
|
||||||
|
is MessageService.Message.ImageMessage -> message.roomId
|
||||||
},
|
},
|
||||||
credentials = credentialsStore.credentials()!!,
|
credentials = credentialsStore.credentials()!!,
|
||||||
when (message) {
|
when (message) {
|
||||||
|
@ -246,6 +278,8 @@ internal class MatrixModules(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is MessageService.Message.ImageMessage -> TODO()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -283,6 +317,14 @@ internal class MatrixModules(
|
||||||
store.roomStore(),
|
store.roomStore(),
|
||||||
store.syncStore(),
|
store.syncStore(),
|
||||||
store.filterStore(),
|
store.filterStore(),
|
||||||
|
deviceNotifier = { services ->
|
||||||
|
val encryption = services.deviceService()
|
||||||
|
val crypto = services.cryptoService()
|
||||||
|
DeviceNotifier { userIds, syncToken ->
|
||||||
|
encryption.updateStaleDevices(userIds)
|
||||||
|
crypto.updateOlmSession(userIds, syncToken)
|
||||||
|
}
|
||||||
|
},
|
||||||
messageDecrypter = { serviceProvider ->
|
messageDecrypter = { serviceProvider ->
|
||||||
val cryptoService = serviceProvider.cryptoService()
|
val cryptoService = serviceProvider.cryptoService()
|
||||||
MessageDecrypter {
|
MessageDecrypter {
|
||||||
|
@ -296,9 +338,9 @@ internal class MatrixModules(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
verificationHandler = { services ->
|
verificationHandler = { services ->
|
||||||
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
|
|
||||||
val cryptoService = services.cryptoService()
|
val cryptoService = services.cryptoService()
|
||||||
VerificationHandler { apiEvent ->
|
VerificationHandler { apiEvent ->
|
||||||
|
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
|
||||||
cryptoService.onVerificationEvent(
|
cryptoService.onVerificationEvent(
|
||||||
when (apiEvent) {
|
when (apiEvent) {
|
||||||
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
|
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
|
||||||
|
@ -308,12 +350,14 @@ internal class MatrixModules(
|
||||||
apiEvent.content.methods,
|
apiEvent.content.methods,
|
||||||
apiEvent.content.timestampPosix,
|
apiEvent.content.timestampPosix,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
||||||
apiEvent.sender,
|
apiEvent.sender,
|
||||||
apiEvent.content.fromDevice,
|
apiEvent.content.fromDevice,
|
||||||
apiEvent.content.transactionId,
|
apiEvent.content.transactionId,
|
||||||
apiEvent.content.methods,
|
apiEvent.content.methods,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
||||||
apiEvent.sender,
|
apiEvent.sender,
|
||||||
apiEvent.content.fromDevice,
|
apiEvent.content.fromDevice,
|
||||||
|
@ -324,6 +368,7 @@ internal class MatrixModules(
|
||||||
apiEvent.content.short,
|
apiEvent.content.short,
|
||||||
apiEvent.content.transactionId,
|
apiEvent.content.transactionId,
|
||||||
)
|
)
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
||||||
is ApiToDeviceEvent.VerificationAccept -> TODO()
|
is ApiToDeviceEvent.VerificationAccept -> TODO()
|
||||||
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
||||||
|
@ -331,6 +376,7 @@ internal class MatrixModules(
|
||||||
apiEvent.content.transactionId,
|
apiEvent.content.transactionId,
|
||||||
apiEvent.content.key
|
apiEvent.content.key
|
||||||
)
|
)
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
||||||
apiEvent.sender,
|
apiEvent.sender,
|
||||||
apiEvent.content.transactionId,
|
apiEvent.content.transactionId,
|
||||||
|
@ -341,14 +387,6 @@ internal class MatrixModules(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deviceNotifier = { services ->
|
|
||||||
val encryption = services.deviceService()
|
|
||||||
val crypto = services.cryptoService()
|
|
||||||
DeviceNotifier { userIds, syncToken ->
|
|
||||||
encryption.updateStaleDevices(userIds)
|
|
||||||
crypto.updateOlmSession(userIds, syncToken)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
oneTimeKeyProducer = { services ->
|
oneTimeKeyProducer = { services ->
|
||||||
val cryptoService = services.cryptoService()
|
val cryptoService = services.cryptoService()
|
||||||
MaybeCreateMoreKeys {
|
MaybeCreateMoreKeys {
|
||||||
|
@ -359,6 +397,7 @@ internal class MatrixModules(
|
||||||
val roomService = services.roomService()
|
val roomService = services.roomService()
|
||||||
object : RoomMembersService {
|
object : RoomMembersService {
|
||||||
override suspend fun find(roomId: RoomId, userIds: List<UserId>) = roomService.findMembers(roomId, userIds)
|
override suspend fun find(roomId: RoomId, userIds: List<UserId>) = roomService.findMembers(roomId, userIds)
|
||||||
|
override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId)
|
||||||
override suspend fun insert(roomId: RoomId, members: List<RoomMember>) = roomService.insertMembers(roomId, members)
|
override suspend fun insert(roomId: RoomId, members: List<RoomMember>) = roomService.insertMembers(roomId, members)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -367,8 +406,6 @@ internal class MatrixModules(
|
||||||
)
|
)
|
||||||
|
|
||||||
installPushService(credentialsStore)
|
installPushService(credentialsStore)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -385,32 +422,49 @@ internal class MatrixModules(
|
||||||
internal class DomainModules(
|
internal class DomainModules(
|
||||||
private val matrixModules: MatrixModules,
|
private val matrixModules: MatrixModules,
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
|
private val workModule: WorkModule,
|
||||||
|
private val storeModule: Lazy<StoreModule>,
|
||||||
|
private val context: Application,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val pushModule by unsafeLazy { PushModule(matrixModules.push, errorTracker) }
|
val pushModule by unsafeLazy {
|
||||||
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run)) }
|
val store = storeModule.value
|
||||||
}
|
val pushHandler = MatrixPushHandler(
|
||||||
|
workScheduler = workModule.workScheduler(),
|
||||||
class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : BackgroundScheduler {
|
credentialsStore = store.credentialsStore(),
|
||||||
override fun schedule(key: String, task: BackgroundScheduler.Task) {
|
matrixModules.sync,
|
||||||
workScheduler.schedule(
|
store.roomStore(),
|
||||||
WorkScheduler.WorkTask(
|
|
||||||
jobId = 1,
|
|
||||||
type = task.type,
|
|
||||||
jsonPayload = task.jsonPayload,
|
|
||||||
)
|
)
|
||||||
|
PushModule(
|
||||||
|
errorTracker,
|
||||||
|
pushHandler,
|
||||||
|
context,
|
||||||
|
dispatchers,
|
||||||
|
SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", dispatchers)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
|
||||||
}
|
}
|
||||||
|
|
||||||
class TaskRunnerAdapter(private val matrixTaskRunner: suspend (MatrixTask) -> MatrixTaskRunner.TaskResult) : TaskRunner {
|
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
|
||||||
|
override fun read(uri: String): ImageContentReader.ImageContent {
|
||||||
|
val androidUri = Uri.parse(uri)
|
||||||
|
val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
|
||||||
|
|
||||||
override suspend fun run(tasks: List<TaskRunner.RunnableWorkTask>): List<TaskRunner.TaskResult> {
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
return tasks.map {
|
BitmapFactory.decodeStream(fileStream, null, options)
|
||||||
when (val result = matrixTaskRunner(MatrixTask(it.task.type, it.task.jsonPayload))) {
|
|
||||||
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
|
return contentResolver.openInputStream(androidUri)?.use { stream ->
|
||||||
MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
|
val output = stream.readBytes()
|
||||||
}
|
ImageContentReader.ImageContent(
|
||||||
}
|
height = options.outHeight,
|
||||||
|
width = options.outWidth,
|
||||||
|
size = output.size.toLong(),
|
||||||
|
mimeType = options.outMimeType,
|
||||||
|
fileName = androidUri.lastPathSegment ?: "file",
|
||||||
|
content = output
|
||||||
|
)
|
||||||
|
} ?: throw IllegalArgumentException("Could not process $uri")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package app.dapk.st.graph
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.push.PushService
|
||||||
|
import app.dapk.st.push.PushTokenPayload
|
||||||
|
import app.dapk.st.work.TaskRunner
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class AppTaskRunner(
|
||||||
|
private val pushService: PushService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult {
|
||||||
|
return when (val type = workTask.task.type) {
|
||||||
|
"push_token" -> {
|
||||||
|
runCatching {
|
||||||
|
val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload)
|
||||||
|
pushService.registerPush(payload.token, payload.gatewayUrl)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { TaskRunner.TaskResult.Success(workTask.source) },
|
||||||
|
onFailure = {
|
||||||
|
val canRetry = if (it is ClientRequestException) {
|
||||||
|
it.response.status.value !in (400 until 500)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
TaskRunner.TaskResult.Failure(workTask.source, canRetry = canRetry)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException("Unknown work type: $type")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package app.dapk.st.graph
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.message.BackgroundScheduler
|
||||||
|
import app.dapk.st.work.WorkScheduler
|
||||||
|
|
||||||
|
class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : BackgroundScheduler {
|
||||||
|
override fun schedule(key: String, task: BackgroundScheduler.Task) {
|
||||||
|
workScheduler.schedule(
|
||||||
|
WorkScheduler.WorkTask(
|
||||||
|
jobId = 1,
|
||||||
|
type = task.type,
|
||||||
|
jsonPayload = task.jsonPayload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package app.dapk.st.graph
|
||||||
|
|
||||||
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.withIoContext
|
||||||
|
import app.dapk.st.domain.DatabaseDropper
|
||||||
|
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||||
|
|
||||||
|
class DefaultDatabaseDropper(
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
private val driver: AndroidSqliteDriver,
|
||||||
|
) : DatabaseDropper {
|
||||||
|
|
||||||
|
override suspend fun dropAllTables(deleteCrypto: Boolean) {
|
||||||
|
coroutineDispatchers.withIoContext {
|
||||||
|
val cursor = driver.executeQuery(
|
||||||
|
identifier = null,
|
||||||
|
sql = "SELECT name FROM sqlite_master WHERE type = 'table'",
|
||||||
|
parameters = 0
|
||||||
|
)
|
||||||
|
cursor.use {
|
||||||
|
while (cursor.next()) {
|
||||||
|
cursor.getString(0)?.let {
|
||||||
|
if (!deleteCrypto && it.startsWith("dbCrypto")) {
|
||||||
|
// skip
|
||||||
|
} else {
|
||||||
|
driver.execute(null, "DELETE FROM $it", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package app.dapk.st.graph
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.MatrixTaskRunner
|
||||||
|
import app.dapk.st.work.TaskRunner
|
||||||
|
|
||||||
|
class TaskRunnerAdapter(
|
||||||
|
private val matrixTaskRunner: suspend (MatrixTaskRunner.MatrixTask) -> MatrixTaskRunner.TaskResult,
|
||||||
|
private val appTaskRunner: AppTaskRunner,
|
||||||
|
) : TaskRunner {
|
||||||
|
|
||||||
|
override suspend fun run(tasks: List<TaskRunner.RunnableWorkTask>): List<TaskRunner.TaskResult> {
|
||||||
|
return tasks.map {
|
||||||
|
when {
|
||||||
|
it.task.type.startsWith("matrix") -> {
|
||||||
|
when (val result = matrixTaskRunner(MatrixTaskRunner.MatrixTask(it.task.type, it.task.jsonPayload))) {
|
||||||
|
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
|
||||||
|
MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> appTaskRunner.run(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<share-target android:targetClass="app.dapk.st.share.ShareEntryActivity">
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
|
<category android:name="android.shortcut.conversation" />
|
||||||
|
</share-target>
|
||||||
|
</shortcuts>
|
41
build.gradle
41
build.gradle
|
@ -9,7 +9,7 @@ buildscript {
|
||||||
classpath Dependencies.mavenCentral.kotlinGradlePlugin
|
classpath Dependencies.mavenCentral.kotlinGradlePlugin
|
||||||
classpath Dependencies.mavenCentral.sqldelightGradlePlugin
|
classpath Dependencies.mavenCentral.sqldelightGradlePlugin
|
||||||
classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin
|
classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin
|
||||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
|
classpath Dependencies.google.firebaseCrashlyticsPlugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,14 +18,16 @@ def launchTask = getGradle()
|
||||||
.getTaskRequests()
|
.getTaskRequests()
|
||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
def isReleaseBuild = launchTask.contains("release")
|
||||||
|
ext.isDebugBuild = !isReleaseBuild
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
freeCompilerArgs = [
|
freeCompilerArgs = [
|
||||||
'-Xopt-in=kotlin.contracts.ExperimentalContracts',
|
'-opt-in=kotlin.contracts.ExperimentalContracts',
|
||||||
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +54,7 @@ ext.applyLibraryPlugins = { project ->
|
||||||
project.apply plugin: 'kotlin-android'
|
project.apply plugin: 'kotlin-android'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.androidSdkVersion = 31
|
ext.androidSdkVersion = 32
|
||||||
|
|
||||||
ext.applyCommonAndroidParameters = { project ->
|
ext.applyCommonAndroidParameters = { project ->
|
||||||
def android = project.android
|
def android = project.android
|
||||||
|
@ -63,14 +65,9 @@ ext.applyCommonAndroidParameters = { project ->
|
||||||
incremental = true
|
incremental = true
|
||||||
}
|
}
|
||||||
android.defaultConfig {
|
android.defaultConfig {
|
||||||
minSdkVersion 29
|
minSdkVersion 24
|
||||||
targetSdkVersion androidSdkVersion
|
targetSdkVersion androidSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
android.buildFeatures.compose = true
|
|
||||||
android.composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = Dependencies.google.kotlinCompilerExtensionVersion
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.applyLibraryModuleOptimisations = { project ->
|
ext.applyLibraryModuleOptimisations = { project ->
|
||||||
|
@ -101,17 +98,26 @@ ext.applyCompose = { project ->
|
||||||
dependencies.implementation Dependencies.google.androidxComposeMaterial
|
dependencies.implementation Dependencies.google.androidxComposeMaterial
|
||||||
dependencies.implementation Dependencies.google.androidxComposeIconsExtended
|
dependencies.implementation Dependencies.google.androidxComposeIconsExtended
|
||||||
dependencies.implementation Dependencies.google.androidxActivityCompose
|
dependencies.implementation Dependencies.google.androidxActivityCompose
|
||||||
|
|
||||||
|
def android = project.android
|
||||||
|
android.buildFeatures.compose = true
|
||||||
|
android.composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = Dependencies.google.kotlinCompilerExtensionVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ext.applyAndroidComposeLibraryModule = { project ->
|
||||||
|
applyAndroidLibraryModule(project)
|
||||||
|
applyCompose(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.applyAndroidLibraryModule = { project ->
|
ext.applyAndroidLibraryModule = { project ->
|
||||||
applyLibraryPlugins(project)
|
applyLibraryPlugins(project)
|
||||||
applyCommonAndroidParameters(project)
|
applyCommonAndroidParameters(project)
|
||||||
applyLibraryModuleOptimisations(project)
|
applyLibraryModuleOptimisations(project)
|
||||||
applyCompose(project)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.applyCrashlyticsIfRelease = { project ->
|
ext.applyCrashlyticsIfRelease = { project ->
|
||||||
def isReleaseBuild = launchTask.contains("release")
|
|
||||||
if (isReleaseBuild) {
|
if (isReleaseBuild) {
|
||||||
project.apply plugin: 'com.google.firebase.crashlytics'
|
project.apply plugin: 'com.google.firebase.crashlytics'
|
||||||
project.afterEvaluate {
|
project.afterEvaluate {
|
||||||
|
@ -126,16 +132,17 @@ ext.kotlinTest = { dependencies ->
|
||||||
dependencies.testImplementation Dependencies.mavenCentral.kluent
|
dependencies.testImplementation Dependencies.mavenCentral.kluent
|
||||||
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
|
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
|
||||||
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10"
|
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10"
|
||||||
dependencies.testImplementation 'io.mockk:mockk:1.12.2'
|
dependencies.testImplementation 'io.mockk:mockk:1.12.7'
|
||||||
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
|
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
||||||
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
|
dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.kotlinFixtures = { dependencies ->
|
ext.kotlinFixtures = { dependencies ->
|
||||||
dependencies.testFixturesImplementation 'io.mockk:mockk:1.12.2'
|
dependencies.testFixturesImplementation 'io.mockk:mockk:1.12.7'
|
||||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
|
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||||
|
dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.androidImportFixturesWorkaround = { project, fixtures ->
|
ext.androidImportFixturesWorkaround = { project, fixtures ->
|
||||||
|
|
|
@ -4,9 +4,9 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
api Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kluent
|
testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||||
testFixturesImplementation 'io.mockk:mockk:1.12.2'
|
testFixturesImplementation Dependencies.mavenCentral.mockk
|
||||||
testFixturesImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
|
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
|
||||||
}
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class AndroidUri(val value: String)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
interface Base64 {
|
||||||
|
fun encode(input: ByteArray): String
|
||||||
|
fun decode(input: String): ByteArray
|
||||||
|
}
|
|
@ -2,4 +2,5 @@ package app.dapk.st.core
|
||||||
|
|
||||||
data class BuildMeta(
|
data class BuildMeta(
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
|
val versionCode: Int,
|
||||||
)
|
)
|
|
@ -2,7 +2,11 @@ package app.dapk.st.core
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
data class CoroutineDispatchers(val io: CoroutineDispatcher = Dispatchers.IO, val global: CoroutineScope = GlobalScope)
|
data class CoroutineDispatchers(
|
||||||
|
val io: CoroutineDispatcher = Dispatchers.IO,
|
||||||
|
val main: CoroutineDispatcher = Dispatchers.Main,
|
||||||
|
val global: CoroutineScope = GlobalScope,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun <T> CoroutineDispatchers.withIoContext(
|
suspend fun <T> CoroutineDispatchers.withIoContext(
|
||||||
block: suspend CoroutineScope.() -> T
|
block: suspend CoroutineScope.() -> T
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
data class DeviceMeta(
|
||||||
|
val apiVersion: Int
|
||||||
|
)
|
|
@ -0,0 +1,27 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
class LRUCache<K, V>(val maxSize: Int) {
|
||||||
|
|
||||||
|
private val internalCache = object : LinkedHashMap<K, V>(0, 0.75f, true) {
|
||||||
|
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
|
||||||
|
return size > maxSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(key: K, value: V) {
|
||||||
|
internalCache[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: K): V? {
|
||||||
|
return internalCache[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrPut(key: K, value: () -> V): V {
|
||||||
|
return get(key) ?: value().also { put(key, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun size() = internalCache.size
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LRUCache<*, *>?.isNullOrEmpty() = this == null || this.size() == 0
|
|
@ -0,0 +1,5 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
sealed interface MimeType {
|
||||||
|
object Image: MimeType
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import kotlin.reflect.KClass
|
||||||
interface ModuleProvider {
|
interface ModuleProvider {
|
||||||
|
|
||||||
fun <T : ProvidableModule> provide(klass: KClass<T>): T
|
fun <T : ProvidableModule> provide(klass: KClass<T>): T
|
||||||
|
fun reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProvidableModule
|
interface ProvidableModule
|
|
@ -31,10 +31,12 @@ class SingletonFlows(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T> get(key: String): Flow<T> {
|
fun <T> get(key: String): Flow<T> {
|
||||||
return cache[key]!! as Flow<T>
|
return cache[key]!! as Flow<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
suspend fun <T> update(key: String, value: T) {
|
suspend fun <T> update(key: String, value: T) {
|
||||||
(cache[key] as? MutableSharedFlow<T>)?.emit(value)
|
(cache[key] as? MutableSharedFlow<T>)?.emit(value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,3 +21,25 @@ inline fun <T, T1 : T, T2 : T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)
|
fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)
|
||||||
|
|
||||||
|
class ResettableUnsafeLazy<T>(private val initializer: () -> T) : Lazy<T> {
|
||||||
|
|
||||||
|
private var _value: T? = null
|
||||||
|
|
||||||
|
override val value: T
|
||||||
|
get() {
|
||||||
|
return if (_value == null) {
|
||||||
|
initializer().also { _value = it }
|
||||||
|
} else {
|
||||||
|
_value!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isInitialized(): Boolean {
|
||||||
|
return _value != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_value = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
|
fun <K, V> Map<K, V>?.containsKey(key: K) = this?.containsKey(key) ?: false
|
||||||
|
|
||||||
|
fun <K, V> MutableMap<K,V>.clearAndPutAll(input: Map<K, V>) {
|
||||||
|
this.clear()
|
||||||
|
this.putAll(input)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package fixture
|
||||||
|
|
||||||
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
object CoroutineDispatchersFixture {
|
||||||
|
|
||||||
|
fun aCoroutineDispatchers() = CoroutineDispatchers(
|
||||||
|
Dispatchers.Unconfined,
|
||||||
|
main = Dispatchers.Unconfined,
|
||||||
|
global = CoroutineScope(Dispatchers.Unconfined)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import io.mockk.MockKMatcherScope
|
import io.mockk.*
|
||||||
import io.mockk.MockKVerificationScope
|
|
||||||
import io.mockk.coJustRun
|
|
||||||
import io.mockk.coVerifyAll
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
@ -12,23 +9,30 @@ fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) {
|
||||||
runTest { testBody(ExpectTest(coroutineContext)) }
|
runTest { testBody(ExpectTest(coroutineContext)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
|
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
|
||||||
|
|
||||||
private val expects = mutableListOf<suspend MockKVerificationScope.() -> Unit>()
|
private val expects = mutableListOf<Pair<Int, suspend MockKVerificationScope.() -> Unit>>()
|
||||||
|
private val groups = mutableListOf<suspend MockKVerificationScope.() -> Unit>()
|
||||||
|
|
||||||
override fun verifyExpects() = coVerifyAll { expects.forEach { it.invoke(this@coVerifyAll) } }
|
override fun verifyExpects() {
|
||||||
|
expects.forEach { (times, block) -> coVerify(exactly = times) { block.invoke(this) } }
|
||||||
override fun <T> T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit) {
|
groups.forEach { coVerifyOrder { it.invoke(this) } }
|
||||||
coJustRun { block(this@expectUnit) }.ignore()
|
|
||||||
expects.add { block(this@expectUnit) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T> T.expectUnit(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) {
|
||||||
|
coJustRun { block(this@expectUnit) }.ignore()
|
||||||
|
expects.add(times to { block(this@expectUnit) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) {
|
||||||
|
groups.add { block(this@captureExpects) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Any.ignore() = Unit
|
private fun Any.ignore() = Unit
|
||||||
|
|
||||||
interface ExpectTestScope : CoroutineScope {
|
interface ExpectTestScope : CoroutineScope {
|
||||||
fun verifyExpects()
|
fun verifyExpects()
|
||||||
fun <T> T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit)
|
fun <T> T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit)
|
||||||
|
fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit)
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package test
|
package test
|
||||||
|
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
inline fun <T : Any, reified R> T.expect(crossinline block: suspend MockKMatcherScope.(T) -> R) {
|
inline fun <T : Any, reified R> T.expect(crossinline block: suspend MockKMatcherScope.(T) -> R) {
|
||||||
coEvery { block(this@expect) } returns mockk(relaxed = true)
|
coEvery { block(this@expect) } returns mockk(relaxed = true)
|
||||||
|
@ -16,11 +18,22 @@ fun <T, B> MockKStubScope<T, B>.delegateReturn() = object : Returns<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T, B> MockKStubScope<Flow<T>, B>.delegateEmit() = object : Emits<T> {
|
||||||
|
override fun emits(vararg values: T) {
|
||||||
|
answers(ConstantAnswer(flowOf(*values)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <T> returns(block: (T) -> Unit) = object : Returns<T> {
|
fun <T> returns(block: (T) -> Unit) = object : Returns<T> {
|
||||||
override fun returns(value: T) = block(value)
|
override fun returns(value: T) = block(value)
|
||||||
override fun throws(value: Throwable) = throw value
|
override fun throws(value: Throwable) = throw value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Emits<T> {
|
||||||
|
fun emits(vararg values: T)
|
||||||
|
}
|
||||||
|
|
||||||
interface Returns<T> {
|
interface Returns<T> {
|
||||||
fun returns(value: T)
|
fun returns(value: T)
|
||||||
fun throws(value: Throwable)
|
fun throws(value: Throwable)
|
||||||
|
|
|
@ -10,6 +10,13 @@ ext.Dependencies.with {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositories.maven {
|
||||||
|
url 'https://jitpack.io'
|
||||||
|
content {
|
||||||
|
includeGroup "com.github.UnifiedPush"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
repositories.mavenCentral {
|
repositories.mavenCentral {
|
||||||
content {
|
content {
|
||||||
includeGroupByRegex "org\\.jetbrains.*"
|
includeGroupByRegex "org\\.jetbrains.*"
|
||||||
|
@ -88,45 +95,64 @@ ext.Dependencies.with {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def kotlinVer = "1.6.10"
|
def kotlinVer = "1.7.10"
|
||||||
def sqldelightVer = "1.5.3"
|
def sqldelightVer = "1.5.3"
|
||||||
def composeVer = "1.1.0"
|
def composeVer = "1.2.1"
|
||||||
|
def ktorVer = "2.1.0"
|
||||||
|
|
||||||
google = new DependenciesContainer()
|
google = new DependenciesContainer()
|
||||||
google.with {
|
google.with {
|
||||||
androidGradlePlugin = "com.android.tools.build:gradle:7.1.2"
|
androidGradlePlugin = "com.android.tools.build:gradle:7.2.1"
|
||||||
|
|
||||||
androidxComposeUi = "androidx.compose.ui:ui:${composeVer}"
|
androidxComposeUi = "androidx.compose.ui:ui:${composeVer}"
|
||||||
androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}"
|
androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}"
|
||||||
androidxComposeMaterial = "androidx.compose.material:material:${composeVer}"
|
androidxComposeMaterial = "androidx.compose.material:material:${composeVer}"
|
||||||
androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}"
|
androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}"
|
||||||
androidxActivityCompose = "androidx.activity:activity-compose:1.4.0"
|
androidxActivityCompose = "androidx.activity:activity-compose:1.4.0"
|
||||||
kotlinCompilerExtensionVersion = "1.1.0-rc02"
|
kotlinCompilerExtensionVersion = "1.3.0"
|
||||||
|
|
||||||
|
firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1"
|
||||||
|
jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5"
|
||||||
}
|
}
|
||||||
|
|
||||||
mavenCentral = new DependenciesContainer()
|
mavenCentral = new DependenciesContainer()
|
||||||
mavenCentral.with {
|
mavenCentral.with {
|
||||||
kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}"
|
kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}"
|
||||||
kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}"
|
kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}"
|
||||||
kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
|
kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0"
|
||||||
kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-RC2"
|
kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
|
||||||
|
kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}"
|
kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}"
|
||||||
|
|
||||||
sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}"
|
sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}"
|
||||||
sqldelightAndroid = "com.squareup.sqldelight:android-driver:${sqldelightVer}"
|
sqldelightAndroid = "com.squareup.sqldelight:android-driver:${sqldelightVer}"
|
||||||
sqldelightInMemory = "com.squareup.sqldelight:sqlite-driver:${sqldelightVer}"
|
sqldelightInMemory = "com.squareup.sqldelight:sqlite-driver:${sqldelightVer}"
|
||||||
|
|
||||||
ktorAndroid = "io.ktor:ktor-client-android:1.6.4"
|
leakCanary = 'com.squareup.leakcanary:leakcanary-android:2.9.1'
|
||||||
ktorCore = "io.ktor:ktor-client-core:1.6.2"
|
|
||||||
ktorSerialization = "io.ktor:ktor-client-serialization:1.5.0"
|
ktorAndroid = "io.ktor:ktor-client-android:${ktorVer}"
|
||||||
ktorLogging = "io.ktor:ktor-client-logging-jvm:1.6.2"
|
ktorCore = "io.ktor:ktor-client-core:${ktorVer}"
|
||||||
ktorJava = "io.ktor:ktor-client-java:1.6.2"
|
ktorSerialization = "io.ktor:ktor-client-serialization:${ktorVer}"
|
||||||
|
ktorJson = "io.ktor:ktor-serialization-kotlinx-json:${ktorVer}"
|
||||||
|
ktorLogging = "io.ktor:ktor-client-logging-jvm:${ktorVer}"
|
||||||
|
ktorJava = "io.ktor:ktor-client-java:${ktorVer}"
|
||||||
|
ktorContentNegotiation = "io.ktor:ktor-client-content-negotiation:${ktorVer}"
|
||||||
|
|
||||||
|
coil = "io.coil-kt:coil-compose:2.2.0"
|
||||||
|
accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.25.1"
|
||||||
|
|
||||||
junit = "junit:junit:4.13.2"
|
junit = "junit:junit:4.13.2"
|
||||||
kluent = "org.amshove.kluent:kluent:1.68"
|
kluent = "org.amshove.kluent:kluent:1.68"
|
||||||
|
mockk = 'io.mockk:mockk:1.12.7'
|
||||||
|
|
||||||
matrixOlm = "org.matrix.android:olm-sdk:3.2.10"
|
matrixOlm = "org.matrix.android:olm-sdk:3.2.12"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jitPack = new DependenciesContainer()
|
||||||
|
jitPack.with {
|
||||||
|
unifiedPush = "com.github.UnifiedPush:android-connector:2.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DependenciesContainer extends GroovyObjectSupport {
|
class DependenciesContainer extends GroovyObjectSupport {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
applyAndroidLibraryModule(project)
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation("io.coil-kt:coil-compose:1.4.0")
|
implementation Dependencies.mavenCentral.coil
|
||||||
implementation "com.google.accompanist:accompanist-systemuicontroller:0.24.1-alpha"
|
implementation Dependencies.mavenCentral.accompanistSystemuicontroller
|
||||||
}
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GenericEmpty() {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text("Nothing to see here...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GenericError(retryAction: () -> Unit) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text("Something went wrong...")
|
||||||
|
Button(onClick = { retryAction() }) {
|
||||||
|
Text("Retry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,12 +10,14 @@ import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.ExperimentalUnitApi
|
import androidx.compose.ui.unit.ExperimentalUnitApi
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
import coil.compose.rememberImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import coil.request.ImageRequest
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
|
|
||||||
@OptIn(ExperimentalUnitApi::class)
|
@OptIn(ExperimentalUnitApi::class)
|
||||||
|
@ -25,7 +27,8 @@ fun BoxScope.CircleishAvatar(avatarUrl: String?, fallbackLabel: String, size: Dp
|
||||||
null -> {
|
null -> {
|
||||||
val colors = SmallTalkTheme.extendedColors.getMissingImageColor(fallbackLabel)
|
val colors = SmallTalkTheme.extendedColors.getMissingImageColor(fallbackLabel)
|
||||||
Box(
|
Box(
|
||||||
Modifier.align(Alignment.Center)
|
Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
.background(color = colors.first, shape = CircleShape)
|
.background(color = colors.first, shape = CircleShape)
|
||||||
.size(size),
|
.size(size),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
|
@ -40,14 +43,16 @@ fun BoxScope.CircleishAvatar(avatarUrl: String?, fallbackLabel: String, size: Dp
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Image(
|
Image(
|
||||||
painter = rememberImagePainter(
|
painter = rememberAsyncImagePainter(
|
||||||
data = avatarUrl,
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
builder = {
|
.data(avatarUrl)
|
||||||
transformations(CircleCropTransformation())
|
.transformations(CircleCropTransformation())
|
||||||
}
|
.build()
|
||||||
),
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(size).align(Alignment.Center)
|
modifier = Modifier
|
||||||
|
.size(size)
|
||||||
|
.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +61,11 @@ fun BoxScope.CircleishAvatar(avatarUrl: String?, fallbackLabel: String, size: Dp
|
||||||
@Composable
|
@Composable
|
||||||
fun MissingAvatarIcon(displayName: String, displayImageSize: Dp) {
|
fun MissingAvatarIcon(displayName: String, displayImageSize: Dp) {
|
||||||
val colors = SmallTalkTheme.extendedColors.getMissingImageColor(displayName)
|
val colors = SmallTalkTheme.extendedColors.getMissingImageColor(displayName)
|
||||||
Box(Modifier.background(color = colors.first, shape = CircleShape).size(displayImageSize), contentAlignment = Alignment.Center) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.background(color = colors.first, shape = CircleShape)
|
||||||
|
.size(displayImageSize), contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = (displayName).first().toString().uppercase(),
|
text = (displayName).first().toString().uppercase(),
|
||||||
color = colors.second
|
color = colors.second
|
||||||
|
@ -67,11 +76,11 @@ fun MissingAvatarIcon(displayName: String, displayImageSize: Dp) {
|
||||||
@Composable
|
@Composable
|
||||||
fun MessengerUrlIcon(avatarUrl: String, displayImageSize: Dp) {
|
fun MessengerUrlIcon(avatarUrl: String, displayImageSize: Dp) {
|
||||||
Image(
|
Image(
|
||||||
painter = rememberImagePainter(
|
painter = rememberAsyncImagePainter(
|
||||||
data = avatarUrl,
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
builder = {
|
.data(avatarUrl)
|
||||||
transformations(CircleCropTransformation())
|
.transformations(CircleCropTransformation())
|
||||||
}
|
.build()
|
||||||
),
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(displayImageSize)
|
modifier = Modifier.size(displayImageSize)
|
||||||
|
|
|
@ -27,14 +27,13 @@ fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
if (currentPage.hasToolbar) {
|
||||||
Toolbar(
|
Toolbar(
|
||||||
onNavigate = navigateAndPopStack,
|
onNavigate = navigateAndPopStack,
|
||||||
title = currentPage.label
|
title = currentPage.label
|
||||||
)
|
)
|
||||||
|
|
||||||
currentPage.parent?.let {
|
|
||||||
BackHandler(onBack = navigateAndPopStack)
|
|
||||||
}
|
}
|
||||||
|
BackHandler(onBack = navigateAndPopStack)
|
||||||
computedWeb[currentPage.route]!!.invoke(currentPage.state)
|
computedWeb[currentPage.route]!!.invoke(currentPage.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,38 @@
|
||||||
package app.dapk.st.design.components
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ColumnScope.Toolbar(onNavigate: () -> Unit, title: String? = null, actions: @Composable RowScope.() -> Unit = {}) {
|
fun Toolbar(
|
||||||
|
onNavigate: (() -> Unit)? = null,
|
||||||
|
title: String? = null,
|
||||||
|
offset: (Density.() -> IntOffset)? = null,
|
||||||
|
actions: @Composable RowScope.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
val navigationIcon = foo(onNavigate)
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier.height(72.dp),
|
modifier = Modifier.height(72.dp).run {
|
||||||
backgroundColor = Color.Transparent,
|
if (offset == null) {
|
||||||
navigationIcon = {
|
this
|
||||||
IconButton(onClick = { onNavigate() }) {
|
} else {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = null)
|
this.offset(offset)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
title = title?.let {
|
title = title?.let {
|
||||||
{ Text(it, maxLines = 2) }
|
{ Text(it, maxLines = 2) }
|
||||||
} ?: {},
|
} ?: {},
|
||||||
|
@ -30,3 +41,17 @@ fun ColumnScope.Toolbar(onNavigate: () -> Unit, title: String? = null, actions:
|
||||||
)
|
)
|
||||||
Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black.copy(alpha = 0.2f), thickness = 0.5.dp)
|
Divider(modifier = Modifier.fillMaxWidth(), color = Color.Black.copy(alpha = 0.2f), thickness = 0.5.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun foo(onNavigate: (() -> Unit)?): (@Composable () -> Unit)? {
|
||||||
|
return onNavigate?.let {
|
||||||
|
{ NavigationIcon(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationIcon(onNavigate: () -> Unit) {
|
||||||
|
IconButton(onClick = { onNavigate.invoke() }) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null) {
|
fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null, body: @Composable () -> Unit = {}) {
|
||||||
val modifier = Modifier.padding(horizontal = 24.dp)
|
val modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -31,6 +31,7 @@ fun TextRow(title: String, content: String? = null, includeDivider: Boolean = tr
|
||||||
Text(text = content, fontSize = 18.sp)
|
Text(text = content, fontSize = 18.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
body()
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
if (includeDivider) {
|
if (includeDivider) {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":core")
|
||||||
|
implementation project(":features:navigator")
|
||||||
|
api project(":domains:android:core")
|
||||||
|
}
|
|
@ -41,6 +41,9 @@ fun LifecycleEffect(onStart: () -> Unit = {}, onStop: () -> Unit = {}) {
|
||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_START -> onStart()
|
Lifecycle.Event.ON_START -> onStart()
|
||||||
Lifecycle.Event.ON_STOP -> onStop()
|
Lifecycle.Event.ON_STOP -> onStop()
|
||||||
|
else -> {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
applyAndroidLibraryModule(project)
|
plugins { id 'kotlin' }
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
compileOnly project(":domains:android:stub")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation project(":features:navigator")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@ package app.dapk.st.core
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
inline fun <reified T : ProvidableModule> Context.module() =
|
inline fun <reified T : ProvidableModule> Context.module() = (this.applicationContext as ModuleProvider).provide(T::class)
|
||||||
(this.applicationContext as ModuleProvider).provide(T::class)
|
|
||||||
|
fun Context.resetModules() = (this.applicationContext as ModuleProvider).reset()
|
|
@ -0,0 +1,19 @@
|
||||||
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
fun <T> DeviceMeta.isAtLeastO(block: () -> T, fallback: () -> T = { throw IllegalStateException("not handled") }): T {
|
||||||
|
return if (this.apiVersion >= Build.VERSION_CODES.O) block() else fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DeviceMeta.onAtLeastO(block: () -> Unit) {
|
||||||
|
if (this.apiVersion >= Build.VERSION_CODES.O) block()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> DeviceMeta.whenPOrHigher(block: () -> T, fallback: () -> T) = whenXOrHigher(Build.VERSION_CODES.P, block, fallback)
|
||||||
|
inline fun <T> DeviceMeta.whenOOrHigher(block: () -> T, fallback: () -> T) = whenXOrHigher(Build.VERSION_CODES.O, block, fallback)
|
||||||
|
|
||||||
|
inline fun <T> DeviceMeta.whenXOrHigher(version: Int, block: () -> T, fallback: () -> T): T {
|
||||||
|
return if (this.apiVersion >= version) block() else fallback()
|
||||||
|
}
|
||||||
|
|
|
@ -2,5 +2,5 @@ applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation "io.coil-kt:coil:1.4.0"
|
implementation Dependencies.mavenCentral.coil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package app.dapk.st.imageloader
|
package app.dapk.st.imageloader
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import coil.request.ImageResult
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import coil.transform.Transformation
|
import coil.transform.Transformation
|
||||||
import coil.load as coilLoad
|
import coil.load as coilLoad
|
||||||
|
@ -14,7 +15,6 @@ import coil.load as coilLoad
|
||||||
interface ImageLoader {
|
interface ImageLoader {
|
||||||
|
|
||||||
suspend fun load(url: String, transformation: Transformation? = null): Drawable?
|
suspend fun load(url: String, transformation: Transformation? = null): Drawable?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IconLoader {
|
interface IconLoader {
|
||||||
|
@ -31,19 +31,24 @@ class CachedIcons(private val imageLoader: ImageLoader) : IconLoader {
|
||||||
|
|
||||||
override suspend fun load(url: String): Icon? {
|
override suspend fun load(url: String): Icon? {
|
||||||
return cache.getOrPut(url) {
|
return cache.getOrPut(url) {
|
||||||
imageLoader.load(url, transformation = circleCrop)?.toBitmap()?.let {
|
imageLoader.load(url, transformation = circleCrop)?.asBitmap()?.let {
|
||||||
Icon.createWithBitmap(it)
|
Icon.createWithBitmap(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Drawable.asBitmap() = (this as? BitmapDrawable)?.bitmap
|
||||||
|
|
||||||
internal class CoilImageLoader(private val context: Context) : ImageLoader {
|
internal class CoilImageLoader(private val context: Context) : ImageLoader {
|
||||||
|
|
||||||
private val coil = context.imageLoader
|
private val coil = context.imageLoader
|
||||||
|
|
||||||
override suspend fun load(url: String, transformation: Transformation?): Drawable? {
|
override suspend fun load(url: String, transformation: Transformation?): Drawable? {
|
||||||
|
return internalLoad(url, transformation).drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun internalLoad(url: String, transformation: Transformation?): ImageResult {
|
||||||
val request = ImageRequest.Builder(context)
|
val request = ImageRequest.Builder(context)
|
||||||
.data(url)
|
.data(url)
|
||||||
.let {
|
.let {
|
||||||
|
@ -53,7 +58,7 @@ internal class CoilImageLoader(private val context: Context) : ImageLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
return coil.execute(request).drawable
|
return coil.execute(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
applyAndroidLibraryModule(project)
|
applyAndroidLibraryModule(project)
|
||||||
|
apply plugin: "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
|
implementation project(':domains:android:core')
|
||||||
|
implementation project(':domains:store')
|
||||||
implementation project(':matrix:services:push')
|
implementation project(':matrix:services:push')
|
||||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||||
implementation 'com.google.firebase:firebase-messaging'
|
implementation 'com.google.firebase:firebase-messaging'
|
||||||
|
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||||
|
implementation Dependencies.jitPack.unifiedPush
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,24 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest package="app.dapk.st.push"/>
|
<manifest package="app.dapk.st.push" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".firebase.FirebasePushService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver android:exported="true" android:enabled="true" android:name=".unifiedpush.UnifiedPushMessageReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
|
||||||
|
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/>
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,17 @@
|
||||||
|
package app.dapk.st.push
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.EventId
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
interface PushHandler {
|
||||||
|
fun onNewToken(payload: PushTokenPayload)
|
||||||
|
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PushTokenPayload(
|
||||||
|
@SerialName("token") val token: String,
|
||||||
|
@SerialName("gateway_url") val gatewayUrl: String,
|
||||||
|
)
|
|
@ -1,16 +1,40 @@
|
||||||
package app.dapk.st.push
|
package app.dapk.st.push
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.matrix.push.PushService
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import app.dapk.st.domain.Preferences
|
||||||
|
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
||||||
|
import app.dapk.st.push.firebase.FirebasePushTokenRegistrar
|
||||||
|
import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
|
||||||
|
|
||||||
class PushModule(
|
class PushModule(
|
||||||
private val pushService: PushService,
|
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
) {
|
private val pushHandler: PushHandler,
|
||||||
|
private val context: Context,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
private val preferences: Preferences,
|
||||||
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun registerFirebasePushTokenUseCase() = RegisterFirebasePushTokenUseCase(
|
private val registrars by unsafeLazy {
|
||||||
pushService,
|
PushTokenRegistrars(
|
||||||
|
context,
|
||||||
|
FirebasePushTokenRegistrar(
|
||||||
errorTracker,
|
errorTracker,
|
||||||
|
context,
|
||||||
|
pushHandler,
|
||||||
|
),
|
||||||
|
UnifiedPushRegistrar(context),
|
||||||
|
PushTokenRegistrarPreferences(preferences)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pushTokenRegistrars() = registrars
|
||||||
|
|
||||||
|
fun pushTokenRegistrar(): PushTokenRegistrar = pushTokenRegistrars()
|
||||||
|
fun pushHandler() = pushHandler
|
||||||
|
fun dispatcher() = dispatchers
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package app.dapk.st.push
|
||||||
|
|
||||||
|
interface PushTokenRegistrar {
|
||||||
|
suspend fun registerCurrentToken()
|
||||||
|
fun unregister()
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package app.dapk.st.push
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
||||||
|
import app.dapk.st.push.firebase.FirebasePushTokenRegistrar
|
||||||
|
import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
|
||||||
|
import org.unifiedpush.android.connector.UnifiedPush
|
||||||
|
|
||||||
|
private val FIREBASE_OPTION = Registrar("Google - Firebase (FCM)")
|
||||||
|
private val NONE = Registrar("None")
|
||||||
|
|
||||||
|
class PushTokenRegistrars(
|
||||||
|
private val context: Context,
|
||||||
|
private val firebasePushTokenRegistrar: FirebasePushTokenRegistrar,
|
||||||
|
private val unifiedPushRegistrar: UnifiedPushRegistrar,
|
||||||
|
private val pushTokenStore: PushTokenRegistrarPreferences,
|
||||||
|
) : PushTokenRegistrar {
|
||||||
|
|
||||||
|
private var selection: Registrar? = null
|
||||||
|
|
||||||
|
fun options(): List<Registrar> {
|
||||||
|
return listOf(NONE, FIREBASE_OPTION) + UnifiedPush.getDistributors(context).map { Registrar(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun currentSelection() = selection ?: (pushTokenStore.currentSelection()?.let { Registrar(it) } ?: FIREBASE_OPTION).also { selection = it }
|
||||||
|
|
||||||
|
suspend fun makeSelection(option: Registrar) {
|
||||||
|
selection = option
|
||||||
|
pushTokenStore.store(option.id)
|
||||||
|
when (option) {
|
||||||
|
NONE -> {
|
||||||
|
firebasePushTokenRegistrar.unregister()
|
||||||
|
unifiedPushRegistrar.unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
FIREBASE_OPTION -> {
|
||||||
|
unifiedPushRegistrar.unregister()
|
||||||
|
firebasePushTokenRegistrar.registerCurrentToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
firebasePushTokenRegistrar.unregister()
|
||||||
|
unifiedPushRegistrar.registerSelection(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerCurrentToken() {
|
||||||
|
when (selection) {
|
||||||
|
FIREBASE_OPTION -> firebasePushTokenRegistrar.registerCurrentToken()
|
||||||
|
NONE -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> unifiedPushRegistrar.registerCurrentToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregister() {
|
||||||
|
when (selection) {
|
||||||
|
FIREBASE_OPTION -> firebasePushTokenRegistrar.unregister()
|
||||||
|
NONE -> {
|
||||||
|
runCatching {
|
||||||
|
firebasePushTokenRegistrar.unregister()
|
||||||
|
unifiedPushRegistrar.unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> unifiedPushRegistrar.unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class Registrar(val id: String)
|
|
@ -1,27 +0,0 @@
|
||||||
package app.dapk.st.push
|
|
||||||
|
|
||||||
import app.dapk.st.core.AppLogTag
|
|
||||||
import app.dapk.st.core.extensions.CrashScope
|
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
|
||||||
import app.dapk.st.core.log
|
|
||||||
import app.dapk.st.matrix.push.PushService
|
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
|
||||||
|
|
||||||
class RegisterFirebasePushTokenUseCase(
|
|
||||||
private val pushService: PushService,
|
|
||||||
override val errorTracker: ErrorTracker,
|
|
||||||
) : CrashScope {
|
|
||||||
|
|
||||||
suspend fun registerCurrentToken() {
|
|
||||||
kotlin.runCatching {
|
|
||||||
FirebaseMessaging.getInstance().token().also {
|
|
||||||
pushService.registerPush(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.trackFailure()
|
|
||||||
.onSuccess {
|
|
||||||
log(AppLogTag.PUSH, "registered new push token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package app.dapk.st.push
|
package app.dapk.st.push.firebase
|
||||||
|
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
|
@ -0,0 +1,36 @@
|
||||||
|
package app.dapk.st.push.firebase
|
||||||
|
|
||||||
|
import app.dapk.st.core.AppLogTag
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import app.dapk.st.core.log
|
||||||
|
import app.dapk.st.core.module
|
||||||
|
import app.dapk.st.matrix.common.EventId
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.push.PushModule
|
||||||
|
import app.dapk.st.push.PushTokenPayload
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
|
||||||
|
private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify"
|
||||||
|
|
||||||
|
class FirebasePushService : FirebaseMessagingService() {
|
||||||
|
|
||||||
|
private val handler by unsafeLazy { module<PushModule>().pushHandler() }
|
||||||
|
|
||||||
|
override fun onNewToken(token: String) {
|
||||||
|
log(AppLogTag.PUSH, "FCM onNewToken")
|
||||||
|
handler.onNewToken(
|
||||||
|
PushTokenPayload(
|
||||||
|
token = token,
|
||||||
|
gatewayUrl = SYGNAL_GATEWAY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageReceived(message: RemoteMessage) {
|
||||||
|
log(AppLogTag.PUSH, "FCM onMessage")
|
||||||
|
val eventId = message.data["event_id"]?.let { EventId(it) }
|
||||||
|
val roomId = message.data["room_id"]?.let { RoomId(it) }
|
||||||
|
handler.onMessageReceived(eventId, roomId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package app.dapk.st.push.firebase
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import app.dapk.st.core.AppLogTag
|
||||||
|
import app.dapk.st.core.extensions.CrashScope
|
||||||
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
|
import app.dapk.st.core.log
|
||||||
|
import app.dapk.st.push.PushHandler
|
||||||
|
import app.dapk.st.push.PushTokenPayload
|
||||||
|
import app.dapk.st.push.PushTokenRegistrar
|
||||||
|
import app.dapk.st.push.unifiedpush.UnifiedPushMessageReceiver
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
|
||||||
|
private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify"
|
||||||
|
|
||||||
|
class FirebasePushTokenRegistrar(
|
||||||
|
override val errorTracker: ErrorTracker,
|
||||||
|
private val context: Context,
|
||||||
|
private val pushHandler: PushHandler,
|
||||||
|
) : PushTokenRegistrar, CrashScope {
|
||||||
|
|
||||||
|
override suspend fun registerCurrentToken() {
|
||||||
|
log(AppLogTag.PUSH, "FCM - register current token")
|
||||||
|
context.packageManager.setComponentEnabledSetting(
|
||||||
|
ComponentName(context, FirebasePushService::class.java),
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||||
|
PackageManager.DONT_KILL_APP,
|
||||||
|
)
|
||||||
|
|
||||||
|
kotlin.runCatching {
|
||||||
|
FirebaseMessaging.getInstance().token().also {
|
||||||
|
pushHandler.onNewToken(
|
||||||
|
PushTokenPayload(
|
||||||
|
token = it,
|
||||||
|
gatewayUrl = SYGNAL_GATEWAY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.trackFailure()
|
||||||
|
.onSuccess {
|
||||||
|
log(AppLogTag.PUSH, "registered new push token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregister() {
|
||||||
|
log(AppLogTag.PUSH, "FCM - unregister")
|
||||||
|
FirebaseMessaging.getInstance().deleteToken()
|
||||||
|
context.stopService(Intent(context, FirebasePushService::class.java))
|
||||||
|
|
||||||
|
context.packageManager.setComponentEnabledSetting(
|
||||||
|
ComponentName(context, FirebasePushService::class.java),
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||||
|
PackageManager.DONT_KILL_APP,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package app.dapk.st.push.unifiedpush
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.dapk.st.core.AppLogTag
|
||||||
|
import app.dapk.st.core.log
|
||||||
|
import app.dapk.st.core.module
|
||||||
|
import app.dapk.st.matrix.common.EventId
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.push.PushModule
|
||||||
|
import app.dapk.st.push.PushTokenPayload
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.unifiedpush.android.connector.MessagingReceiver
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
private const val FALLBACK_UNIFIED_PUSH_GATEWAY = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
|
||||||
|
|
||||||
|
class UnifiedPushMessageReceiver : MessagingReceiver() {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
|
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
||||||
|
log(AppLogTag.PUSH, "UnifiedPush onMessage, $message")
|
||||||
|
val module = context.module<PushModule>()
|
||||||
|
val handler = module.pushHandler()
|
||||||
|
scope.launch {
|
||||||
|
withContext(module.dispatcher().io) {
|
||||||
|
val payload = json.decodeFromString(UnifiedPushMessagePayload.serializer(), String(message))
|
||||||
|
handler.onMessageReceived(payload.notification.eventId?.let { EventId(it) }, payload.notification.roomId?.let { RoomId(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
||||||
|
log(AppLogTag.PUSH, "UnifiedPush onNewEndpoint $endpoint")
|
||||||
|
val module = context.module<PushModule>()
|
||||||
|
val handler = module.pushHandler()
|
||||||
|
scope.launch {
|
||||||
|
withContext(module.dispatcher().io) {
|
||||||
|
val matrixEndpoint = URL(endpoint).let { URL("${it.protocol}://${it.host}/_matrix/push/v1/notify") }
|
||||||
|
val content = runCatching { matrixEndpoint.openStream().use { String(it.readBytes()) } }.getOrNull() ?: ""
|
||||||
|
val gatewayUrl = when {
|
||||||
|
content.contains("\"gateway\":\"matrix\"") -> matrixEndpoint.toString()
|
||||||
|
else -> FALLBACK_UNIFIED_PUSH_GATEWAY
|
||||||
|
}
|
||||||
|
handler.onNewToken(PushTokenPayload(token = endpoint, gatewayUrl = gatewayUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRegistrationFailed(context: Context, instance: String) {
|
||||||
|
log(AppLogTag.PUSH, "UnifiedPush onRegistrationFailed")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnregistered(context: Context, instance: String) {
|
||||||
|
log(AppLogTag.PUSH, "UnifiedPush onUnregistered")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class UnifiedPushMessagePayload(
|
||||||
|
@SerialName("notification") val notification: Notification,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Notification(
|
||||||
|
@SerialName("event_id") val eventId: String?,
|
||||||
|
@SerialName("room_id") val roomId: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package app.dapk.st.push.unifiedpush
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import app.dapk.st.core.AppLogTag
|
||||||
|
import app.dapk.st.core.log
|
||||||
|
import app.dapk.st.push.PushTokenRegistrar
|
||||||
|
import app.dapk.st.push.Registrar
|
||||||
|
import org.unifiedpush.android.connector.UnifiedPush
|
||||||
|
|
||||||
|
class UnifiedPushRegistrar(
|
||||||
|
private val context: Context,
|
||||||
|
) : PushTokenRegistrar {
|
||||||
|
|
||||||
|
fun registerSelection(registrar: Registrar) {
|
||||||
|
log(AppLogTag.PUSH, "UnifiedPush - register: $registrar")
|
||||||
|
UnifiedPush.saveDistributor(context, registrar.id)
|
||||||
|
registerApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerCurrentToken() {
|
||||||
|
log(AppLogTag.PUSH, "UnifiedPush - register current token")
|
||||||
|
if (UnifiedPush.getDistributor(context).isNotEmpty()) {
|
||||||
|
registerApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerApp() {
|
||||||
|
context.packageManager.setComponentEnabledSetting(
|
||||||
|
ComponentName(context, UnifiedPushMessageReceiver::class.java),
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||||
|
PackageManager.DONT_KILL_APP,
|
||||||
|
)
|
||||||
|
UnifiedPush.registerApp(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregister() {
|
||||||
|
UnifiedPush.unregisterApp(context)
|
||||||
|
context.packageManager.setComponentEnabledSetting(
|
||||||
|
ComponentName(context, UnifiedPushMessageReceiver::class.java),
|
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||||
|
PackageManager.DONT_KILL_APP,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ if (localProperties.exists()) {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def androidVer = androidSdkVersion
|
def androidVer = androidSdkVersion
|
||||||
|
api files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
|
||||||
|
|
||||||
kotlinFixtures(it)
|
kotlinFixtures(it)
|
||||||
testFixturesImplementation testFixtures(project(":core"))
|
testFixturesImplementation testFixtures(project(":core"))
|
||||||
testFixturesImplementation files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
|
testFixturesImplementation files("${properties.getProperty("sdk.dir")}/platforms/android-${androidVer}/android.jar")
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeContext {
|
||||||
|
val instance = mockk<Context>()
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
|
||||||
|
class FakeInboxStyle {
|
||||||
|
private val _summary = slot<String>()
|
||||||
|
|
||||||
|
val instance = mockk<Notification.InboxStyle>()
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
val summary: String
|
||||||
|
get() = _summary.captured
|
||||||
|
|
||||||
|
fun captureInteractions() {
|
||||||
|
every { instance.addLine(capture(lines)) } returns instance
|
||||||
|
every { instance.setSummaryText(capture(_summary)) } returns instance
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.Person
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeMessagingStyle {
|
||||||
|
var user: Person? = null
|
||||||
|
val instance = mockk<Notification.MessagingStyle>()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aFakeMessagingStyle() = FakeMessagingStyle().instance
|
|
@ -0,0 +1,12 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeNotification {
|
||||||
|
|
||||||
|
val instance = mockk<Notification>()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aFakeNotification() = FakeNotification().instance
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeNotificationBuilder {
|
||||||
|
val instance = mockk<Notification.Builder>(relaxed = true)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
|
||||||
|
class FakeNotificationManager {
|
||||||
|
|
||||||
|
val instance = mockk<NotificationManager>()
|
||||||
|
|
||||||
|
fun verifyCancelled(tag: String, id: Int) {
|
||||||
|
verify { instance.cancel(tag, id) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fake
|
||||||
|
|
||||||
|
import android.app.Person
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakePersonBuilder {
|
||||||
|
val instance = mockk<Person.Builder>(relaxed = true)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
@file:JvmName("SnapshotStateKt")
|
@file:JvmName("SnapshotStateKt")
|
||||||
|
@file:Suppress("UNUSED")
|
||||||
package androidx.compose.runtime
|
package androidx.compose.runtime
|
||||||
|
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
|
@ -9,7 +9,7 @@ dependencies {
|
||||||
|
|
||||||
kotlinFixtures(it)
|
kotlinFixtures(it)
|
||||||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
testFixturesImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
|
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesTest
|
||||||
testFixturesImplementation testFixtures(project(":core"))
|
testFixturesImplementation testFixtures(project(":core"))
|
||||||
testFixturesCompileOnly project(":domains:android:viewmodel-stub")
|
testFixturesCompileOnly project(":domains:android:viewmodel-stub")
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@ import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import test.ExpectTest
|
import test.ExpectTest
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
class ViewModelTest {
|
class ViewModelTest {
|
||||||
|
|
||||||
var instance: TestMutableState<Any>? = null
|
var instance: TestMutableState<Any>? = null
|
||||||
|
|
|
@ -8,15 +8,15 @@ interface TaskRunner {
|
||||||
suspend fun run(tasks: List<RunnableWorkTask>): List<TaskResult>
|
suspend fun run(tasks: List<RunnableWorkTask>): List<TaskResult>
|
||||||
|
|
||||||
data class RunnableWorkTask(
|
data class RunnableWorkTask(
|
||||||
val source: JobWorkItem,
|
val source: JobWorkItem?,
|
||||||
val task: WorkTask
|
val task: WorkTask
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface TaskResult {
|
sealed interface TaskResult {
|
||||||
val source: JobWorkItem
|
val source: JobWorkItem?
|
||||||
|
|
||||||
data class Success(override val source: JobWorkItem) : TaskResult
|
data class Success(override val source: JobWorkItem?) : TaskResult
|
||||||
data class Failure(override val source: JobWorkItem, val canRetry: Boolean) : TaskResult
|
data class Failure(override val source: JobWorkItem?, val canRetry: Boolean) : TaskResult
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ package app.dapk.st.work
|
||||||
import android.app.job.JobParameters
|
import android.app.job.JobParameters
|
||||||
import android.app.job.JobService
|
import android.app.job.JobService
|
||||||
import android.app.job.JobWorkItem
|
import android.app.job.JobWorkItem
|
||||||
|
import android.os.Build
|
||||||
import app.dapk.st.core.extensions.Scope
|
import app.dapk.st.core.extensions.Scope
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.core.module
|
import app.dapk.st.core.module
|
||||||
|
@ -24,11 +25,15 @@ class WorkAndroidService : JobService() {
|
||||||
when (it) {
|
when (it) {
|
||||||
is TaskRunner.TaskResult.Failure -> {
|
is TaskRunner.TaskResult.Failure -> {
|
||||||
if (!it.canRetry) {
|
if (!it.canRetry) {
|
||||||
params.completeWork(it.source)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
params.completeWork(it.source!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TaskRunner.TaskResult.Success -> {
|
is TaskRunner.TaskResult.Success -> {
|
||||||
params.completeWork(it.source)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
params.completeWork(it.source!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +45,7 @@ class WorkAndroidService : JobService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JobParameters.collectAllTasks(): List<RunnableWorkTask> {
|
private fun JobParameters.collectAllTasks(): List<RunnableWorkTask> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
var work: JobWorkItem?
|
var work: JobWorkItem?
|
||||||
val tasks = mutableListOf<RunnableWorkTask>()
|
val tasks = mutableListOf<RunnableWorkTask>()
|
||||||
do {
|
do {
|
||||||
|
@ -58,6 +64,18 @@ class WorkAndroidService : JobService() {
|
||||||
}
|
}
|
||||||
} while (work != null)
|
} while (work != null)
|
||||||
return tasks
|
return tasks
|
||||||
|
} else {
|
||||||
|
return listOf(
|
||||||
|
RunnableWorkTask(
|
||||||
|
source = null,
|
||||||
|
task = WorkTask(
|
||||||
|
jobId = this.jobId,
|
||||||
|
type = this.extras.getString("task-type")!!,
|
||||||
|
jsonPayload = this.extras.getString("task-payload")!!,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopJob(params: JobParameters): Boolean {
|
override fun onStopJob(params: JobParameters): Boolean {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.app.job.JobWorkItem
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
internal class WorkSchedulingJobScheduler(
|
internal class WorkSchedulingJobScheduler(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -23,12 +24,17 @@ internal class WorkSchedulingJobScheduler(
|
||||||
.setRequiresDeviceIdle(false)
|
.setRequiresDeviceIdle(false)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val item = JobWorkItem(
|
val item = JobWorkItem(
|
||||||
Intent()
|
Intent()
|
||||||
.putExtra("task-type", task.type)
|
.putExtra("task-type", task.type)
|
||||||
.putExtra("task-payload", task.jsonPayload)
|
.putExtra("task-payload", task.jsonPayload)
|
||||||
)
|
)
|
||||||
|
|
||||||
jobScheduler.enqueue(job, item)
|
jobScheduler.enqueue(job, item)
|
||||||
|
} else {
|
||||||
|
job.extras.putString("task-type", task.type)
|
||||||
|
job.extras.putString("task-payload", task.jsonPayload)
|
||||||
|
jobScheduler.schedule(job)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,5 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly 'org.json:json:20211205'
|
compileOnly 'org.json:json:20220320'
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package app.dapk.st.olm
|
package app.dapk.st.olm
|
||||||
|
|
||||||
|
import app.dapk.st.core.Base64
|
||||||
import app.dapk.st.domain.OlmPersistence
|
import app.dapk.st.domain.OlmPersistence
|
||||||
import app.dapk.st.domain.SerializedObject
|
import app.dapk.st.domain.SerializedObject
|
||||||
import app.dapk.st.matrix.common.Curve25519
|
import app.dapk.st.matrix.common.Curve25519
|
||||||
|
@ -10,10 +11,10 @@ import org.matrix.olm.OlmInboundGroupSession
|
||||||
import org.matrix.olm.OlmOutboundGroupSession
|
import org.matrix.olm.OlmOutboundGroupSession
|
||||||
import org.matrix.olm.OlmSession
|
import org.matrix.olm.OlmSession
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class OlmPersistenceWrapper(
|
class OlmPersistenceWrapper(
|
||||||
private val olmPersistence: OlmPersistence,
|
private val olmPersistence: OlmPersistence,
|
||||||
|
private val base64: Base64,
|
||||||
) : OlmStore {
|
) : OlmStore {
|
||||||
|
|
||||||
override suspend fun read(): OlmAccount? {
|
override suspend fun read(): OlmAccount? {
|
||||||
|
@ -49,21 +50,21 @@ class OlmPersistenceWrapper(
|
||||||
override suspend fun readInbound(sessionId: SessionId): OlmInboundGroupSession? {
|
override suspend fun readInbound(sessionId: SessionId): OlmInboundGroupSession? {
|
||||||
return olmPersistence.readInbound(sessionId)?.value?.deserialize()
|
return olmPersistence.readInbound(sessionId)?.value?.deserialize()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T : Serializable> T.serialize(): String {
|
private fun <T : Serializable> T.serialize(): String {
|
||||||
val baos = ByteArrayOutputStream()
|
val baos = ByteArrayOutputStream()
|
||||||
ObjectOutputStream(baos).use {
|
ObjectOutputStream(baos).use {
|
||||||
it.writeObject(this)
|
it.writeObject(this)
|
||||||
}
|
}
|
||||||
return Base64.getEncoder().encode(baos.toByteArray()).toString(Charsets.UTF_8)
|
return base64.encode(baos.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun <T : Serializable> String.deserialize(): T {
|
private fun <T : Serializable> String.deserialize(): T {
|
||||||
val decoded = Base64.getDecoder().decode(this)
|
val decoded = base64.decode(this)
|
||||||
val baos = ByteArrayInputStream(decoded)
|
val baos = ByteArrayInputStream(decoded)
|
||||||
return ObjectInputStream(baos).use {
|
return ObjectInputStream(baos).use {
|
||||||
it.readObject() as T
|
it.readObject() as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -254,7 +254,7 @@ class OlmWrapper(
|
||||||
|
|
||||||
return readSession.firstNotNullOfOrNull { (_, session) ->
|
return readSession.firstNotNullOfOrNull { (_, session) ->
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
when (type.toInt()) {
|
when (type) {
|
||||||
OlmMessage.MESSAGE_TYPE_PRE_KEY -> {
|
OlmMessage.MESSAGE_TYPE_PRE_KEY -> {
|
||||||
if (session.matchesInboundSession(body.value)) {
|
if (session.matchesInboundSession(body.value)) {
|
||||||
logger.matrixLog(CRYPTO, "matched inbound session, attempting decrypt")
|
logger.matrixLog(CRYPTO, "matched inbound session, attempting decrypt")
|
||||||
|
@ -270,6 +270,8 @@ class OlmWrapper(
|
||||||
session.decryptMessage(olmMessage)?.let { JsonString(it) }?.also {
|
session.decryptMessage(olmMessage)?.let { JsonString(it) }?.also {
|
||||||
logger.crypto("alt flow identity: $senderKey : ${session.sessionIdentifier()}")
|
logger.crypto("alt flow identity: $senderKey : ${session.sessionIdentifier()}")
|
||||||
olmStore.persistSession(senderKey, SessionId(session.sessionIdentifier()), session)
|
olmStore.persistSession(senderKey, SessionId(session.sessionIdentifier()), session)
|
||||||
|
}.also {
|
||||||
|
session.releaseSession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -287,6 +289,8 @@ class OlmWrapper(
|
||||||
}.ifNull {
|
}.ifNull {
|
||||||
logger.matrixLog(CRYPTO, "failed to decrypt olm session")
|
logger.matrixLog(CRYPTO, "failed to decrypt olm session")
|
||||||
DecryptionResult.Failed(errors.joinToString { it.message ?: "N/A" })
|
DecryptionResult.Failed(errors.joinToString { it.message ?: "N/A" })
|
||||||
|
}.also {
|
||||||
|
readSession.forEach { it.second.releaseSession() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,7 +314,9 @@ class OlmWrapper(
|
||||||
errorTracker.track(it)
|
errorTracker.track(it)
|
||||||
DecryptionResult.Failed(it.message ?: "Unknown")
|
DecryptionResult.Failed(it.message ?: "Unknown")
|
||||||
}
|
}
|
||||||
)
|
).also {
|
||||||
|
megolmSession.releaseSession()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package app.dapk.st.domain
|
||||||
|
|
||||||
|
class ApplicationPreferences(
|
||||||
|
private val preferences: Preferences,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun readVersion(): ApplicationVersion? {
|
||||||
|
return preferences.readString("version")?.let { ApplicationVersion(it.toInt()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setVersion(version: ApplicationVersion) {
|
||||||
|
return preferences.store("version", version.value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class ApplicationVersion(val value: Int)
|
||||||
|
|
|
@ -35,4 +35,12 @@ class MemberPersistence(
|
||||||
.map { Json.decodeFromString(RoomMember.serializer(), it) }
|
.map { Json.decodeFromString(RoomMember.serializer(), it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun query(roomId: RoomId, limit: Int): List<RoomMember> {
|
||||||
|
return coroutineDispatchers.withIoContext {
|
||||||
|
database.roomMemberQueries.selectMembersByRoom(roomId.value, limit.toLong())
|
||||||
|
.executeAsList()
|
||||||
|
.map { Json.decodeFromString(RoomMember.serializer(), it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -7,6 +7,7 @@ import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.domain.eventlog.EventLogPersistence
|
import app.dapk.st.domain.eventlog.EventLogPersistence
|
||||||
import app.dapk.st.domain.localecho.LocalEchoPersistence
|
import app.dapk.st.domain.localecho.LocalEchoPersistence
|
||||||
import app.dapk.st.domain.profile.ProfilePersistence
|
import app.dapk.st.domain.profile.ProfilePersistence
|
||||||
|
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
||||||
import app.dapk.st.domain.sync.OverviewPersistence
|
import app.dapk.st.domain.sync.OverviewPersistence
|
||||||
import app.dapk.st.domain.sync.RoomPersistence
|
import app.dapk.st.domain.sync.RoomPersistence
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
|
@ -34,6 +35,10 @@ class StoreModule(
|
||||||
fun filterStore(): FilterStore = FilterPreferences(preferences)
|
fun filterStore(): FilterStore = FilterPreferences(preferences)
|
||||||
val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) }
|
val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) }
|
||||||
|
|
||||||
|
fun pushStore() = PushTokenRegistrarPreferences(preferences)
|
||||||
|
|
||||||
|
fun applicationStore() = ApplicationPreferences(preferences)
|
||||||
|
|
||||||
fun olmStore() = OlmPersistence(database, credentialsStore())
|
fun olmStore() = OlmPersistence(database, credentialsStore())
|
||||||
fun knownDevicesStore() = DevicePersistence(database, KnownDevicesCache(), coroutineDispatchers)
|
fun knownDevicesStore() = DevicePersistence(database, KnownDevicesCache(), coroutineDispatchers)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package app.dapk.st.domain
|
package app.dapk.st.domain
|
||||||
|
|
||||||
|
import app.dapk.st.core.AppLogTag
|
||||||
|
import app.dapk.st.core.log
|
||||||
import app.dapk.st.matrix.common.SyncToken
|
import app.dapk.st.matrix.common.SyncToken
|
||||||
import app.dapk.st.matrix.sync.SyncStore
|
import app.dapk.st.matrix.sync.SyncStore
|
||||||
import app.dapk.st.matrix.sync.SyncStore.SyncKey
|
import app.dapk.st.matrix.sync.SyncStore.SyncKey
|
||||||
|
|
|
@ -33,11 +33,13 @@ class LocalEchoPersistence(
|
||||||
inMemoryEchos.value = echos.groupBy {
|
inMemoryEchos.value = echos.groupBy {
|
||||||
when (val message = it.message) {
|
when (val message = it.message) {
|
||||||
is MessageService.Message.TextMessage -> message.roomId
|
is MessageService.Message.TextMessage -> message.roomId
|
||||||
|
is MessageService.Message.ImageMessage -> message.roomId
|
||||||
}
|
}
|
||||||
}.mapValues {
|
}.mapValues {
|
||||||
it.value.associateBy {
|
it.value.associateBy {
|
||||||
when (val message = it.message) {
|
when (val message = it.message) {
|
||||||
is MessageService.Message.TextMessage -> message.localId
|
is MessageService.Message.TextMessage -> message.localId
|
||||||
|
is MessageService.Message.ImageMessage -> message.localId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +58,7 @@ class LocalEchoPersistence(
|
||||||
database.transaction {
|
database.transaction {
|
||||||
when (message) {
|
when (message) {
|
||||||
is MessageService.Message.TextMessage -> database.localEchoQueries.delete(message.localId)
|
is MessageService.Message.TextMessage -> database.localEchoQueries.delete(message.localId)
|
||||||
|
is MessageService.Message.ImageMessage -> database.localEchoQueries.delete(message.localId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
|
@ -84,6 +87,14 @@ class LocalEchoPersistence(
|
||||||
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
|
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is MessageService.Message.ImageMessage -> database.localEchoQueries.insert(
|
||||||
|
DbLocalEcho(
|
||||||
|
message.localId,
|
||||||
|
message.roomId.value,
|
||||||
|
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package app.dapk.st.domain.push
|
||||||
|
|
||||||
|
import app.dapk.st.domain.Preferences
|
||||||
|
|
||||||
|
private const val SELECTION_KEY = "push_token_selection"
|
||||||
|
|
||||||
|
class PushTokenRegistrarPreferences(
|
||||||
|
private val preferences: Preferences,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun currentSelection() = preferences.readString(SELECTION_KEY)
|
||||||
|
|
||||||
|
suspend fun store(registrar: String) {
|
||||||
|
preferences.store(SELECTION_KEY, registrar)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,8 @@ import app.dapk.st.matrix.sync.RoomInvite
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
private val json = Json
|
private val json = Json
|
||||||
|
@ -31,11 +29,22 @@ internal class OverviewPersistence(
|
||||||
.map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
|
.map { it.map { json.decodeFromString(RoomOverview.serializer(), it.blob) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun removeRooms(roomsToRemove: List<RoomId>) {
|
||||||
|
dispatchers.withIoContext {
|
||||||
|
database.transaction {
|
||||||
|
roomsToRemove.forEach {
|
||||||
|
database.inviteStateQueries.remove(it.value)
|
||||||
|
database.overviewStateQueries.remove(it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun persistInvites(invites: List<RoomInvite>) {
|
override suspend fun persistInvites(invites: List<RoomInvite>) {
|
||||||
dispatchers.withIoContext {
|
dispatchers.withIoContext {
|
||||||
database.inviteStateQueries.transaction {
|
database.inviteStateQueries.transaction {
|
||||||
invites.forEach {
|
invites.forEach {
|
||||||
database.inviteStateQueries.insert(it.roomId.value)
|
database.inviteStateQueries.insert(it.roomId.value, json.encodeToString(RoomInvite.serializer(), it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +54,15 @@ internal class OverviewPersistence(
|
||||||
return database.inviteStateQueries.selectAll()
|
return database.inviteStateQueries.selectAll()
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToList()
|
.mapToList()
|
||||||
.map { it.map { RoomInvite(RoomId(it)) } }
|
.map { it.map { json.decodeFromString(RoomInvite.serializer(), it.blob) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeInvites(invites: List<RoomId>) {
|
||||||
|
dispatchers.withIoContext {
|
||||||
|
database.inviteStateQueries.transaction {
|
||||||
|
invites.forEach { database.inviteStateQueries.remove(it.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun persist(overviewState: OverviewState) {
|
override suspend fun persist(overviewState: OverviewState) {
|
||||||
|
@ -59,7 +76,7 @@ internal class OverviewPersistence(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun retrieve(): OverviewState {
|
override suspend fun retrieve(): OverviewState {
|
||||||
return withContext(Dispatchers.IO) {
|
return dispatchers.withIoContext {
|
||||||
val overviews = database.overviewStateQueries.selectAll().executeAsList()
|
val overviews = database.overviewStateQueries.selectAll().executeAsList()
|
||||||
overviews.map { json.decodeFromString(RoomOverview.serializer(), it.blob) }
|
overviews.map { json.decodeFromString(RoomOverview.serializer(), it.blob) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,13 @@ internal class RoomPersistence(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(rooms: List<RoomId>) {
|
||||||
|
coroutineDispatchers
|
||||||
|
database.roomEventQueries.transaction {
|
||||||
|
rooms.forEach { database.roomEventQueries.remove(it.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun latest(roomId: RoomId): Flow<RoomState> {
|
override fun latest(roomId: RoomId): Flow<RoomState> {
|
||||||
val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map {
|
val overviewFlow = database.overviewStateQueries.selectRoom(roomId.value).asFlow().mapToOneNotNull().map {
|
||||||
json.decodeFromString(RoomOverview.serializer(), it)
|
json.decodeFromString(RoomOverview.serializer(), it)
|
||||||
|
@ -75,7 +82,7 @@ internal class RoomPersistence(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> {
|
override fun observeUnread(): Flow<Map<RoomOverview, List<RoomEvent>>> {
|
||||||
return database.roomEventQueries.selectAllUnread()
|
return database.roomEventQueries.selectAllUnread()
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToList()
|
.mapToList()
|
||||||
|
@ -91,7 +98,7 @@ internal class RoomPersistence(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun observeUnreadCountById(): Flow<Map<RoomId, Int>> {
|
override fun observeUnreadCountById(): Flow<Map<RoomId, Int>> {
|
||||||
return database.roomEventQueries.selectAllUnread()
|
return database.roomEventQueries.selectAllUnread()
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToList()
|
.mapToList()
|
||||||
|
@ -107,7 +114,7 @@ internal class RoomPersistence(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun observeEvent(eventId: EventId): Flow<EventId> {
|
override fun observeEvent(eventId: EventId): Flow<EventId> {
|
||||||
return database.roomEventQueries.selectEvent(event_id = eventId.value)
|
return database.roomEventQueries.selectEvent(event_id = eventId.value)
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.mapToOneNotNull()
|
.mapToOneNotNull()
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
CREATE TABLE dbInviteState (
|
CREATE TABLE dbInviteState (
|
||||||
room_id TEXT NOT NULL,
|
room_id TEXT NOT NULL,
|
||||||
|
blob TEXT NOT NULL,
|
||||||
PRIMARY KEY (room_id)
|
PRIMARY KEY (room_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
selectAll:
|
selectAll:
|
||||||
SELECT room_id
|
SELECT room_id, blob
|
||||||
FROM dbInviteState;
|
FROM dbInviteState;
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
INSERT OR REPLACE INTO dbInviteState(room_id)
|
INSERT OR REPLACE INTO dbInviteState(room_id, blob)
|
||||||
VALUES (?);
|
VALUES (?, ?);
|
||||||
|
|
||||||
|
remove:
|
||||||
|
DELETE FROM dbInviteState
|
||||||
|
WHERE room_id = ?;
|
|
@ -19,3 +19,7 @@ WHERE room_id = ?;
|
||||||
insert:
|
insert:
|
||||||
INSERT OR REPLACE INTO dbOverviewState(room_id, latest_activity_timestamp_utc, read_marker, blob)
|
INSERT OR REPLACE INTO dbOverviewState(room_id, latest_activity_timestamp_utc, read_marker, blob)
|
||||||
VALUES (?, ?, ?, ?);
|
VALUES (?, ?, ?, ?);
|
||||||
|
|
||||||
|
remove:
|
||||||
|
DELETE FROM dbOverviewState
|
||||||
|
WHERE room_id = ?;
|
|
@ -30,4 +30,10 @@ WHERE event_id = ?;
|
||||||
selectAllUnread:
|
selectAllUnread:
|
||||||
SELECT dbRoomEvent.blob, dbRoomEvent.room_id
|
SELECT dbRoomEvent.blob, dbRoomEvent.room_id
|
||||||
FROM dbUnreadEvent
|
FROM dbUnreadEvent
|
||||||
INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id;
|
INNER JOIN dbRoomEvent ON dbUnreadEvent.event_id = dbRoomEvent.event_id
|
||||||
|
ORDER BY dbRoomEvent.timestamp_utc DESC
|
||||||
|
LIMIT 100;
|
||||||
|
|
||||||
|
remove:
|
||||||
|
DELETE FROM dbRoomEvent
|
||||||
|
WHERE room_id = ?;
|
|
@ -10,6 +10,13 @@ SELECT blob
|
||||||
FROM dbRoomMember
|
FROM dbRoomMember
|
||||||
WHERE room_id = ? AND user_id IN ?;
|
WHERE room_id = ? AND user_id IN ?;
|
||||||
|
|
||||||
|
selectMembersByRoom:
|
||||||
|
SELECT blob
|
||||||
|
FROM dbRoomMember
|
||||||
|
WHERE room_id = ?
|
||||||
|
LIMIT ?;
|
||||||
|
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
INSERT OR REPLACE INTO dbRoomMember(user_id, room_id, blob)
|
INSERT OR REPLACE INTO dbRoomMember(user_id, room_id, blob)
|
||||||
VALUES (?, ?, ?);
|
VALUES (?, ?, ?);
|
|
@ -1,13 +1,23 @@
|
||||||
applyAndroidLibraryModule(project)
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:sync")
|
implementation project(":matrix:services:sync")
|
||||||
implementation project(":matrix:services:message")
|
implementation project(":matrix:services:message")
|
||||||
implementation project(":matrix:services:room")
|
implementation project(":matrix:services:room")
|
||||||
implementation project(":domains:android:core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:viewmodel")
|
implementation project(":domains:android:viewmodel")
|
||||||
implementation project(":features:messenger")
|
implementation project(":features:messenger")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation project(":design-library")
|
implementation project(":design-library")
|
||||||
implementation("io.coil-kt:coil-compose:1.4.0")
|
implementation Dependencies.mavenCentral.coil
|
||||||
|
|
||||||
|
kotlinTest(it)
|
||||||
|
|
||||||
|
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
}
|
}
|
|
@ -10,29 +10,34 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import app.dapk.st.core.LifecycleEffect
|
import app.dapk.st.core.LifecycleEffect
|
||||||
import app.dapk.st.core.StartObserving
|
import app.dapk.st.core.StartObserving
|
||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
|
import app.dapk.st.design.components.CircleishAvatar
|
||||||
|
import app.dapk.st.design.components.GenericEmpty
|
||||||
|
import app.dapk.st.design.components.GenericError
|
||||||
|
import app.dapk.st.design.components.Toolbar
|
||||||
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
|
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
|
||||||
import app.dapk.st.directory.DirectoryScreenState.Content
|
import app.dapk.st.directory.DirectoryScreenState.Content
|
||||||
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
|
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
|
||||||
import app.dapk.st.design.components.CircleishAvatar
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
import app.dapk.st.matrix.sync.RoomOverview
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
|
@ -44,36 +49,56 @@ import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DirectoryScreen(directoryViewModel: DirectoryViewModel) {
|
fun DirectoryScreen(directoryViewModel: DirectoryViewModel) {
|
||||||
val state = directoryViewModel.state
|
val state = directoryViewModel.state
|
||||||
directoryViewModel.ObserveEvents()
|
|
||||||
|
val listState: LazyListState = rememberLazyListState(
|
||||||
|
initialFirstVisibleItemIndex = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
val toolbarHeight = 72.dp
|
||||||
|
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
|
||||||
|
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
directoryViewModel.ObserveEvents(listState, toolbarOffsetHeightPx)
|
||||||
|
|
||||||
LifecycleEffect(
|
LifecycleEffect(
|
||||||
onStart = { directoryViewModel.start() },
|
onStart = { directoryViewModel.start() },
|
||||||
onStop = { directoryViewModel.stop() }
|
onStop = { directoryViewModel.stop() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val delta = available.y
|
||||||
|
val newOffset = toolbarOffsetHeightPx.value + delta
|
||||||
|
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
|
) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is Content -> {
|
|
||||||
Content(state)
|
|
||||||
}
|
|
||||||
EmptyLoading -> CenteredLoading()
|
EmptyLoading -> CenteredLoading()
|
||||||
is Error -> {
|
DirectoryScreenState.Empty -> GenericEmpty()
|
||||||
Box(contentAlignment = Alignment.Center) {
|
is Error -> GenericError {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
// TODO
|
||||||
Text("Something went wrong...")
|
|
||||||
Button(onClick = {}) {
|
|
||||||
Text("Retry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
is Content -> Content(listState, state)
|
||||||
}
|
}
|
||||||
|
Toolbar(title = "Messages", offset = { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DirectoryViewModel.ObserveEvents() {
|
private fun DirectoryViewModel.ObserveEvents(listState: LazyListState, toolbarPosition: MutableState<Float>) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
StartObserving {
|
StartObserving {
|
||||||
this@ObserveEvents.events.launch {
|
this@ObserveEvents.events.launch {
|
||||||
|
@ -81,21 +106,24 @@ private fun DirectoryViewModel.ObserveEvents() {
|
||||||
is OpenDownloadUrl -> {
|
is OpenDownloadUrl -> {
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
|
||||||
}
|
}
|
||||||
|
DirectoryEvent.ScrollToTop -> {
|
||||||
|
toolbarPosition.value = 0f
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val clock = Clock.systemUTC()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Content(state: Content) {
|
private fun Content(listState: LazyListState, state: Content) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigateToRoom = { roomId: RoomId ->
|
val navigateToRoom = { roomId: RoomId ->
|
||||||
context.startActivity(MessengerActivity.newInstance(context, roomId))
|
context.startActivity(MessengerActivity.newInstance(context, roomId))
|
||||||
}
|
}
|
||||||
val clock = Clock.systemUTC()
|
|
||||||
val listState: LazyListState = rememberLazyListState(
|
|
||||||
initialFirstVisibleItemIndex = 0,
|
|
||||||
)
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(key1 = state.overviewState) {
|
LaunchedEffect(key1 = state.overviewState) {
|
||||||
|
@ -103,7 +131,7 @@ private fun Content(state: Content) {
|
||||||
scope.launch { listState.scrollToItem(0) }
|
scope.launch { listState.scrollToItem(0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LazyColumn(Modifier.fillMaxSize(), state = listState) {
|
LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(top = 72.dp)) {
|
||||||
items(
|
items(
|
||||||
items = state.overviewState,
|
items = state.overviewState,
|
||||||
key = { it.overview.roomId.value },
|
key = { it.overview.roomId.value },
|
||||||
|
@ -119,7 +147,11 @@ private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock
|
||||||
val roomName = overview.roomName ?: "Empty room"
|
val roomName = overview.roomName ?: "Empty room"
|
||||||
val hasUnread = room.unreadCount.value > 0
|
val hasUnread = room.unreadCount.value > 0
|
||||||
|
|
||||||
Box(Modifier.height(IntrinsicSize.Min).fillMaxWidth().clickable {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
onClick(overview.roomId)
|
onClick(overview.roomId)
|
||||||
}) {
|
}) {
|
||||||
Row(Modifier.padding(20.dp)) {
|
Row(Modifier.padding(20.dp)) {
|
||||||
|
@ -164,13 +196,24 @@ private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Box(Modifier.align(Alignment.CenterVertically)) {
|
Box(Modifier.align(Alignment.CenterVertically)) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.align(Alignment.Center).background(color = MaterialTheme.colors.primary, shape = CircleShape).size(22.dp),
|
Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.background(color = MaterialTheme.colors.primary, shape = CircleShape)
|
||||||
|
.size(22.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
val unreadTextSize = when (room.unreadCount.value > 99) {
|
||||||
|
true -> 9.sp
|
||||||
|
false -> 10.sp
|
||||||
|
}
|
||||||
|
val unreadLabelContent = when {
|
||||||
|
room.unreadCount.value > 99 -> "99+"
|
||||||
|
else -> room.unreadCount.value.toString()
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
fontSize = 10.sp,
|
fontSize = unreadTextSize,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
text = room.unreadCount.value.toString(),
|
text = unreadLabelContent,
|
||||||
color = MaterialTheme.colors.onPrimary
|
color = MaterialTheme.colors.onPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package app.dapk.st.directory
|
||||||
sealed interface DirectoryScreenState {
|
sealed interface DirectoryScreenState {
|
||||||
|
|
||||||
object EmptyLoading : DirectoryScreenState
|
object EmptyLoading : DirectoryScreenState
|
||||||
|
object Empty : DirectoryScreenState
|
||||||
data class Content(
|
data class Content(
|
||||||
val overviewState: DirectoryState,
|
val overviewState: DirectoryState,
|
||||||
) : DirectoryScreenState
|
) : DirectoryScreenState
|
||||||
|
@ -10,5 +11,6 @@ sealed interface DirectoryScreenState {
|
||||||
|
|
||||||
sealed interface DirectoryEvent {
|
sealed interface DirectoryEvent {
|
||||||
data class OpenDownloadUrl(val url: String) : DirectoryEvent
|
data class OpenDownloadUrl(val url: String) : DirectoryEvent
|
||||||
|
object ScrollToTop : DirectoryEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue