Cancelling downloads

pull/1/head
Koitharu 6 years ago
parent e47d494b1c
commit e123399911

@ -31,11 +31,11 @@ class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaReposit
): List<Manga> { ): List<Manga> {
val files = context.getExternalFilesDirs("manga") val files = context.getExternalFilesDirs("manga")
.flatMap { x -> x?.listFiles(CbzFilter())?.toList().orEmpty() } .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) { override suspend fun getDetails(manga: Manga) = if (manga.chapters == null) {
getDetails(Uri.parse(manga.url).toFile()) getFromFile(Uri.parse(manga.url).toFile())
} else manga } else manga
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { 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 zip = ZipFile(file)
val fileUri = file.toUri().toString() val fileUri = file.toUri().toString()
val entry = zip.getEntry(MangaZip.INDEX_ENTRY) 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) = private fun zipUri(file: File, entryName: String) =
Uri.fromParts("cbz", file.path, entryName).toString() 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.get
import org.koin.core.inject import org.koin.core.inject
import org.koitharu.kotatsu.core.model.MangaSource 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.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@ -21,6 +22,8 @@ object MangaProviderFactory : KoinComponent {
} }
} }
fun createLocal() = LocalMangaRepository(loaderContext)
fun create(source: MangaSource): MangaRepository { fun create(source: MangaSource): MangaRepository {
val constructor = source.cls.getConstructor(MangaLoaderContext::class.java) val constructor = source.cls.getConstructor(MangaLoaderContext::class.java)
return constructor.newInstance(loaderContext) return constructor.newInstance(loaderContext)

@ -12,9 +12,9 @@ import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
@WorkerThread @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") ?: throw RuntimeException("Cannot create temporary directory")
private val index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText()) 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.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@ -10,6 +11,9 @@ import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga 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 import kotlin.math.roundToInt
class DownloadNotification(private val context: Context) { class DownloadNotification(private val context: Context) {
@ -37,6 +41,33 @@ class DownloadNotification(private val context: Context) {
builder.setProgress(1, 0, true) builder.setProgress(1, 0, true)
builder.setSmallIcon(android.R.drawable.stat_sys_download) builder.setSmallIcon(android.R.drawable.stat_sys_download)
builder.setLargeIcon(null) 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?) { fun setLargeIcon(icon: Drawable?) {
@ -57,16 +88,27 @@ class DownloadNotification(private val context: Context) {
builder.setContentText(context.getString(R.string.processing_)) builder.setContentText(context.getString(R.string.processing_))
} }
fun setDone() { fun setDone(manga: Manga) {
builder.setProgress(0, 0, false) builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.download_complete)) builder.setContentText(context.getString(R.string.download_complete))
builder.setContentIntent(createIntent(context, manga))
builder.setSmallIcon(android.R.drawable.stat_sys_download_done) 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) { fun update(id: Int = NOTIFICATION_ID) {
manager.notify(id, builder.build()) manager.notify(id, builder.build())
} }
fun dismiss(id: Int = NOTIFICATION_ID) {
manager.cancel(id)
}
operator fun invoke(): Notification = builder.build() operator fun invoke(): Notification = builder.build()
companion object { companion object {
@ -75,5 +117,13 @@ class DownloadNotification(private val context: Context) {
const val CHANNEL_ID = "download" const val CHANNEL_ID = "download"
private const val PROGRESS_STEP = 20 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.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil import coil.Coil
import coil.api.get import coil.api.get
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.inject import org.koin.core.inject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.local.PagesCache import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@ -37,6 +35,8 @@ class DownloadService : BaseService() {
private val okHttp by inject<OkHttpClient>() private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>() private val cache by inject<PagesCache>()
private val jobs = HashMap<Int, Job>()
private val mutex = Mutex()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -44,21 +44,35 @@ class DownloadService : BaseService() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val manga = intent?.getParcelableExtra<Manga>(EXTRA_MANGA) when (intent?.action) {
val chapters = intent?.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ACTION_DOWNLOAD_START -> {
val manga = intent.getParcelableExtra<Manga>(EXTRA_MANGA)
val chapters = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet()
if (manga != null) { if (manga != null) {
downloadManga(manga, chapters) jobs[startId] = downloadManga(manga, chapters, startId)
} else { } else {
stopSelf(startId) 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 return START_NOT_STICKY
} }
private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?) { private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job {
val destination = getExternalFilesDir("manga")!! return launch(Dispatchers.IO) {
mutex.lock()
withContext(Dispatchers.Main) {
notification.fillFrom(manga) notification.fillFrom(manga)
notification.setCancelId(startId)
startForeground(DownloadNotification.NOTIFICATION_ID, notification()) startForeground(DownloadNotification.NOTIFICATION_ID, notification())
launch(Dispatchers.IO) { }
val destination = getExternalFilesDir("manga")!!
var output: MangaZip? = null var output: MangaZip? = null
try { try {
val repo = MangaProviderFactory.create(manga.source) val repo = MangaProviderFactory.create(manga.source)
@ -106,21 +120,41 @@ class DownloadService : BaseService() {
} }
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
notification.setCancelId(0)
notification.setPostProcessing() notification.setPostProcessing()
notification.update() notification.update()
} }
output.compress() output.compress()
val result = MangaProviderFactory.createLocal().getFromFile(output.file)
withContext(Dispatchers.Main) { 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) notification.update(manga.id.toInt().absoluteValue)
} }
} finally { } finally {
withContext(NonCancellable) { withContext(NonCancellable) {
jobs.remove(startId)
output?.cleanup() output?.cleanup()
destination.sub("page.tmp").delete() destination.sub("page.tmp").delete()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
stopForeground(true) stopForeground(true)
notification.dismiss()
stopSelf(startId)
} }
mutex.unlock()
} }
} }
} }
@ -145,12 +179,19 @@ class DownloadService : BaseService() {
companion object { 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_MANGA = "manga"
private const val EXTRA_CHAPTERS_IDS = "chapters_ids" 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) { fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>? = null) {
confirmDataTransfer(context) { confirmDataTransfer(context) {
val intent = Intent(context, DownloadService::class.java) val intent = Intent(context, DownloadService::class.java)
intent.action = ACTION_DOWNLOAD_START
intent.putExtra(EXTRA_MANGA, manga) intent.putExtra(EXTRA_MANGA, manga)
if (chaptersIds != null) { if (chaptersIds != null) {
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray()) 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) { private fun confirmDataTransfer(context: Context, callback: () -> Unit) {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val settings = AppSettings(context) 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) { return buildString(length + 5) {
for (c in this@transliterate) { for (c in this@transliterate) {
val p = cyr.binarySearch(c) val p = cyr.binarySearch(c.toLowerCase())
if (p in lat.indices) { if (p in lat.indices) {
append(lat[p]) append(lat[p])
} else if (!skipMissing) { } else if (!skipMissing) {

@ -3,8 +3,8 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?attr/colorControlNormal" android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0" android:viewportWidth="24"
android:viewportHeight="24.0"> android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" 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" /> 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="warning">Предупреждение</string>
<string name="network_consumption_warning">Данная операция может привести к большому расходу траффика</string> <string name="network_consumption_warning">Данная операция может привести к большому расходу траффика</string>
<string name="dont_ask_again">Больше не спрашивать</string> <string name="dont_ask_again">Больше не спрашивать</string>
<string name="cancelling_">Отмена…</string>
<string name="error">Ошибка</string>
</resources> </resources>

@ -94,4 +94,6 @@
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="network_consumption_warning">This operation may consume a lot of network traffic</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="dont_ask_again">Don`t ask again</string>
<string name="cancelling_">Cancelling…</string>
<string name="error">Error</string>
</resources> </resources>
Loading…
Cancel
Save