Fix crashes in CoroutineIntentService
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user