From be1896e53ffcd0ec2a939334f25c3208c4469ee6 Mon Sep 17 00:00:00 2001 From: gavine99 Date: Fri, 28 Mar 2025 16:27:20 +1100 Subject: [PATCH] implement new receivesms worker. modify sms receive interactor to be used by new worker and add more logging. create custom workfactory that supports injection into workers. modify housekeeping worker to use injection. --- data/build.gradle | 1 + .../com/moez/QKSMS/receiver/SmsReceiver.kt | 38 +++++-- .../HousekeepingWorker.kt} | 34 ++++--- .../QKSMS/worker/InjectionWorkerFactory.kt | 57 +++++++++++ .../com/moez/QKSMS/worker/ReceiveSmsWorker.kt | 53 ++++++++++ .../com/moez/QKSMS/interactor/ReceiveSms.kt | 98 ++++++++++--------- presentation/build.gradle | 1 + presentation/src/main/AndroidManifest.xml | 12 +++ .../com/moez/QKSMS/common/QKApplication.kt | 16 ++- .../com/moez/QKSMS/injection/AppModule.kt | 5 + 10 files changed, 246 insertions(+), 69 deletions(-) rename data/src/main/java/com/moez/QKSMS/{manager/HousekeepingWorkManager.kt => worker/HousekeepingWorker.kt} (78%) create mode 100644 data/src/main/java/com/moez/QKSMS/worker/InjectionWorkerFactory.kt create mode 100644 data/src/main/java/com/moez/QKSMS/worker/ReceiveSmsWorker.kt diff --git a/data/build.gradle b/data/build.gradle index 7787cd04..1b333e5c 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -97,6 +97,7 @@ dependencies { // work manager implementation("androidx.work:work-runtime-ktx:$androidx_work_version") + implementation "androidx.work:work-rxjava2:$androidx_work_version" implementation project(":android-smsmms") implementation project(':common') diff --git a/data/src/main/java/com/moez/QKSMS/receiver/SmsReceiver.kt b/data/src/main/java/com/moez/QKSMS/receiver/SmsReceiver.kt index 5f6badf3..123f052f 100644 --- a/data/src/main/java/com/moez/QKSMS/receiver/SmsReceiver.kt +++ b/data/src/main/java/com/moez/QKSMS/receiver/SmsReceiver.kt @@ -22,24 +22,48 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.provider.Telephony.Sms -import dev.octoshrimpy.quik.interactor.ReceiveSms +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import androidx.work.workDataOf import dagger.android.AndroidInjection +import dev.octoshrimpy.quik.repository.MessageRepository +import dev.octoshrimpy.quik.worker.ReceiveSmsWorker +import dev.octoshrimpy.quik.worker.ReceiveSmsWorker.Companion.INPUT_DATA_KEY_MESSAGE_ID +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers import timber.log.Timber import javax.inject.Inject class SmsReceiver : BroadcastReceiver() { - - @Inject lateinit var receiveMessage: ReceiveSms + @Inject lateinit var messageRepo: MessageRepository override fun onReceive(context: Context, intent: Intent) { AndroidInjection.inject(this, context) - Timber.v("onReceive") Sms.Intents.getMessagesFromIntent(intent)?.let { messages -> - val subId = intent.extras?.getInt("subscription", -1) ?: -1 + // reduce list of messages to single message and save in db + val messageId = Single.just(messages) + .observeOn(Schedulers.io()) + .map { + Timber.v("onReceive() new sms") // here so runs on io thread - val pendingResult = goAsync() - receiveMessage.execute(ReceiveSms.Params(subId, messages)) { pendingResult.finish() } + messageRepo.insertReceivedSms( + intent.extras?.getInt("subscription", -1) ?: -1, + messages[0].displayOriginatingAddress, + messages.mapNotNull { it.displayMessageBody }.reduce { body, new -> body + new }, + messages[0].timestampMillis + ).id + } + .blockingGet() + + // start worker with message id as param + WorkManager.getInstance(context).enqueue( + OneTimeWorkRequestBuilder() + .setInputData(workDataOf(INPUT_DATA_KEY_MESSAGE_ID to messageId)) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + ) } } diff --git a/data/src/main/java/com/moez/QKSMS/manager/HousekeepingWorkManager.kt b/data/src/main/java/com/moez/QKSMS/worker/HousekeepingWorker.kt similarity index 78% rename from data/src/main/java/com/moez/QKSMS/manager/HousekeepingWorkManager.kt rename to data/src/main/java/com/moez/QKSMS/worker/HousekeepingWorker.kt index 49c0a8bc..eed1531c 100644 --- a/data/src/main/java/com/moez/QKSMS/manager/HousekeepingWorkManager.kt +++ b/data/src/main/java/com/moez/QKSMS/worker/HousekeepingWorker.kt @@ -16,25 +16,31 @@ * You should have received a copy of the GNU General Public License * along with QUIK. If not, see . */ -package com.moez.QKSMS.manager +package dev.octoshrimpy.quik.worker import android.content.Context import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.moez.QKSMS.manager.MediaRecorderManager import com.moez.QKSMS.manager.MediaRecorderManager.AUDIO_FILE_PREFIX +import dev.octoshrimpy.quik.repository.ScheduledMessageRepository import dev.octoshrimpy.quik.repository.ScheduledMessageRepositoryImpl +import dev.octoshrimpy.quik.worker.ReceiveSmsWorker.Companion.INPUT_DATA_KEY_MESSAGE_ID import java.io.File import java.util.concurrent.TimeUnit +import javax.inject.Inject -class HousekeepingWorkManager(appContext: Context, workerParams: WorkerParameters) - : Worker(appContext, workerParams) { - +class HousekeepingWorker(appContext: Context, workerParams: WorkerParameters) +: Worker(appContext, workerParams) { companion object { - private val WORKER_TAG: String = HousekeepingWorkManager::class.java.simpleName + private val WORKER_TAG: String = HousekeepingWorker::class.java.simpleName fun register(context: Context) { // don't check return value because, well, we can't do much about a failure @@ -42,7 +48,7 @@ class HousekeepingWorkManager(appContext: Context, workerParams: WorkerParameter WORKER_TAG, ExistingPeriodicWorkPolicy.KEEP, PeriodicWorkRequest.Builder( - HousekeepingWorkManager::class.java, + HousekeepingWorker::class.java, 24, TimeUnit.HOURS ) @@ -57,14 +63,16 @@ class HousekeepingWorkManager(appContext: Context, workerParams: WorkerParameter ) .addTag(WORKER_TAG) .build() - ) - } - - fun cancel(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(WORKER_TAG) - } + ) } + fun cancel(context: Context) { + WorkManager.getInstance(context).cancelUniqueWork(WORKER_TAG) + } + } + + @Inject lateinit var scheduledMessageRepository: ScheduledMessageRepository + override fun doWork(): Result { removeOrphanedScheduledMessageAttachmentFiles() @@ -75,7 +83,7 @@ class HousekeepingWorkManager(appContext: Context, workerParams: WorkerParameter private fun removeOrphanedScheduledMessageAttachmentFiles() { // get list of all scheduled message ids - val scheduledMessageIds = ScheduledMessageRepositoryImpl().getAllScheduledMessageIdsSnapshot() + val scheduledMessageIds = scheduledMessageRepository.getAllScheduledMessageIdsSnapshot() // remove orphaned scheduled message dirs in files dir File(applicationContext.filesDir,"") diff --git a/data/src/main/java/com/moez/QKSMS/worker/InjectionWorkerFactory.kt b/data/src/main/java/com/moez/QKSMS/worker/InjectionWorkerFactory.kt new file mode 100644 index 00000000..aa27322a --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/worker/InjectionWorkerFactory.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 + * + * This file is part of QUIK. + * + * QUIK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QUIK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QUIK. If not, see . + */ +package dev.octoshrimpy.quik.worker + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.Worker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import dev.octoshrimpy.quik.interactor.ReceiveSms +import dev.octoshrimpy.quik.repository.ScheduledMessageRepository +import javax.inject.Inject + +class InjectionWorkerFactory @Inject constructor( + private val receiveSms: ReceiveSms, + private val scheduledMessageRepository: ScheduledMessageRepository +) +: WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + val instance = Class + .forName(workerClassName) + .asSubclass(Worker::class.java) + .getDeclaredConstructor(Context::class.java, WorkerParameters::class.java) + .newInstance(appContext, workerParameters) + + when (instance) { + is HousekeepingWorker -> { + instance.scheduledMessageRepository = scheduledMessageRepository + } + is ReceiveSmsWorker -> { + instance.receiveSms = receiveSms + } + } + + return instance + } +} \ No newline at end of file diff --git a/data/src/main/java/com/moez/QKSMS/worker/ReceiveSmsWorker.kt b/data/src/main/java/com/moez/QKSMS/worker/ReceiveSmsWorker.kt new file mode 100644 index 00000000..f408e48c --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/worker/ReceiveSmsWorker.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 + * + * This file is part of QUIK. + * + * QUIK is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QUIK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QUIK. If not, see . + */ +package dev.octoshrimpy.quik.worker + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import dev.octoshrimpy.quik.interactor.ReceiveSms +import timber.log.Timber +import javax.inject.Inject + +class ReceiveSmsWorker(appContext: Context, workerParams: WorkerParameters) + : Worker(appContext, workerParams) { + companion object { + const val INPUT_DATA_KEY_MESSAGE_ID = "messageId" + } + + @Inject lateinit var receiveSms: ReceiveSms + + override fun doWork(): Result { + Timber.v("started") + + val messageId = inputData.getLong(INPUT_DATA_KEY_MESSAGE_ID, -1) + if (messageId == -1L) { + Timber.v("failed. message id was -1") + return Result.failure(inputData) + } + + // process the new message + receiveSms.execute(messageId) + + Timber.v("finished") + + return Result.success() + } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/ReceiveSms.kt b/domain/src/main/java/com/moez/QKSMS/interactor/ReceiveSms.kt index 2a76d866..8082b90f 100644 --- a/domain/src/main/java/com/moez/QKSMS/interactor/ReceiveSms.kt +++ b/domain/src/main/java/com/moez/QKSMS/interactor/ReceiveSms.kt @@ -18,7 +18,6 @@ */ package dev.octoshrimpy.quik.interactor -import android.telephony.SmsMessage import dev.octoshrimpy.quik.blocking.BlockingClient import dev.octoshrimpy.quik.extensions.mapNotNull import dev.octoshrimpy.quik.manager.NotificationManager @@ -38,60 +37,67 @@ class ReceiveSms @Inject constructor( private val notificationManager: NotificationManager, private val updateBadge: UpdateBadge, private val shortcutManager: ShortcutManager -) : Interactor() { +) : Interactor() { - class Params(val subId: Int, val messages: Array) - - override fun buildObservable(params: Params): Flowable<*> { + override fun buildObservable(params: Long): Flowable<*> { return Flowable.just(params) - .filter { it.messages.isNotEmpty() } - .mapNotNull { - // Don't continue if the sender is blocked - val messages = it.messages - val address = messages[0].displayOriginatingAddress - val action = blockingClient.shouldBlock(address).blockingGet() - val shouldDrop = prefs.drop.get() - Timber.v("block=$action, drop=$shouldDrop") + .mapNotNull { messageRepo.getMessage(it) } + .mapNotNull { + val action = blockingClient.shouldBlock(it.address).blockingGet() - // If we should drop the message, don't even save it - if (action is BlockingClient.Action.Block && shouldDrop) { + when { + ((action is BlockingClient.Action.Block) && prefs.drop.get()) -> { + // blocked and 'drop blocked.' remove from db and don't continue + Timber.v("address is blocked and drop blocked is on. dropped") + messageRepo.deleteMessages(it.id) return@mapNotNull null } - - val time = messages[0].timestampMillis - val body: String = messages - .mapNotNull { message -> message.displayMessageBody } - .reduce { body, new -> body + new } - - // Add the message to the db - val message = messageRepo.insertReceivedSms(it.subId, address, body, time) - - when (action) { - is BlockingClient.Action.Block -> { - messageRepo.markRead(message.threadId) - conversationRepo.markBlocked(listOf(message.threadId), prefs.blockingManager.get(), action.reason) - } - is BlockingClient.Action.Unblock -> conversationRepo.markUnblocked(message.threadId) - else -> Unit + action is BlockingClient.Action.Block -> { + // blocked + Timber.v("address is blocked") + messageRepo.markRead(it.threadId) + conversationRepo.markBlocked( + listOf(it.threadId), + prefs.blockingManager.get(), + action.reason + ) } + action is BlockingClient.Action.Unblock -> { + // unblock + Timber.v("unblock conversation if blocked") + conversationRepo.markUnblocked(it.threadId) + } + } - message + // update and fetch conversation + conversationRepo.updateConversations(it.threadId) + conversationRepo.getOrCreateConversation(it.threadId) + } + .mapNotNull { + // don't notify (continue) for blocked conversations + if (it.blocked) { + Timber.v("no notifications for blocked") + return@mapNotNull null } - .doOnNext { message -> - conversationRepo.updateConversations(message.threadId) // Update the conversation + + // unarchive conversation if necessary + if (it.archived) { + Timber.v("conversation unarchived") + conversationRepo.markUnarchived(it.id) } - .mapNotNull { message -> - conversationRepo.getOrCreateConversation(message.threadId) // Map message to conversation - } - .filter { conversation -> !conversation.blocked } // Don't notify for blocked conversations - .doOnNext { conversation -> - // Unarchive conversation if necessary - if (conversation.archived) conversationRepo.markUnarchived(conversation.id) - } - .map { conversation -> conversation.id } // Map to the id because [delay] will put us on the wrong thread - .doOnNext { threadId -> notificationManager.update(threadId) } // Update the notification - .doOnNext { shortcutManager.updateShortcuts() } // Update shortcuts - .flatMap { updateBadge.buildObservable(Unit) } // Update the badge and widget + + // update/create notification + Timber.v("update/create notification") + notificationManager.update(it.id) + + // update shortcuts + Timber.v("update shortcuts") + shortcutManager.updateShortcuts() + + // update the badge and widget + Timber.v("update badge and widget") + updateBadge.buildObservable(Unit) + } } } diff --git a/presentation/build.gradle b/presentation/build.gradle index 9759730c..5b84fe5d 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -123,6 +123,7 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:$androidx_emoji_version" implementation "androidx.viewpager2:viewpager2:$androidx_viewpager_version" implementation "com.google.android.material:material:$material_version" + implementation("androidx.work:work-runtime-ktx:$androidx_work_version") // conductor implementation "com.bluelinelabs:conductor:$conductor_version" diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index f93b74f3..5caa853a 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -246,6 +246,18 @@ + + + + + diff --git a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt index c7f67db1..f9b7f8ef 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt @@ -25,7 +25,9 @@ import android.content.BroadcastReceiver import androidx.core.provider.FontRequest import androidx.emoji.text.EmojiCompat import androidx.emoji.text.FontRequestEmojiCompatConfig -import com.moez.QKSMS.manager.HousekeepingWorkManager +import androidx.work.Configuration +import androidx.work.WorkManager +import androidx.work.WorkerFactory import com.moez.QKSMS.manager.SpeakManager import dev.octoshrimpy.quik.R import dev.octoshrimpy.quik.common.util.CrashlyticsTree @@ -46,6 +48,7 @@ import dagger.android.HasActivityInjector import dagger.android.HasBroadcastReceiverInjector import dagger.android.HasServiceInjector import dev.octoshrimpy.quik.interactor.SpeakThreads +import dev.octoshrimpy.quik.worker.HousekeepingWorker import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.Dispatchers @@ -72,6 +75,7 @@ class QKApplication : Application(), HasActivityInjector, HasBroadcastReceiverIn @Inject lateinit var nightModeManager: NightModeManager @Inject lateinit var realmMigration: QkRealmMigration @Inject lateinit var referralManager: ReferralManager + @Inject lateinit var workerFactory: WorkerFactory override fun onCreate() { super.onCreate() @@ -116,8 +120,14 @@ class QKApplication : Application(), HasActivityInjector, HasBroadcastReceiverIn .configureWith(AutoDisposeConfigurer::configure) .install() - // register, or re-register, once-a-day-housekeeping work manager - HousekeepingWorkManager.register(applicationContext) + // init work manager with custom factory supporting dagger/injection capability + WorkManager.initialize( + this, + Configuration.Builder().setWorkerFactory(workerFactory).build() + ) + + // register, or re-register, housekeeping work manager + HousekeepingWorker.register(applicationContext) } override fun activityInjector(): AndroidInjector { diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt index 5ceb8344..a37cf052 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.SharedPreferences import android.preference.PreferenceManager import androidx.lifecycle.ViewModelProvider +import androidx.work.WorkerFactory import com.f2prateek.rx.preferences2.RxSharedPreferences import dev.octoshrimpy.quik.blocking.BlockingClient import dev.octoshrimpy.quik.blocking.BlockingManager @@ -88,6 +89,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import dagger.Module import dagger.Provides +import dev.octoshrimpy.quik.worker.InjectionWorkerFactory import javax.inject.Singleton @Module(subcomponents = [ @@ -217,4 +219,7 @@ class AppModule(private var application: Application) { @Provides fun provideSyncRepository(repository: SyncRepositoryImpl): SyncRepository = repository + // worker factory + @Provides + fun provideWorkerFactory(workerFactory: InjectionWorkerFactory): WorkerFactory = workerFactory } \ No newline at end of file