Cancelling downloads
This commit is contained in:
@@ -31,11 +31,11 @@ class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaReposit
|
||||
): List<Manga> {
|
||||
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<MangaPage> {
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<OkHttpClient>()
|
||||
private val cache by inject<PagesCache>()
|
||||
private val jobs = HashMap<Int, Job>()
|
||||
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<Manga>(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<Manga>(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<Long>?) {
|
||||
val destination = getExternalFilesDir("manga")!!
|
||||
notification.fillFrom(manga)
|
||||
startForeground(DownloadNotification.NOTIFICATION_ID, notification())
|
||||
launch(Dispatchers.IO) {
|
||||
private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, 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<Long>? = 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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
|
||||
@@ -93,4 +93,6 @@
|
||||
<string name="warning">Предупреждение</string>
|
||||
<string name="network_consumption_warning">Данная операция может привести к большому расходу траффика</string>
|
||||
<string name="dont_ask_again">Больше не спрашивать</string>
|
||||
<string name="cancelling_">Отмена…</string>
|
||||
<string name="error">Ошибка</string>
|
||||
</resources>
|
||||
@@ -94,4 +94,6 @@
|
||||
<string name="warning">Warning</string>
|
||||
<string name="network_consumption_warning">This operation may consume a lot of network traffic</string>
|
||||
<string name="dont_ask_again">Don`t ask again</string>
|
||||
<string name="cancelling_">Cancelling…</string>
|
||||
<string name="error">Error</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user