Fix crashes in CoroutineIntentService

This commit is contained in:
Koitharu
2022-10-01 09:28:11 +03:00
parent 23239f1fec
commit 4af8e73303
6 changed files with 68 additions and 11 deletions

View File

@@ -4,11 +4,13 @@ import android.app.Service
import android.content.Intent import android.content.Intent
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
abstract class CoroutineIntentService : BaseService() { abstract class CoroutineIntentService : BaseService() {
@@ -21,11 +23,13 @@ abstract class CoroutineIntentService : BaseService() {
return Service.START_REDELIVER_INTENT 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 { mutex.withLock {
try { try {
withContext(dispatcher) { if (intent != null) {
processIntent(intent) withContext(dispatcher) {
processIntent(startId, intent)
}
} }
} finally { } finally {
stopSelf(startId) 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)
}
} }

View File

@@ -16,7 +16,12 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint 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 kotlinx.coroutines.launch
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -85,7 +90,9 @@ class DownloadService : BaseService() {
override fun onDestroy() { override fun onDestroy() {
unregisterReceiver(controlReceiver) unregisterReceiver(controlReceiver)
wakeLock.release() if (wakeLock.isHeld) {
wakeLock.release()
}
isRunning = false isRunning = false
super.onDestroy() super.onDestroy()
} }
@@ -169,6 +176,7 @@ class DownloadService : BaseService() {
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
jobs[cancelId]?.cancel() jobs[cancelId]?.cancel()
} }
ACTION_DOWNLOAD_RESUME -> { ACTION_DOWNLOAD_RESUME -> {
val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0)
jobs[cancelId]?.resume() jobs[cancelId]?.resume()

View File

@@ -23,7 +23,12 @@ import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.local.domain.importer.MangaImporter import org.koitharu.kotatsu.local.domain.importer.MangaImporter
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.PendingIntentCompat 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 import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -48,8 +53,8 @@ class ImportService : CoroutineIntentService() {
super.onDestroy() super.onDestroy()
} }
override suspend fun processIntent(intent: Intent?) { override suspend fun processIntent(startId: Int, intent: Intent) {
val uris = intent?.getParcelableArrayListExtra<Uri>(EXTRA_URIS) val uris = intent.getParcelableArrayListExtra<Uri>(EXTRA_URIS)
if (uris.isNullOrEmpty()) { if (uris.isNullOrEmpty()) {
return return
} }
@@ -69,6 +74,10 @@ class ImportService : CoroutineIntentService() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
} }
override fun onError(startId: Int, error: Throwable) {
error.report(null)
}
private suspend fun importImpl(uri: Uri): Manga { private suspend fun importImpl(uri: Uri): Manga {
val importer = importerFactory.create(uri) val importer = importerFactory.create(uri)
return importer.import(uri) return importer.import(uri)

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import javax.inject.Inject import javax.inject.Inject
@@ -34,8 +35,8 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
super.onDestroy() super.onDestroy()
} }
override suspend fun processIntent(intent: Intent?) { override suspend fun processIntent(startId: Int, intent: Intent) {
val manga = intent?.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return val manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
startForeground() startForeground()
val mangaWithChapters = localMangaRepository.getDetails(manga) val mangaWithChapters = localMangaRepository.getDetails(manga)
@@ -47,6 +48,21 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) 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() { private fun startForeground() {
val title = getString(R.string.local_manga_processing) val title = getString(R.string.local_manga_processing)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

View File

@@ -5,7 +5,9 @@ import android.content.res.Resources
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import okio.FileNotFoundException import okio.FileNotFoundException
import okio.IOException
import org.acra.ktx.sendWithAcra import org.acra.ktx.sendWithAcra
import org.json.JSONException
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CaughtException import org.koitharu.kotatsu.core.exceptions.CaughtException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException 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.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
private const val MSG_NO_SPACE_LEFT = "No space left on device"
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is AuthRequiredException -> resources.getString(R.string.auth_required) is AuthRequiredException -> resources.getString(R.string.auth_required)
is CloudFlareProtectedException -> resources.getString(R.string.captcha_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 WrongPasswordException -> resources.getString(R.string.wrong_password)
is NotFoundException -> resources.getString(R.string.not_found_404) is NotFoundException -> resources.getString(R.string.not_found_404)
is IOException -> getDisplayMessage(message, resources) ?: localizedMessage
else -> localizedMessage else -> localizedMessage
} ?: resources.getString(R.string.error_occurred) } ?: 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 { fun Throwable.isReportable(): Boolean {
return this is Error || this.javaClass in reportableExceptions return this is Error || this.javaClass in reportableExceptions
} }
@@ -55,6 +66,7 @@ fun Throwable.report(message: String?) {
private val reportableExceptions = arraySetOf<Class<*>>( private val reportableExceptions = arraySetOf<Class<*>>(
ParseException::class.java, ParseException::class.java,
JSONException::class.java,
RuntimeException::class.java, RuntimeException::class.java,
IllegalStateException::class.java, IllegalStateException::class.java,
IllegalArgumentException::class.java, IllegalArgumentException::class.java,

View File

@@ -388,4 +388,5 @@
<string name="color_correction_hint">The chosen color settings will be remembered for this manga</string> <string name="color_correction_hint">The chosen color settings will be remembered for this manga</string>
<string name="text_unsaved_changes_prompt">You have unsaved changes, do you want to save or discard them?</string> <string name="text_unsaved_changes_prompt">You have unsaved changes, do you want to save or discard them?</string>
<string name="discard">Discard</string> <string name="discard">Discard</string>
<string name="error_no_space_left">No space left on device</string>
</resources> </resources>