From e1233999116e0d8865deb8e119782d3169f47739 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 11 Mar 2020 20:37:58 +0200 Subject: [PATCH] Cancelling downloads --- .../core/parser/LocalMangaRepository.kt | 17 ++-- .../kotatsu/domain/MangaProviderFactory.kt | 3 + .../koitharu/kotatsu/domain/local/MangaZip.kt | 4 +- .../ui/download/DownloadNotification.kt | 52 +++++++++++- .../kotatsu/ui/download/DownloadService.kt | 80 +++++++++++++++---- .../kotatsu/utils/ext/NotificationExt.kt | 8 ++ .../koitharu/kotatsu/utils/ext/StringExt.kt | 2 +- app/src/main/res/drawable/ic_cross.xml | 4 +- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 10 files changed, 143 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/NotificationExt.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt index f947ececd..5e07c1cb0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt @@ -31,11 +31,11 @@ class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaReposit ): List { val files = context.getExternalFilesDirs("manga") .flatMap { x -> x?.listFiles(CbzFilter())?.toList().orEmpty() } - return files.mapNotNull { x -> safe { getDetails(x) } } + return files.mapNotNull { x -> safe { getFromFile(x) } } } override suspend fun getDetails(manga: Manga) = if (manga.chapters == null) { - getDetails(Uri.parse(manga.url).toFile()) + getFromFile(Uri.parse(manga.url).toFile()) } else manga override suspend fun getPages(chapter: MangaChapter): List { @@ -59,7 +59,13 @@ class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaReposit } } - private fun getDetails(file: File): Manga { + + fun delete(manga: Manga): Boolean { + val file = Uri.parse(manga.url).toFile() + return file.delete() + } + + fun getFromFile(file: File): Manga { val zip = ZipFile(file) val fileUri = file.toUri().toString() val entry = zip.getEntry(MangaZip.INDEX_ENTRY) @@ -98,11 +104,6 @@ class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaReposit } } - fun delete(manga: Manga): Boolean { - val file = Uri.parse(manga.url).toFile() - return file.delete() - } - private fun zipUri(file: File, entryName: String) = Uri.fromParts("cbz", file.path, entryName).toString() diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/MangaProviderFactory.kt b/app/src/main/java/org/koitharu/kotatsu/domain/MangaProviderFactory.kt index af668d280..9814e6494 100644 --- a/app/src/main/java/org/koitharu/kotatsu/domain/MangaProviderFactory.kt +++ b/app/src/main/java/org/koitharu/kotatsu/domain/MangaProviderFactory.kt @@ -4,6 +4,7 @@ import org.koin.core.KoinComponent import org.koin.core.get import org.koin.core.inject import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings @@ -21,6 +22,8 @@ object MangaProviderFactory : KoinComponent { } } + fun createLocal() = LocalMangaRepository(loaderContext) + fun create(source: MangaSource): MangaRepository { val constructor = source.cls.getConstructor(MangaLoaderContext::class.java) return constructor.newInstance(loaderContext) diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt index 5d305f425..e6bbbdbc1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt +++ b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt @@ -12,9 +12,9 @@ import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream @WorkerThread -class MangaZip(private val file: File) { +class MangaZip(val file: File) { - private val dir = file.parentFile?.sub(file.name + ".dir")?.takeIf { it.mkdir() } + private val dir = file.parentFile?.sub(file.name + ".tmp")?.takeIf { it.mkdir() } ?: throw RuntimeException("Cannot create temporary directory") private val index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText()) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt index 636a0ba0c..398478a4b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.ui.download import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable @@ -10,6 +11,9 @@ import android.os.Build import androidx.core.app.NotificationCompat import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.ui.details.MangaDetailsActivity +import org.koitharu.kotatsu.utils.ext.clearActions +import org.koitharu.kotatsu.utils.ext.getDisplayMessage import kotlin.math.roundToInt class DownloadNotification(private val context: Context) { @@ -37,6 +41,33 @@ class DownloadNotification(private val context: Context) { builder.setProgress(1, 0, true) builder.setSmallIcon(android.R.drawable.stat_sys_download) builder.setLargeIcon(null) + builder.setContentIntent(null) + } + + fun setCancelId(startId: Int) { + if (startId == 0) { + builder.clearActions() + } else { + val intent = DownloadService.getCancelIntent(context, startId) + builder.addAction( + R.drawable.ic_cross, + context.getString(android.R.string.cancel), + PendingIntent.getService( + context, + startId, + intent, + PendingIntent.FLAG_CANCEL_CURRENT + ) + ) + } + } + + fun setError(e: Throwable) { + builder.setProgress(0, 0, false) + builder.setSmallIcon(android.R.drawable.stat_notify_error) + builder.setSubText(context.getString(R.string.error)) + builder.setContentText(e.getDisplayMessage(context.resources)) + builder.setContentIntent(null) } fun setLargeIcon(icon: Drawable?) { @@ -57,16 +88,27 @@ class DownloadNotification(private val context: Context) { builder.setContentText(context.getString(R.string.processing_)) } - fun setDone() { + fun setDone(manga: Manga) { builder.setProgress(0, 0, false) builder.setContentText(context.getString(R.string.download_complete)) + builder.setContentIntent(createIntent(context, manga)) builder.setSmallIcon(android.R.drawable.stat_sys_download_done) } + fun setCancelling() { + builder.setProgress(1, 0, true) + builder.setContentText(context.getString(R.string.cancelling_)) + builder.setContentIntent(null) + } + fun update(id: Int = NOTIFICATION_ID) { manager.notify(id, builder.build()) } + fun dismiss(id: Int = NOTIFICATION_ID) { + manager.cancel(id) + } + operator fun invoke(): Notification = builder.build() companion object { @@ -75,5 +117,13 @@ class DownloadNotification(private val context: Context) { const val CHANNEL_ID = "download" private const val PROGRESS_STEP = 20 + + @JvmStatic + private fun createIntent(context: Context, manga: Manga) = PendingIntent.getActivity( + context, + manga.hashCode(), + MangaDetailsActivity.newIntent(context, manga), + PendingIntent.FLAG_CANCEL_CURRENT + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt index 1ac2f5e60..c8dbae2b1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt @@ -4,17 +4,15 @@ import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.webkit.MimeTypeMap -import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import coil.Coil import coil.api.get -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex import okhttp3.OkHttpClient import okhttp3.Request import org.koin.core.inject +import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.local.PagesCache import org.koitharu.kotatsu.core.model.Manga @@ -37,6 +35,8 @@ class DownloadService : BaseService() { private val okHttp by inject() private val cache by inject() + private val jobs = HashMap() + private val mutex = Mutex() override fun onCreate() { super.onCreate() @@ -44,21 +44,35 @@ class DownloadService : BaseService() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val manga = intent?.getParcelableExtra(EXTRA_MANGA) - val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() - if (manga != null) { - downloadManga(manga, chapters) - } else { - stopSelf(startId) + when (intent?.action) { + ACTION_DOWNLOAD_START -> { + val manga = intent.getParcelableExtra(EXTRA_MANGA) + val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() + if (manga != null) { + jobs[startId] = downloadManga(manga, chapters, startId) + } else { + stopSelf(startId) + } + } + ACTION_DOWNLOAD_CANCEL -> { + val cancelId = intent.getIntExtra(EXTRA_CANCEL_ID, 0) + jobs.remove(cancelId)?.cancel() + stopSelf(startId) + } + else -> stopSelf(startId) } return START_NOT_STICKY } - private fun downloadManga(manga: Manga, chaptersIds: Set?) { - val destination = getExternalFilesDir("manga")!! - notification.fillFrom(manga) - startForeground(DownloadNotification.NOTIFICATION_ID, notification()) - launch(Dispatchers.IO) { + private fun downloadManga(manga: Manga, chaptersIds: Set?, startId: Int): Job { + return launch(Dispatchers.IO) { + mutex.lock() + withContext(Dispatchers.Main) { + notification.fillFrom(manga) + notification.setCancelId(startId) + startForeground(DownloadNotification.NOTIFICATION_ID, notification()) + } + val destination = getExternalFilesDir("manga")!! var output: MangaZip? = null try { val repo = MangaProviderFactory.create(manga.source) @@ -106,21 +120,41 @@ class DownloadService : BaseService() { } } withContext(Dispatchers.Main) { + notification.setCancelId(0) notification.setPostProcessing() notification.update() } output.compress() + val result = MangaProviderFactory.createLocal().getFromFile(output.file) withContext(Dispatchers.Main) { - notification.setDone() + notification.setDone(result) + notification.dismiss() + notification.update(manga.id.toInt().absoluteValue) + } + } catch (_: CancellationException) { + withContext(Dispatchers.Main + NonCancellable) { + notification.setCancelling() + notification.setCancelId(0) + notification.update() + } + } catch (e: Throwable) { + withContext(Dispatchers.Main) { + notification.setError(e) + notification.setCancelId(0) + notification.dismiss() notification.update(manga.id.toInt().absoluteValue) } } finally { withContext(NonCancellable) { + jobs.remove(startId) output?.cleanup() destination.sub("page.tmp").delete() withContext(Dispatchers.Main) { stopForeground(true) + notification.dismiss() + stopSelf(startId) } + mutex.unlock() } } } @@ -145,12 +179,19 @@ class DownloadService : BaseService() { companion object { + private const val ACTION_DOWNLOAD_START = + "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_START" + private const val ACTION_DOWNLOAD_CANCEL = + "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL" + private const val EXTRA_MANGA = "manga" private const val EXTRA_CHAPTERS_IDS = "chapters_ids" + private const val EXTRA_CANCEL_ID = "cancel_id" fun start(context: Context, manga: Manga, chaptersIds: Collection? = null) { confirmDataTransfer(context) { val intent = Intent(context, DownloadService::class.java) + intent.action = ACTION_DOWNLOAD_START intent.putExtra(EXTRA_MANGA, manga) if (chaptersIds != null) { intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray()) @@ -159,6 +200,11 @@ class DownloadService : BaseService() { } } + fun getCancelIntent(context: Context, startId: Int) = + Intent(context, DownloadService::class.java) + .setAction(ACTION_DOWNLOAD_CANCEL) + .putExtra(ACTION_DOWNLOAD_CANCEL, startId) + private fun confirmDataTransfer(context: Context, callback: () -> Unit) { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val settings = AppSettings(context) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/NotificationExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/NotificationExt.kt new file mode 100644 index 000000000..3e9a53b7b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/NotificationExt.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.utils.ext + +import androidx.core.app.NotificationCompat + +fun NotificationCompat.Builder.clearActions(): NotificationCompat.Builder { + mActions.clear() + return this +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index ed1d0bea2..f9cd52848 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -47,7 +47,7 @@ fun String.transliterate(skipMissing: Boolean): String { ) return buildString(length + 5) { for (c in this@transliterate) { - val p = cyr.binarySearch(c) + val p = cyr.binarySearch(c.toLowerCase()) if (p in lat.indices) { append(lat[p]) } else if (!skipMissing) { diff --git a/app/src/main/res/drawable/ic_cross.xml b/app/src/main/res/drawable/ic_cross.xml index 5cfade110..e95e6a295 100644 --- a/app/src/main/res/drawable/ic_cross.xml +++ b/app/src/main/res/drawable/ic_cross.xml @@ -3,8 +3,8 @@ android:width="24dp" android:height="24dp" android:tint="?attr/colorControlNormal" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e6000d879..8aff71eac 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -93,4 +93,6 @@ Предупреждение Данная операция может привести к большому расходу траффика Больше не спрашивать + Отмена… + Ошибка \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df56bd600..7c6114985 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,4 +94,6 @@ Warning This operation may consume a lot of network traffic Don`t ask again + Cancelling… + Error \ No newline at end of file