From 4af8e73303c43f3dfb1862db8f3cb8a90661194b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 1 Oct 2022 09:28:11 +0300 Subject: [PATCH] Fix crashes in CoroutineIntentService --- .../kotatsu/base/ui/CoroutineIntentService.kt | 19 ++++++++++++++---- .../download/ui/service/DownloadService.kt | 12 +++++++++-- .../kotatsu/local/ui/ImportService.kt | 15 +++++++++++--- .../local/ui/LocalChaptersRemoveService.kt | 20 +++++++++++++++++-- .../kotatsu/utils/ext/ThrowableExt.kt | 12 +++++++++++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 68 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt index 0e63b3950..32c9c2904 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt @@ -4,11 +4,13 @@ import android.app.Service import android.content.Intent import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug abstract class CoroutineIntentService : BaseService() { @@ -21,11 +23,13 @@ abstract class CoroutineIntentService : BaseService() { return Service.START_REDELIVER_INTENT } - private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch { + private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch(errorHandler(startId)) { mutex.withLock { try { - withContext(dispatcher) { - processIntent(intent) + if (intent != null) { + withContext(dispatcher) { + processIntent(startId, intent) + } } } finally { stopSelf(startId) @@ -33,5 +37,12 @@ abstract class CoroutineIntentService : BaseService() { } } - protected abstract suspend fun processIntent(intent: Intent?) + protected abstract suspend fun processIntent(startId: Int, intent: Intent) + + protected abstract fun onError(startId: Int, error: Throwable) + + private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable -> + throwable.printStackTraceDebug() + onError(startId, throwable) + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index 873171ef0..34be0abb2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -16,7 +16,12 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R @@ -85,7 +90,9 @@ class DownloadService : BaseService() { override fun onDestroy() { unregisterReceiver(controlReceiver) - wakeLock.release() + if (wakeLock.isHeld) { + wakeLock.release() + } isRunning = false super.onDestroy() } @@ -169,6 +176,7 @@ class DownloadService : BaseService() { val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) jobs[cancelId]?.cancel() } + ACTION_DOWNLOAD_RESUME -> { val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) jobs[cancelId]?.resume() diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportService.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportService.kt index 408bae352..86732aa91 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/ImportService.kt @@ -23,7 +23,12 @@ import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.local.domain.importer.MangaImporter import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.PendingIntentCompat -import org.koitharu.kotatsu.utils.ext.* +import org.koitharu.kotatsu.utils.ext.asArrayList +import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.referer +import org.koitharu.kotatsu.utils.ext.report +import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import javax.inject.Inject @AndroidEntryPoint @@ -48,8 +53,8 @@ class ImportService : CoroutineIntentService() { super.onDestroy() } - override suspend fun processIntent(intent: Intent?) { - val uris = intent?.getParcelableArrayListExtra(EXTRA_URIS) + override suspend fun processIntent(startId: Int, intent: Intent) { + val uris = intent.getParcelableArrayListExtra(EXTRA_URIS) if (uris.isNullOrEmpty()) { return } @@ -69,6 +74,10 @@ class ImportService : CoroutineIntentService() { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) } + override fun onError(startId: Int, error: Throwable) { + error.report(null) + } + private suspend fun importImpl(uri: Uri): Manga { val importer = importerFactory.create(uri) return importer.import(uri) diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt index e469fccae..9e73ffc69 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat import javax.inject.Inject @@ -34,8 +35,8 @@ class LocalChaptersRemoveService : CoroutineIntentService() { super.onDestroy() } - override suspend fun processIntent(intent: Intent?) { - val manga = intent?.getParcelableExtraCompat(EXTRA_MANGA)?.manga ?: return + override suspend fun processIntent(startId: Int, intent: Intent) { + val manga = intent.getParcelableExtraCompat(EXTRA_MANGA)?.manga ?: return val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return startForeground() val mangaWithChapters = localMangaRepository.getDetails(manga) @@ -47,6 +48,21 @@ class LocalChaptersRemoveService : CoroutineIntentService() { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) } + override fun onError(startId: Int, error: Throwable) { + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.error_occurred)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setDefaults(0) + .setColor(ContextCompat.getColor(this, R.color.blue_primary_dark)) + .setSilent(true) + .setContentText(error.getDisplayMessage(resources)) + .setSmallIcon(android.R.drawable.stat_notify_error) + .setAutoCancel(true) + .build() + val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + nm.notify(NOTIFICATION_ID + startId, notification) + } + private fun startForeground() { val title = getString(R.string.local_manga_processing) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt index 439ad2c4b..d06a7b491 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt @@ -5,7 +5,9 @@ import android.content.res.Resources import androidx.collection.arraySetOf import kotlinx.coroutines.CancellationException import okio.FileNotFoundException +import okio.IOException import org.acra.ktx.sendWithAcra +import org.json.JSONException import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.CaughtException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException @@ -20,6 +22,8 @@ import org.koitharu.kotatsu.parsers.exception.ParseException import java.net.SocketTimeoutException import java.net.UnknownHostException +private const val MSG_NO_SPACE_LEFT = "No space left on device" + fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { is AuthRequiredException -> resources.getString(R.string.auth_required) is CloudFlareProtectedException -> resources.getString(R.string.captcha_required) @@ -41,9 +45,16 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { is WrongPasswordException -> resources.getString(R.string.wrong_password) is NotFoundException -> resources.getString(R.string.not_found_404) + is IOException -> getDisplayMessage(message, resources) ?: localizedMessage else -> localizedMessage } ?: resources.getString(R.string.error_occurred) +private fun getDisplayMessage(msg: String?, resources: Resources): String? = when { + msg.isNullOrEmpty() -> null + msg.contains(MSG_NO_SPACE_LEFT) -> resources.getString(R.string.error_no_space_left) + else -> null +} + fun Throwable.isReportable(): Boolean { return this is Error || this.javaClass in reportableExceptions } @@ -55,6 +66,7 @@ fun Throwable.report(message: String?) { private val reportableExceptions = arraySetOf>( ParseException::class.java, + JSONException::class.java, RuntimeException::class.java, IllegalStateException::class.java, IllegalArgumentException::class.java, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36cd8f13b..16906a84e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -388,4 +388,5 @@ The chosen color settings will be remembered for this manga You have unsaved changes, do you want to save or discard them? Discard + No space left on device