From 09eb82ca2e838a1cb8ed8879c763a1fe425ecaeb Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 11 Apr 2024 18:01:28 +0300 Subject: [PATCH] Move import to service --- app/src/main/AndroidManifest.xml | 3 + .../kotatsu/core/ui/CoroutineIntentService.kt | 4 + .../kotatsu/local/ui/ImportDialogFragment.kt | 8 +- .../ui/{ImportWorker.kt => ImportService.kt} | 110 ++++++++++-------- .../local/ui/LocalStorageCleanupWorker.kt | 2 +- .../remotelist/ui/RemoteListViewModel.kt | 2 +- 6 files changed, 74 insertions(+), 55 deletions(-) rename app/src/main/kotlin/org/koitharu/kotatsu/local/ui/{ImportWorker.kt => ImportService.kt} (63%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b9a3026c..be76705d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -281,6 +281,9 @@ + diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt index 2d4ead31d..e8f167f32 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt @@ -1,6 +1,8 @@ package org.koitharu.kotatsu.core.ui import android.content.Intent +import androidx.annotation.AnyThread +import androidx.annotation.WorkerThread import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineExceptionHandler @@ -39,8 +41,10 @@ abstract class CoroutineIntentService : BaseService() { } } + @WorkerThread protected abstract suspend fun processIntent(startId: Int, intent: Intent) + @AnyThread protected abstract fun onError(startId: Int, error: Throwable) private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt index cd40efb5a..fdc21e1d8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportDialogFragment.kt @@ -66,8 +66,12 @@ class ImportDialogFragment : AlertDialogFragment(), View.On storageManager.takePermissions(it) } val ctx = requireContext() - ImportWorker.start(ctx, uris) - Toast.makeText(ctx, R.string.import_will_start_soon, Toast.LENGTH_LONG).show() + val msg = if (ImportService.start(ctx, uris)) { + R.string.import_will_start_soon + } else { + R.string.error_occurred + } + Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show() dismiss() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt similarity index 63% rename from app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportWorker.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt index 2841bbe02..45003d292 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt @@ -3,63 +3,75 @@ package org.koitharu.kotatsu.local.ui import android.app.Notification import android.app.PendingIntent import android.content.Context +import android.content.Intent import android.content.pm.ServiceInfo import android.net.Uri -import android.os.Build import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat -import androidx.hilt.work.HiltWorker -import androidx.work.Constraints -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.ForegroundInfo -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.OutOfQuotaPolicy -import androidx.work.WorkManager -import androidx.work.WorkerParameters +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat import coil.ImageLoader import coil.request.ImageRequest -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.runBlocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ErrorReporterReceiver +import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.local.data.importer.SingleMangaImporter import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import javax.inject.Inject -@HiltWorker -class ImportWorker @AssistedInject constructor( - @Assisted appContext: Context, - @Assisted params: WorkerParameters, - private val importer: SingleMangaImporter, - private val coil: ImageLoader -) : CoroutineWorker(appContext, params) { +@AndroidEntryPoint +class ImportService : CoroutineIntentService() { - private val notificationManager by lazy { NotificationManagerCompat.from(appContext) } + @Inject + lateinit var importer: SingleMangaImporter - override suspend fun doWork(): Result { - val uri = inputData.getString(DATA_URI)?.toUriOrNull() ?: return Result.failure() - setForeground(getForegroundInfo()) - val result = runCatchingCancellable { - importer.import(uri).manga + @Inject + lateinit var coil: ImageLoader + + private lateinit var notificationManager: NotificationManagerCompat + + override fun onCreate() { + super.onCreate() + notificationManager = NotificationManagerCompat.from(applicationContext) + } + + override suspend fun processIntent(startId: Int, intent: Intent) { + val uri = requireNotNull(intent.getStringExtra(DATA_URI)?.toUriOrNull()) { "No unput uri" } + startForeground() + try { + val result = runCatchingCancellable { + importer.import(uri).manga + } + if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { + val notification = buildNotification(result) + notificationManager.notify(TAG, startId, notification) + } + } finally { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) } + } + + override fun onError(startId: Int, error: Throwable) { if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { - val notification = buildNotification(result) - notificationManager.notify(uri.hashCode(), notification) + val notification = runBlocking { buildNotification(Result.failure(error)) } + notificationManager.notify(TAG, startId, notification) } - return Result.success() } - override suspend fun getForegroundInfo(): ForegroundInfo { + private suspend fun startForeground() { val title = applicationContext.getString(R.string.importing_manga) - val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) + val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT) .setName(title) .setShowBadge(false) .setVibrationEnabled(false) @@ -80,14 +92,15 @@ class ImportWorker @AssistedInject constructor( .setCategory(NotificationCompat.CATEGORY_PROGRESS) .build() - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) - } else { - ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification) - } + ServiceCompat.startForeground( + this, + FOREGROUND_NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, + ) } - private suspend fun buildNotification(result: kotlin.Result): Notification { + private suspend fun buildNotification(result: Result): Notification { val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setDefaults(0) @@ -135,26 +148,21 @@ class ImportWorker @AssistedInject constructor( companion object { - const val DATA_URI = "uri" - + private const val DATA_URI = "uri" private const val TAG = "import" private const val CHANNEL_ID = "importing" private const val FOREGROUND_NOTIFICATION_ID = 37 - fun start(context: Context, uris: Iterable) { - val constraints = Constraints.Builder() - .setRequiresStorageNotLow(true) - .build() - val requests = uris.map { uri -> - OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .addTag(TAG) - .setInputData(Data.Builder().putString(DATA_URI, uri.toString()).build()) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .build() + fun start(context: Context, uris: Iterable): Boolean = try { + for (uri in uris) { + val intent = Intent(context, ImportService::class.java) + intent.putExtra(DATA_URI, uri.toString()) + ContextCompat.startForegroundService(context, intent) } - WorkManager.getInstance(context) - .enqueue(requests) + true + } catch (e: Exception) { + e.printStackTraceDebug() + false } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt index cc6c6ccab..661a50281 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt @@ -48,7 +48,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor( val constraints = Constraints.Builder() .setRequiresBatteryNotLow(true) .build() - val request = OneTimeWorkRequestBuilder() + val request = OneTimeWorkRequestBuilder() .setConstraints(constraints) .addTag(TAG) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 87bb46103..fa80a5f1b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -92,7 +92,7 @@ open class RemoteListViewModel @Inject constructor( } onBuildList(this) } - }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState)) init { filter.observeState()