abstracting the firebase push service to the push module

This commit is contained in:
Adam Brown 2022-08-17 19:16:45 +01:00
parent 7fe3cfa240
commit b69f2c69c0
12 changed files with 168 additions and 130 deletions

View File

@ -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.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
@ -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
@ -85,7 +87,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
featureModules.notificationsModule.firebasePushTokenUseCase().unregister()
appModule.coroutineDispatchers.io.cancel()
applicationScope.cancel()
stopService(Intent(this, PushAndroidService::class.java))
stopService(Intent(this, FirebasePushService::class.java))
lazyAppModule.reset()
lazyFeatureModules.reset()

View File

@ -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)
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,
@ -191,14 +190,10 @@ 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,20 @@ internal class MatrixModules(
internal class DomainModules(
private val matrixModules: MatrixModules,
private val errorTracker: ErrorTracker,
private val workModule: WorkModule,
private val storeModule: Lazy<StoreModule>,
) {
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(matrixModules.push, errorTracker, pushHandler)
}
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
}

View File

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

View File

@ -2,6 +2,7 @@ applyAndroidLibraryModule(project)
dependencies {
implementation project(':core')
implementation project(':domains:android:core')
implementation project(':matrix:services:push')
implementation platform('com.google.firebase:firebase-bom:29.0.3')
implementation 'com.google.firebase:firebase-messaging'

View File

@ -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=".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.CustomR">
<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>

View File

@ -0,0 +1,23 @@
package app.dapk.st.push
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.core.module
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class FirebasePushService : FirebaseMessagingService() {
private val handler by unsafeLazy { module<PushModule>().pushHandler() }
override fun onNewToken(token: String) {
handler.onNewToken(token)
}
override fun onMessageReceived(message: RemoteMessage) {
val eventId = message.data["event_id"]?.let { EventId(it) }
val roomId = message.data["room_id"]?.let { RoomId(it) }
handler.onMessageReceived(eventId, roomId)
}
}

View File

@ -0,0 +1,9 @@
package app.dapk.st.push
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId
interface PushHandler {
fun onNewToken(token: String)
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
}

View File

@ -1,16 +1,20 @@
package app.dapk.st.push
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.matrix.push.PushService
class PushModule(
private val pushService: PushService,
private val errorTracker: ErrorTracker,
) {
private val pushHandler: PushHandler,
) : ProvidableModule {
fun registerFirebasePushTokenUseCase() = RegisterFirebasePushTokenUseCase(
pushService,
errorTracker,
)
fun pushHandler() = pushHandler
}

View File

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

View File

@ -0,0 +1,83 @@
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.work.WorkScheduler
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
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(token: String) {
log(AppLogTag.PUSH, "new push token received")
workScheduler.schedule(
WorkScheduler.WorkTask(
type = "push_token",
jobId = 2,
jsonPayload = token
)
)
}
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) }
}

View File

@ -6,33 +6,21 @@ 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 +43,4 @@ class NotificationsModule(
private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
fun workScheduler() = workScheduler
}

View File

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