Merge pull request #82 from ouchadam/feature/unified-push
Feature/unified push
This commit is contained in:
commit
da0b1764b1
|
@ -16,8 +16,9 @@ import app.dapk.st.home.HomeModule
|
|||
import app.dapk.st.login.LoginModule
|
||||
import app.dapk.st.messenger.MessengerModule
|
||||
import app.dapk.st.notifications.NotificationsModule
|
||||
import app.dapk.st.notifications.PushAndroidService
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import app.dapk.st.push.firebase.FirebasePushService
|
||||
import app.dapk.st.push.PushModule
|
||||
import app.dapk.st.settings.SettingsModule
|
||||
import app.dapk.st.share.ShareEntryModule
|
||||
import app.dapk.st.work.TaskRunnerModule
|
||||
|
@ -54,7 +55,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||
|
||||
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
|
||||
applicationScope.launch {
|
||||
notificationsModule.firebasePushTokenUseCase().registerCurrentToken()
|
||||
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
||||
storeModule.localEchoStore.preload()
|
||||
}
|
||||
|
||||
|
@ -73,6 +74,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||
SettingsModule::class -> featureModules.settingsModule
|
||||
ProfileModule::class -> featureModules.profileModule
|
||||
NotificationsModule::class -> featureModules.notificationsModule
|
||||
PushModule::class -> featureModules.pushModule
|
||||
MessengerModule::class -> featureModules.messengerModule
|
||||
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
|
||||
CoreAndroidModule::class -> appModule.coreAndroidModule
|
||||
|
@ -82,10 +84,9 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
|||
}
|
||||
|
||||
override fun reset() {
|
||||
featureModules.notificationsModule.firebasePushTokenUseCase().unregister()
|
||||
featureModules.pushModule.pushTokenRegistrar().unregister()
|
||||
appModule.coroutineDispatchers.io.cancel()
|
||||
applicationScope.cancel()
|
||||
stopService(Intent(this, PushAndroidService::class.java))
|
||||
lazyAppModule.reset()
|
||||
lazyFeatureModules.reset()
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ import app.dapk.st.messenger.MessengerActivity
|
|||
import app.dapk.st.messenger.MessengerModule
|
||||
import app.dapk.st.navigator.IntentFactory
|
||||
import app.dapk.st.navigator.MessageAttachment
|
||||
import app.dapk.st.notifications.MatrixPushHandler
|
||||
import app.dapk.st.notifications.NotificationsModule
|
||||
import app.dapk.st.olm.DeviceKeyFactory
|
||||
import app.dapk.st.olm.OlmPersistenceWrapper
|
||||
|
@ -91,7 +92,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||
private val imageLoaderModule = ImageLoaderModule(context)
|
||||
|
||||
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 {
|
||||
override fun notificationOpenApp(context: Context) = PendingIntent.getActivity(
|
||||
|
@ -125,7 +126,6 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||
matrixModules,
|
||||
domainModules,
|
||||
trackingModule,
|
||||
workModule,
|
||||
coreAndroidModule,
|
||||
imageLoaderModule,
|
||||
context,
|
||||
|
@ -140,7 +140,6 @@ internal class FeatureModules internal constructor(
|
|||
private val matrixModules: MatrixModules,
|
||||
private val domainModules: DomainModules,
|
||||
private val trackingModule: TrackingModule,
|
||||
private val workModule: WorkModule,
|
||||
private val coreAndroidModule: CoreAndroidModule,
|
||||
imageLoaderModule: ImageLoaderModule,
|
||||
context: Context,
|
||||
|
@ -181,6 +180,7 @@ internal class FeatureModules internal constructor(
|
|||
val settingsModule by unsafeLazy {
|
||||
SettingsModule(
|
||||
storeModule.value,
|
||||
pushModule,
|
||||
matrixModules.crypto,
|
||||
matrixModules.sync,
|
||||
context.contentResolver,
|
||||
|
@ -191,14 +191,9 @@ internal class FeatureModules internal constructor(
|
|||
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) }
|
||||
val notificationsModule by unsafeLazy {
|
||||
NotificationsModule(
|
||||
matrixModules.push,
|
||||
matrixModules.sync,
|
||||
storeModule.value.credentialsStore(),
|
||||
domainModules.pushModule.registerFirebasePushTokenUseCase(),
|
||||
imageLoaderModule.iconLoader(),
|
||||
storeModule.value.roomStore(),
|
||||
context,
|
||||
workModule.workScheduler(),
|
||||
intentFactory = coreAndroidModule.intentFactory(),
|
||||
dispatchers = coroutineDispatchers,
|
||||
deviceMeta = DeviceMeta(Build.VERSION.SDK_INT)
|
||||
|
@ -209,6 +204,10 @@ internal class FeatureModules internal constructor(
|
|||
ShareEntryModule(matrixModules.sync, matrixModules.room)
|
||||
}
|
||||
|
||||
val pushModule by unsafeLazy {
|
||||
domainModules.pushModule
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class MatrixModules(
|
||||
|
@ -423,9 +422,28 @@ internal class MatrixModules(
|
|||
internal class DomainModules(
|
||||
private val matrixModules: MatrixModules,
|
||||
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 store = storeModule.value
|
||||
val pushHandler = MatrixPushHandler(
|
||||
workScheduler = workModule.workScheduler(),
|
||||
credentialsStore = store.credentialsStore(),
|
||||
matrixModules.sync,
|
||||
store.roomStore(),
|
||||
)
|
||||
PushModule(
|
||||
errorTracker,
|
||||
pushHandler,
|
||||
context,
|
||||
dispatchers,
|
||||
SharedPreferencesDelegate(context.applicationContext, fileName = "dapk-user-preferences", dispatchers)
|
||||
)
|
||||
}
|
||||
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
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,
|
||||
|
@ -12,7 +14,8 @@ class AppTaskRunner(
|
|||
return when (val type = workTask.task.type) {
|
||||
"push_token" -> {
|
||||
runCatching {
|
||||
pushService.registerPush(workTask.task.jsonPayload)
|
||||
val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload)
|
||||
pushService.registerPush(payload.token, payload.gatewayUrl)
|
||||
}.fold(
|
||||
onSuccess = { TaskRunner.TaskResult.Success(workTask.source) },
|
||||
onFailure = {
|
||||
|
@ -25,9 +28,10 @@ class AppTaskRunner(
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown work type: $type")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,13 @@ ext.Dependencies.with {
|
|||
}
|
||||
}
|
||||
|
||||
repositories.maven {
|
||||
url 'https://jitpack.io'
|
||||
content {
|
||||
includeGroup "com.github.UnifiedPush"
|
||||
}
|
||||
}
|
||||
|
||||
repositories.mavenCentral {
|
||||
content {
|
||||
includeGroupByRegex "org\\.jetbrains.*"
|
||||
|
@ -140,6 +147,12 @@ ext.Dependencies.with {
|
|||
|
||||
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 {
|
||||
|
|
|
@ -2,7 +2,6 @@ package app.dapk.st.core
|
|||
|
||||
import android.content.Context
|
||||
|
||||
inline fun <reified T : ProvidableModule> Context.module() =
|
||||
(this.applicationContext as ModuleProvider).provide(T::class)
|
||||
inline fun <reified T : ProvidableModule> Context.module() = (this.applicationContext as ModuleProvider).provide(T::class)
|
||||
|
||||
fun Context.resetModules() = (this.applicationContext as ModuleProvider).reset()
|
|
@ -1,8 +1,13 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
apply plugin: "org.jetbrains.kotlin.plugin.serialization"
|
||||
|
||||
dependencies {
|
||||
implementation project(':core')
|
||||
implementation project(':domains:android:core')
|
||||
implementation project(':domains:store')
|
||||
implementation project(':matrix:services:push')
|
||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
implementation Dependencies.jitPack.unifiedPush
|
||||
}
|
||||
|
|
|
@ -1,2 +1,24 @@
|
|||
<?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
|
||||
|
||||
import android.content.Context
|
||||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.matrix.push.PushService
|
||||
import app.dapk.st.core.extensions.unsafeLazy
|
||||
import app.dapk.st.domain.Preferences
|
||||
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
||||
import app.dapk.st.push.firebase.FirebasePushTokenRegistrar
|
||||
import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
|
||||
|
||||
class PushModule(
|
||||
private val pushService: PushService,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) {
|
||||
private val pushHandler: PushHandler,
|
||||
private val context: Context,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val preferences: Preferences,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun registerFirebasePushTokenUseCase() = RegisterFirebasePushTokenUseCase(
|
||||
pushService,
|
||||
errorTracker,
|
||||
)
|
||||
private val registrars by unsafeLazy {
|
||||
PushTokenRegistrars(
|
||||
context,
|
||||
FirebasePushTokenRegistrar(
|
||||
errorTracker,
|
||||
context,
|
||||
pushHandler,
|
||||
),
|
||||
UnifiedPushRegistrar(context),
|
||||
PushTokenRegistrarPreferences(preferences)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
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,31 +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 {
|
||||
|
||||
fun unregister() {
|
||||
FirebaseMessaging.getInstance().deleteToken()
|
||||
}
|
||||
|
||||
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 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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,7 @@ import app.dapk.st.core.extensions.unsafeLazy
|
|||
import app.dapk.st.domain.eventlog.EventLogPersistence
|
||||
import app.dapk.st.domain.localecho.LocalEchoPersistence
|
||||
import app.dapk.st.domain.profile.ProfilePersistence
|
||||
import app.dapk.st.domain.push.PushTokenRegistrarPreferences
|
||||
import app.dapk.st.domain.sync.OverviewPersistence
|
||||
import app.dapk.st.domain.sync.RoomPersistence
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
|
@ -34,6 +35,8 @@ class StoreModule(
|
|||
fun filterStore(): FilterStore = FilterPreferences(preferences)
|
||||
val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) }
|
||||
|
||||
fun pushStore() = PushTokenRegistrarPreferences(preferences)
|
||||
|
||||
fun applicationStore() = ApplicationPreferences(preferences)
|
||||
|
||||
fun olmStore() = OlmPersistence(database, credentialsStore())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package app.dapk.st.login
|
|||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.matrix.auth.AuthService
|
||||
import app.dapk.st.matrix.crypto.CryptoService
|
||||
import app.dapk.st.matrix.room.ProfileService
|
||||
import app.dapk.st.push.PushModule
|
||||
|
||||
|
@ -15,6 +14,6 @@ class LoginModule(
|
|||
) : ProvidableModule {
|
||||
|
||||
fun loginViewModel(): LoginViewModel {
|
||||
return LoginViewModel(authService, pushModule.registerFirebasePushTokenUseCase(), profileService, errorTracker)
|
||||
return LoginViewModel(authService, pushModule.pushTokenRegistrar(), profileService, errorTracker)
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import app.dapk.st.login.LoginEvent.LoginComplete
|
|||
import app.dapk.st.login.LoginScreenState.*
|
||||
import app.dapk.st.matrix.auth.AuthService
|
||||
import app.dapk.st.matrix.room.ProfileService
|
||||
import app.dapk.st.push.RegisterFirebasePushTokenUseCase
|
||||
import app.dapk.st.push.PushTokenRegistrar
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
class LoginViewModel(
|
||||
private val authService: AuthService,
|
||||
private val firebasePushTokenUseCase: RegisterFirebasePushTokenUseCase,
|
||||
private val pushTokenRegistrar: PushTokenRegistrar,
|
||||
private val profileService: ProfileService,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) : DapkViewModel<LoginScreenState, LoginEvent>(
|
||||
|
@ -32,7 +32,7 @@ class LoginViewModel(
|
|||
is AuthService.LoginResult.Success -> {
|
||||
runCatching {
|
||||
listOf(
|
||||
async { firebasePushTokenUseCase.registerCurrentToken() },
|
||||
async { pushTokenRegistrar.registerCurrentToken() },
|
||||
async { preloadMe() },
|
||||
).awaitAll()
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
|
||||
implementation platform('com.google.firebase:firebase-bom:29.0.3')
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
|
||||
kotlinTest(it)
|
||||
|
||||
|
|
|
@ -1,14 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.dapk.st.notifications">
|
||||
|
||||
<application>
|
||||
<service
|
||||
android:name=".PushAndroidService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
<manifest package="app.dapk.st.notifications"/>
|
|
@ -0,0 +1,85 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import app.dapk.st.core.AppLogTag
|
||||
import app.dapk.st.core.log
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.push.PushHandler
|
||||
import app.dapk.st.push.PushTokenPayload
|
||||
import app.dapk.st.work.WorkScheduler
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private var previousJob: Job? = null
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class MatrixPushHandler(
|
||||
private val workScheduler: WorkScheduler,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val syncService: SyncService,
|
||||
private val roomStore: RoomStore,
|
||||
) : PushHandler {
|
||||
|
||||
override fun onNewToken(payload: PushTokenPayload) {
|
||||
log(AppLogTag.PUSH, "new push token received")
|
||||
workScheduler.schedule(
|
||||
WorkScheduler.WorkTask(
|
||||
type = "push_token",
|
||||
jobId = 2,
|
||||
jsonPayload = Json.encodeToString(PushTokenPayload.serializer(), payload)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) {
|
||||
log(AppLogTag.PUSH, "push received")
|
||||
previousJob?.cancel()
|
||||
previousJob = GlobalScope.launch {
|
||||
when (credentialsStore.credentials()) {
|
||||
null -> log(AppLogTag.PUSH, "push ignored due to missing api credentials")
|
||||
else -> doSync(roomId, eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doSync(roomId: RoomId?, eventId: EventId?) {
|
||||
when (roomId) {
|
||||
null -> {
|
||||
log(AppLogTag.PUSH, "empty push payload - keeping sync alive until unread changes")
|
||||
waitForUnreadChange(60_000) ?: log(AppLogTag.PUSH, "timed out waiting for sync")
|
||||
}
|
||||
|
||||
else -> {
|
||||
log(AppLogTag.PUSH, "push with eventId payload - keeping sync alive until the event shows up in the sync response")
|
||||
waitForEvent(
|
||||
timeout = 60_000,
|
||||
eventId!!,
|
||||
) ?: log(AppLogTag.PUSH, "timed out waiting for sync")
|
||||
}
|
||||
}
|
||||
log(AppLogTag.PUSH, "push sync finished")
|
||||
}
|
||||
|
||||
private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(syncService.startSyncing().startInstantly(), syncService.observeEvent(eventId)) { _, event -> event }
|
||||
.firstOrNull {
|
||||
it == eventId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForUnreadChange(timeout: Long): String? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(syncService.startSyncing().startInstantly(), roomStore.observeUnread()) { _, unread -> unread }
|
||||
.first()
|
||||
"ignored"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<Unit>.startInstantly() = this.onStart { emit(Unit) }
|
||||
}
|
|
@ -6,33 +6,18 @@ import app.dapk.st.core.CoroutineDispatchers
|
|||
import app.dapk.st.core.DeviceMeta
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.imageloader.IconLoader
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.push.PushService
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.navigator.IntentFactory
|
||||
import app.dapk.st.push.RegisterFirebasePushTokenUseCase
|
||||
import app.dapk.st.work.WorkScheduler
|
||||
|
||||
class NotificationsModule(
|
||||
private val pushService: PushService,
|
||||
private val syncService: SyncService,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val firebasePushTokenUseCase: RegisterFirebasePushTokenUseCase,
|
||||
private val iconLoader: IconLoader,
|
||||
private val roomStore: RoomStore,
|
||||
private val context: Context,
|
||||
private val workScheduler: WorkScheduler,
|
||||
private val intentFactory: IntentFactory,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val deviceMeta: DeviceMeta,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun pushUseCase() = pushService
|
||||
fun syncService() = syncService
|
||||
fun credentialProvider() = credentialsStore
|
||||
fun firebasePushTokenUseCase() = firebasePushTokenUseCase
|
||||
fun roomStore() = roomStore
|
||||
fun notificationsUseCase() = RenderNotificationsUseCase(
|
||||
notificationRenderer = NotificationRenderer(
|
||||
notificationManager(),
|
||||
|
@ -55,5 +40,4 @@ class NotificationsModule(
|
|||
|
||||
private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
fun workScheduler() = workScheduler
|
||||
}
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import android.content.Context
|
||||
import app.dapk.st.core.AppLogTag.PUSH
|
||||
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.work.WorkScheduler
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
import com.google.firebase.messaging.RemoteMessage
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
private var previousJob: Job? = null
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class PushAndroidService : FirebaseMessagingService() {
|
||||
|
||||
private val module by unsafeLazy { module<NotificationsModule>() }
|
||||
private lateinit var context: Context
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = applicationContext
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
log(PUSH, "new push token received")
|
||||
module.workScheduler().schedule(
|
||||
WorkScheduler.WorkTask(
|
||||
type = "push_token",
|
||||
jobId = 2,
|
||||
jsonPayload = token
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
val eventId = message.data["event_id"]?.let { EventId(it) }
|
||||
val roomId = message.data["room_id"]?.let { RoomId(it) }
|
||||
|
||||
log(PUSH, "push received")
|
||||
previousJob?.cancel()
|
||||
previousJob = GlobalScope.launch {
|
||||
when (module.credentialProvider().credentials()) {
|
||||
null -> log(PUSH, "push ignored due to missing api credentials")
|
||||
else -> doSync(roomId, eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doSync(roomId: RoomId?, eventId: EventId?) {
|
||||
when (roomId) {
|
||||
null -> {
|
||||
log(PUSH, "empty push payload - keeping sync alive until unread changes")
|
||||
waitForUnreadChange(60_000) ?: log(PUSH, "timed out waiting for sync")
|
||||
}
|
||||
else -> {
|
||||
log(PUSH, "push with eventId payload - keeping sync alive until the event shows up in the sync response")
|
||||
waitForEvent(
|
||||
timeout = 60_000,
|
||||
eventId!!,
|
||||
) ?: log(PUSH, "timed out waiting for sync")
|
||||
}
|
||||
}
|
||||
log(PUSH, "push sync finished")
|
||||
}
|
||||
|
||||
private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(module.syncService().startSyncing().startInstantly(), module.syncService().observeEvent(eventId)) { _, event -> event }
|
||||
.firstOrNull {
|
||||
it == eventId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForUnreadChange(timeout: Long): String? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(module.syncService().startSyncing().startInstantly(), module.roomStore().observeUnread()) { _, unread -> unread }
|
||||
.first()
|
||||
"ignored"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Flow<Unit>.startInstantly() = this.onStart { emit(Unit) }
|
|
@ -5,6 +5,7 @@ dependencies {
|
|||
implementation project(":matrix:services:crypto")
|
||||
implementation project(":features:navigator")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':domains:android:push')
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":design-library")
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package app.dapk.st.settings
|
||||
|
||||
import app.dapk.st.core.BuildMeta
|
||||
import app.dapk.st.push.PushTokenRegistrars
|
||||
|
||||
internal class SettingsItemFactory(private val buildMeta: BuildMeta) {
|
||||
internal class SettingsItemFactory(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val pushTokenRegistrars: PushTokenRegistrars,
|
||||
) {
|
||||
|
||||
fun root() = listOf(
|
||||
suspend fun root() = listOf(
|
||||
SettingItem.Header("General"),
|
||||
SettingItem.Text(SettingItem.Id.Encryption, "Encryption"),
|
||||
SettingItem.Text(SettingItem.Id.EventLog, "Event log"),
|
||||
SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id),
|
||||
SettingItem.Header("Data"),
|
||||
SettingItem.Text(SettingItem.Id.ClearCache, "Clear cache"),
|
||||
SettingItem.Header("Account"),
|
||||
|
|
|
@ -7,10 +7,12 @@ import app.dapk.st.core.ProvidableModule
|
|||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.matrix.crypto.CryptoService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.push.PushModule
|
||||
import app.dapk.st.settings.eventlogger.EventLoggerViewModel
|
||||
|
||||
class SettingsModule(
|
||||
private val storeModule: StoreModule,
|
||||
private val pushModule: PushModule,
|
||||
private val cryptoService: CryptoService,
|
||||
private val syncService: SyncService,
|
||||
private val contentResolver: ContentResolver,
|
||||
|
@ -24,7 +26,8 @@ class SettingsModule(
|
|||
cryptoService,
|
||||
syncService,
|
||||
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
||||
SettingsItemFactory(buildMeta),
|
||||
SettingsItemFactory(buildMeta, pushModule.pushTokenRegistrars()),
|
||||
pushModule.pushTokenRegistrars(),
|
||||
)
|
||||
|
||||
internal fun eventLogViewModel(): EventLoggerViewModel {
|
||||
|
|
|
@ -68,6 +68,9 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
|||
item(Page.Routes.encryption) {
|
||||
Encryption(viewModel, it)
|
||||
}
|
||||
item(Page.Routes.pushProviders) {
|
||||
PushProviders(viewModel, it)
|
||||
}
|
||||
item(Page.Routes.importRoomKeys) {
|
||||
when (it.importProgress) {
|
||||
null -> {
|
||||
|
@ -132,6 +135,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Lce.Content -> {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
@ -142,6 +146,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Lce.Error -> {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
@ -152,6 +157,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Lce.Loading -> CenteredLoading()
|
||||
}
|
||||
}
|
||||
|
@ -176,6 +182,7 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||
|
||||
SettingsTextRow(item.content, item.subtitle, itemOnClick)
|
||||
}
|
||||
|
||||
is SettingItem.AccessToken -> {
|
||||
Row(
|
||||
Modifier
|
||||
|
@ -193,6 +200,7 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SettingItem.Header -> Header(item.label)
|
||||
}
|
||||
}
|
||||
|
@ -203,6 +211,7 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||
is Lce.Error -> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
is Lce.Loading -> {
|
||||
// TODO
|
||||
}
|
||||
|
@ -216,6 +225,32 @@ private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun PushProviders(viewModel: SettingsViewModel, state: Page.PushProviders) {
|
||||
LaunchedEffect(true) {
|
||||
viewModel.fetchPushProviders()
|
||||
}
|
||||
|
||||
when (val lce = state.options) {
|
||||
null -> {}
|
||||
is Lce.Loading -> CenteredLoading()
|
||||
is Lce.Content -> {
|
||||
LazyColumn {
|
||||
items(lce.value) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(selected = it == state.selection, onClick = { viewModel.selectPushProvider(it) })
|
||||
Text(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Lce.Error -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun SettingsViewModel.ObserveEvents(onSignOut: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
@ -228,10 +263,12 @@ private fun SettingsViewModel.ObserveEvents(onSignOut: () -> Unit) {
|
|||
clipboard.setPrimaryClip(ClipData.newPlainText("dapk token", it.content))
|
||||
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
is SettingsEvent.Toast -> Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
|
||||
OpenEventLog -> {
|
||||
context.startActivity(Intent(context, EventLogActivity::class.java))
|
||||
}
|
||||
|
||||
is OpenUrl -> {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW).apply { data = it.url.toUri() })
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.design.components.Route
|
||||
import app.dapk.st.design.components.SpiderPage
|
||||
import app.dapk.st.push.Registrar
|
||||
|
||||
internal data class SettingsScreenState(
|
||||
val page: SpiderPage<out Page>,
|
||||
|
@ -17,9 +18,15 @@ internal sealed interface Page {
|
|||
val importProgress: Lce<Unit>? = null,
|
||||
) : Page
|
||||
|
||||
data class PushProviders(
|
||||
val selection: Registrar? = null,
|
||||
val options: Lce<List<Registrar>>? = Lce.Loading()
|
||||
) : Page
|
||||
|
||||
object Routes {
|
||||
val root = Route<Root>("Settings")
|
||||
val encryption = Route<Page.Security>("Encryption")
|
||||
val pushProviders = Route<Page.PushProviders>("PushProviders")
|
||||
val importRoomKeys = Route<Page.ImportRoomKey>("ImportRoomKey")
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +49,7 @@ internal sealed interface SettingItem {
|
|||
AccessToken,
|
||||
ClearCache,
|
||||
EventLog,
|
||||
PushProvider,
|
||||
Encryption,
|
||||
PrivacyPolicy,
|
||||
Ignored,
|
||||
|
|
|
@ -8,6 +8,8 @@ import app.dapk.st.design.components.SpiderPage
|
|||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.matrix.crypto.CryptoService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.push.PushTokenRegistrars
|
||||
import app.dapk.st.push.Registrar
|
||||
import app.dapk.st.settings.SettingItem.Id.*
|
||||
import app.dapk.st.settings.SettingsEvent.*
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
|
@ -24,6 +26,7 @@ internal class SettingsViewModel(
|
|||
private val syncService: SyncService,
|
||||
private val uriFilenameResolver: UriFilenameResolver,
|
||||
private val settingsItemFactory: SettingsItemFactory,
|
||||
private val pushTokenRegistrars: PushTokenRegistrars,
|
||||
factory: MutableStateFactory<SettingsScreenState> = defaultStateFactory(),
|
||||
) : DapkViewModel<SettingsScreenState, SettingsEvent>(
|
||||
initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))),
|
||||
|
@ -50,39 +53,72 @@ internal class SettingsViewModel(
|
|||
_events.emit(SignedOut)
|
||||
}
|
||||
}
|
||||
|
||||
AccessToken -> {
|
||||
viewModelScope.launch {
|
||||
require(item is SettingItem.AccessToken)
|
||||
_events.emit(CopyToClipboard("Token copied", item.accessToken))
|
||||
}
|
||||
}
|
||||
|
||||
ClearCache -> {
|
||||
viewModelScope.launch {
|
||||
cacheCleaner.cleanCache(removeCredentials = false)
|
||||
_events.emit(Toast(message = "Cache deleted"))
|
||||
}
|
||||
}
|
||||
|
||||
EventLog -> {
|
||||
viewModelScope.launch {
|
||||
_events.emit(OpenEventLog)
|
||||
}
|
||||
}
|
||||
|
||||
Encryption -> {
|
||||
updateState {
|
||||
copy(page = SpiderPage(Page.Routes.encryption, "Encryption", Page.Routes.root, Page.Security))
|
||||
}
|
||||
}
|
||||
|
||||
PrivacyPolicy -> {
|
||||
viewModelScope.launch {
|
||||
_events.emit(OpenUrl(PRIVACY_POLICY_URL))
|
||||
}
|
||||
}
|
||||
|
||||
PushProvider -> {
|
||||
updateState {
|
||||
copy(page = SpiderPage(Page.Routes.pushProviders, "Push providers", Page.Routes.root, Page.PushProviders()))
|
||||
}
|
||||
}
|
||||
|
||||
Ignored -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchPushProviders() {
|
||||
updatePageState<Page.PushProviders> { copy(options = Lce.Loading()) }
|
||||
viewModelScope.launch {
|
||||
val currentSelection = pushTokenRegistrars.currentSelection()
|
||||
val options = pushTokenRegistrars.options()
|
||||
updatePageState<Page.PushProviders> {
|
||||
copy(
|
||||
selection = currentSelection,
|
||||
options = Lce.Content(options)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectPushProvider(registrar: Registrar) {
|
||||
viewModelScope.launch {
|
||||
pushTokenRegistrars.makeSelection(registrar)
|
||||
fetchPushProviders()
|
||||
}
|
||||
}
|
||||
|
||||
fun importFromFileKeys(file: Uri, passphrase: String) {
|
||||
updatePageState<Page.ImportRoomKey> { copy(importProgress = Lce.Loading()) }
|
||||
viewModelScope.launch {
|
||||
|
|
|
@ -1,25 +1,38 @@
|
|||
package app.dapk.st.settings
|
||||
|
||||
import app.dapk.st.core.BuildMeta
|
||||
import app.dapk.st.push.PushTokenRegistrars
|
||||
import app.dapk.st.push.Registrar
|
||||
import internalfixture.aSettingHeaderItem
|
||||
import internalfixture.aSettingTextItem
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
import test.delegateReturn
|
||||
|
||||
private val A_SELECTION = Registrar("A_SELECTION")
|
||||
|
||||
class SettingsItemFactoryTest {
|
||||
|
||||
private val buildMeta = BuildMeta(versionName = "a-version-name", versionCode = 100)
|
||||
private val fakePushTokenRegistrars = FakePushRegistrars()
|
||||
|
||||
private val settingsItemFactory = SettingsItemFactory(buildMeta)
|
||||
private val settingsItemFactory = SettingsItemFactory(buildMeta, fakePushTokenRegistrars.instance)
|
||||
|
||||
@Test
|
||||
fun `when creating root items, then is expected`() {
|
||||
fun `when creating root items, then is expected`() = runTest {
|
||||
fakePushTokenRegistrars.givenCurrentSelection().returns(A_SELECTION)
|
||||
|
||||
val result = settingsItemFactory.root()
|
||||
|
||||
result shouldBeEqualTo listOf(
|
||||
aSettingHeaderItem("General"),
|
||||
aSettingTextItem(SettingItem.Id.Encryption, "Encryption"),
|
||||
aSettingTextItem(SettingItem.Id.EventLog, "Event log"),
|
||||
aSettingTextItem(SettingItem.Id.PushProvider, "Push provider", A_SELECTION.id),
|
||||
aSettingHeaderItem("Data"),
|
||||
aSettingTextItem(SettingItem.Id.ClearCache, "Clear cache"),
|
||||
aSettingHeaderItem("Account"),
|
||||
|
@ -29,4 +42,12 @@ class SettingsItemFactoryTest {
|
|||
aSettingTextItem(SettingItem.Id.Ignored, "Version", buildMeta.versionName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class FakePushRegistrars {
|
||||
|
||||
val instance = mockk<PushTokenRegistrars>()
|
||||
|
||||
fun givenCurrentSelection() = coEvery { instance.currentSelection() }.delegateReturn()
|
||||
|
||||
}
|
|
@ -34,6 +34,7 @@ internal class SettingsViewModelTest {
|
|||
private val fakeCryptoService = FakeCryptoService()
|
||||
private val fakeSyncService = FakeSyncService()
|
||||
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||
private val fakePushTokenRegistrars = FakePushRegistrars()
|
||||
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
|
||||
|
||||
private val viewModel = SettingsViewModel(
|
||||
|
@ -43,6 +44,7 @@ internal class SettingsViewModelTest {
|
|||
fakeSyncService,
|
||||
fakeUriFilenameResolver.instance,
|
||||
fakeSettingsItemFactory.instance,
|
||||
fakePushTokenRegistrars.instance,
|
||||
runViewModelTest.testMutableStateFactory(),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package internalfake
|
||||
|
||||
import app.dapk.st.settings.SettingsItemFactory
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import test.delegateReturn
|
||||
|
@ -8,5 +9,5 @@ import test.delegateReturn
|
|||
internal class FakeSettingsItemFactory {
|
||||
val instance = mockk<SettingsItemFactory>()
|
||||
|
||||
fun givenRoot() = every { instance.root() }.delegateReturn()
|
||||
fun givenRoot() = coEvery { instance.root() }.delegateReturn()
|
||||
}
|
|
@ -12,7 +12,7 @@ private val SERVICE_KEY = PushService::class
|
|||
|
||||
interface PushService : MatrixService {
|
||||
|
||||
suspend fun registerPush(token: String)
|
||||
suspend fun registerPush(token: String, gatewayUrl: String)
|
||||
|
||||
@Serializable
|
||||
data class PushRequest(
|
||||
|
|
|
@ -13,8 +13,8 @@ class DefaultPushService(
|
|||
|
||||
private val useCase = RegisterPushUseCase(httpClient, credentialsStore, logger)
|
||||
|
||||
override suspend fun registerPush(token: String) {
|
||||
useCase.registerPushToken(token)
|
||||
override suspend fun registerPush(token: String, gatewayUrl: String) {
|
||||
useCase.registerPushToken(token, gatewayUrl)
|
||||
}
|
||||
|
||||
}
|
|
@ -13,7 +13,7 @@ internal class RegisterPushUseCase(
|
|||
private val logger: MatrixLogger,
|
||||
) {
|
||||
|
||||
suspend fun registerPushToken(token: String) {
|
||||
suspend fun registerPushToken(token: String, gatewayUrl: String) {
|
||||
if (credentialsStore.isSignedIn()) {
|
||||
logger.matrixLog("register push token: $token")
|
||||
matrixClient.execute(
|
||||
|
@ -29,7 +29,7 @@ internal class RegisterPushUseCase(
|
|||
append = false,
|
||||
data = PushRequest.Payload(
|
||||
format = "event_id_only",
|
||||
url = "https://sygnal.dapk.app/_matrix/push/v1/notify",
|
||||
url = gatewayUrl,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue