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