Move import to service

master
Koitharu 2 years ago
parent 4d7ff5f6cc
commit 09eb82ca2e
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -281,6 +281,9 @@
<service <service
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
android:name="org.koitharu.kotatsu.local.ui.ImportService"
android:foregroundServiceType="dataSync" />
<service <service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.core.ui package org.koitharu.kotatsu.core.ui
import android.content.Intent import android.content.Intent
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@ -39,8 +41,10 @@ abstract class CoroutineIntentService : BaseService() {
} }
} }
@WorkerThread
protected abstract suspend fun processIntent(startId: Int, intent: Intent) protected abstract suspend fun processIntent(startId: Int, intent: Intent)
@AnyThread
protected abstract fun onError(startId: Int, error: Throwable) protected abstract fun onError(startId: Int, error: Throwable)
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable -> private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->

@ -66,8 +66,12 @@ class ImportDialogFragment : AlertDialogFragment<DialogImportBinding>(), View.On
storageManager.takePermissions(it) storageManager.takePermissions(it)
} }
val ctx = requireContext() val ctx = requireContext()
ImportWorker.start(ctx, uris) val msg = if (ImportService.start(ctx, uris)) {
Toast.makeText(ctx, R.string.import_will_start_soon, Toast.LENGTH_LONG).show() R.string.import_will_start_soon
} else {
R.string.error_occurred
}
Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show()
dismiss() dismiss()
} }

@ -3,63 +3,75 @@ package org.koitharu.kotatsu.local.ui
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.hilt.work.HiltWorker import androidx.core.app.ServiceCompat
import androidx.work.Constraints import androidx.core.content.ContextCompat
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 coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.assisted.Assisted import dagger.hilt.android.AndroidEntryPoint
import dagger.assisted.AssistedInject import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ErrorReporterReceiver 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.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage 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.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.local.data.importer.SingleMangaImporter import org.koitharu.kotatsu.local.data.importer.SingleMangaImporter
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
@HiltWorker @AndroidEntryPoint
class ImportWorker @AssistedInject constructor( class ImportService : CoroutineIntentService() {
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val importer: SingleMangaImporter,
private val coil: ImageLoader
) : CoroutineWorker(appContext, params) {
private val notificationManager by lazy { NotificationManagerCompat.from(appContext) } @Inject
lateinit var importer: SingleMangaImporter
override suspend fun doWork(): Result { @Inject
val uri = inputData.getString(DATA_URI)?.toUriOrNull() ?: return Result.failure() lateinit var coil: ImageLoader
setForeground(getForegroundInfo())
val result = runCatchingCancellable { private lateinit var notificationManager: NotificationManagerCompat
importer.import(uri).manga
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)) { if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(result) val notification = runBlocking { buildNotification(Result.failure(error)) }
notificationManager.notify(uri.hashCode(), notification) 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 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) .setName(title)
.setShowBadge(false) .setShowBadge(false)
.setVibrationEnabled(false) .setVibrationEnabled(false)
@ -80,14 +92,15 @@ class ImportWorker @AssistedInject constructor(
.setCategory(NotificationCompat.CATEGORY_PROGRESS) .setCategory(NotificationCompat.CATEGORY_PROGRESS)
.build() .build()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ServiceCompat.startForeground(
ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) this,
} else { FOREGROUND_NOTIFICATION_ID,
ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification) notification,
} ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} }
private suspend fun buildNotification(result: kotlin.Result<Manga>): Notification { private suspend fun buildNotification(result: Result<Manga>): Notification {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(0) .setDefaults(0)
@ -135,26 +148,21 @@ class ImportWorker @AssistedInject constructor(
companion object { companion object {
const val DATA_URI = "uri" private const val DATA_URI = "uri"
private const val TAG = "import" private const val TAG = "import"
private const val CHANNEL_ID = "importing" private const val CHANNEL_ID = "importing"
private const val FOREGROUND_NOTIFICATION_ID = 37 private const val FOREGROUND_NOTIFICATION_ID = 37
fun start(context: Context, uris: Iterable<Uri>) { fun start(context: Context, uris: Iterable<Uri>): Boolean = try {
val constraints = Constraints.Builder() for (uri in uris) {
.setRequiresStorageNotLow(true) val intent = Intent(context, ImportService::class.java)
.build() intent.putExtra(DATA_URI, uri.toString())
val requests = uris.map { uri -> ContextCompat.startForegroundService(context, intent)
OneTimeWorkRequestBuilder<ImportWorker>()
.setConstraints(constraints)
.addTag(TAG)
.setInputData(Data.Builder().putString(DATA_URI, uri.toString()).build())
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
} }
WorkManager.getInstance(context) true
.enqueue(requests) } catch (e: Exception) {
e.printStackTraceDebug()
false
} }
} }
} }

@ -48,7 +48,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor(
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.build() .build()
val request = OneTimeWorkRequestBuilder<ImportWorker>() val request = OneTimeWorkRequestBuilder<LocalStorageCleanupWorker>()
.setConstraints(constraints) .setConstraints(constraints)
.addTag(TAG) .addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)

@ -92,7 +92,7 @@ open class RemoteListViewModel @Inject constructor(
} }
onBuildList(this) onBuildList(this)
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
init { init {
filter.observeState() filter.observeState()

Loading…
Cancel
Save