Cancelling downloads

This commit is contained in:
Koitharu
2020-03-11 20:37:58 +02:00
parent e47d494b1c
commit e123399911
10 changed files with 143 additions and 31 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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())

View File

@@ -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
)
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>