From c445ea90bad53e4ce0ec645590166448815a5222 Mon Sep 17 00:00:00 2001 From: Bram van de Kerkhof Date: Fri, 20 Jan 2017 21:18:15 +0100 Subject: [PATCH] Notification Improvements (#594) * Download notifier improvements * Notification improvements Added a Notification Service. Added a Notification Activity Handler. * Removed service. Everything is now managed by single broadcast * Fixed some flags * Fixed ReaderActivity call * Code review * Added Handler. Removed dismiss onDestroy --- app/src/main/AndroidManifest.xml | 69 ++--- .../data/download/DownloadManager.kt | 16 +- .../data/download/DownloadNotifier.kt | 109 +++++-- .../tachiyomi/data/download/Downloader.kt | 46 ++- .../data/library/LibraryUpdateService.kt | 32 +- .../data/notification/NotificationHandler.kt | 57 ++++ .../data/notification/NotificationReceiver.kt | 277 ++++++++++++++++++ .../data/updater/UpdateCheckerJob.kt | 8 +- .../data/updater/UpdateDownloaderReceiver.kt | 144 +++++++++ .../data/updater/UpdateDownloaderService.kt | 246 +++++++++------- .../updater/UpdateNotificationReceiver.kt | 70 ----- ...ownloadFragment.kt => DownloadActivity.kt} | 71 ++--- .../ui/download/DownloadPresenter.kt | 15 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 4 +- .../tachiyomi/ui/reader/ReaderPresenter.kt | 4 +- .../ImageNotifier.kt => SaveImageNotifier.kt} | 14 +- .../notification/ImageNotificationReceiver.kt | 84 ------ .../tachiyomi/util/ContextExtensions.kt | 41 +++ .../kanade/tachiyomi/util/FileExtensions.kt | 33 +++ .../ic_av_pause_grey_24dp_img.png | Bin 0 -> 331 bytes .../ic_av_play_arrow_grey_img.png | Bin 0 -> 631 bytes .../ic_av_pause_grey_24dp_img.png | Bin 0 -> 363 bytes .../ic_av_play_arrow_grey_img.png | Bin 0 -> 421 bytes .../ic_av_pause_grey_24dp_img.png | Bin 0 -> 536 bytes .../ic_av_play_arrow_grey_img.png | Bin 0 -> 780 bytes .../ic_av_pause_grey_24dp_img.png | Bin 0 -> 699 bytes .../ic_av_play_arrow_grey_img.png | Bin 0 -> 1308 bytes .../ic_av_pause_grey_24dp_img.png | Bin 0 -> 850 bytes .../ic_av_play_arrow_grey_img.png | Bin 0 -> 1862 bytes .../res/layout/activity_download_manager.xml | 42 +++ app/src/main/res/menu/menu_navigation.xml | 3 +- app/src/main/res/values/strings.xml | 2 + 32 files changed, 993 insertions(+), 394 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/download/{DownloadFragment.kt => DownloadActivity.kt} (81%) rename app/src/main/java/eu/kanade/tachiyomi/ui/reader/{notification/ImageNotifier.kt => SaveImageNotifier.kt} (82%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotificationReceiver.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt create mode 100644 app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png create mode 100644 app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png create mode 100644 app/src/main/res/drawable-mdpi/ic_av_pause_grey_24dp_img.png create mode 100644 app/src/main/res/drawable-mdpi/ic_av_play_arrow_grey_img.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_av_pause_grey_24dp_img.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_av_play_arrow_grey_img.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_av_pause_grey_24dp_img.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow_grey_img.png create mode 100644 app/src/main/res/layout/activity_download_manager.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d21a2262..9813e42df 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,16 +1,17 @@ - - + - - + + - + android:theme="@style/Theme.Tachiyomi"> + @@ -31,40 +31,40 @@ - + android:exported="true" + android:parentActivityName=".ui.main.MainActivity" /> - + android:theme="@style/Theme.Reader" /> - + android:parentActivityName=".ui.main.MainActivity" /> - + android:parentActivityName=".ui.main.MainActivity" /> - + android:theme="@style/FilePickerTheme" /> + + + + android:resource="@xml/provider_paths" /> - + - + - + - - - - - - - + + 1. */ var multipleDownloadThreads = false + /** + * Updated when error is thrown + */ + var errorThrown = false + + /** + * Updated when only single page is downloaded + */ + var isSingleChapter = false + + /** + * Updated when paused + */ + var paused = false + /** * Shows a notification from this builder. * @@ -48,6 +72,14 @@ internal class DownloadNotifier(private val context: Context) { context.notificationManager.notify(id, build()) } + /** + * Clear old actions if they exist. + */ + private fun clearActions() = with(notification) { + if (!mActions.isEmpty()) + mActions.clear() + } + /** * Dismiss the downloader's notification. Downloader error notifications use a different id, so * those can only be dismissed by the user. @@ -88,24 +120,15 @@ internal class DownloadNotifier(private val context: Context) { * @param queue the queue containing downloads. */ private fun doOnProgressChange(download: Download?, queue: DownloadQueue) { - // Check if download is completed - if (multipleDownloadThreads) { - if (queue.isEmpty()) { - onChapterCompleted(null) - return - } - } else { - if (download != null && download.pages!!.size == download.downloadedImages) { - onChapterCompleted(download) - return - } - } - // Create notification with(notification) { - // Check if icon needs refresh + // Check if first call. if (!isDownloading) { setSmallIcon(android.R.drawable.stat_sys_download) + setAutoCancel(false) + clearActions() + // Open download manager when clicked + setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) isDownloading = true } @@ -121,7 +144,9 @@ internal class DownloadNotifier(private val context: Context) { setProgress(initialQueueSize, initialQueueSize - queue.size, false) } else { download?.let { - setContentTitle(it.chapter.name.chop(30)) + val title = it.manga.title.chop(15) + val chapter = download.chapter.name.replaceFirst("$title[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") + setContentTitle("$title - $chapter".chop(30)) setContentText(context.getString(R.string.chapter_downloading_progress) .format(it.downloadedImages, it.pages!!.size)) setProgress(it.pages!!.size, it.downloadedImages, false) @@ -133,17 +158,57 @@ internal class DownloadNotifier(private val context: Context) { notification.show() } + /** + * Show notification when download is paused. + */ + fun onDownloadPaused() { + with(notification) { + setContentTitle(context.getString(R.string.chapter_paused)) + setContentText(context.getString(R.string.download_notifier_download_paused)) + setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img) + setAutoCancel(false) + setProgress(0, 0, false) + clearActions() + // Open download manager when clicked + setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) + // Resume action + addAction(R.drawable.ic_av_play_arrow_grey_img, + context.getString(R.string.action_resume), + NotificationReceiver.resumeDownloadsPendingBroadcast(context)) + //Clear action + addAction(R.drawable.ic_clear_grey_24dp_img, + context.getString(R.string.action_clear), + NotificationReceiver.clearDownloadsPendingBroadcast(context)) + } + + // Show notification. + notification.show() + + // Reset initial values + isDownloading = false + initialQueueSize = 0 + } + /** * Called when chapter is downloaded. * * @param download download object containing download information. */ - private fun onChapterCompleted(download: Download?) { + fun onDownloadCompleted(download: Download, queue: DownloadQueue) { + // Check if last download + if (!queue.isEmpty()) { + return + } // Create notification. with(notification) { - setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name)) + val title = download.manga.title.chop(15) + val chapter = download.chapter.name.replaceFirst("$title[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") + setContentTitle("$title - $chapter".chop(30)) setContentText(context.getString(R.string.update_check_notification_download_complete)) setSmallIcon(android.R.drawable.stat_sys_download_done) + setAutoCancel(true) + clearActions() + setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter)) setProgress(0, 0, false) } @@ -165,9 +230,15 @@ internal class DownloadNotifier(private val context: Context) { setContentTitle(context.getString(R.string.download_notifier_downloader_title)) setContentText(reason) setSmallIcon(android.R.drawable.stat_sys_warning) + setAutoCancel(true) + clearActions() + setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setProgress(0, 0, false) } notification.show() + + // Reset download information + isDownloading = false } /** @@ -183,11 +254,15 @@ internal class DownloadNotifier(private val context: Context) { setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title)) setContentText(error ?: context.getString(R.string.download_notifier_unkown_error)) setSmallIcon(android.R.drawable.stat_sys_warning) + clearActions() + setAutoCancel(false) + setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setProgress(0, 0, false) } notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID) // Reset download information + errorThrown = true isDownloading = false } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index b9f8b49d6..537a020ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -133,15 +133,42 @@ class Downloader(private val context: Context, private val provider: DownloadPro if (reason != null) { notifier.onWarning(reason) } else { - notifier.dismiss() + if (notifier.paused) { + notifier.paused = false + notifier.onDownloadPaused() + } else if (notifier.isSingleChapter && !notifier.errorThrown) { + notifier.isSingleChapter = false + } else { + notifier.dismiss() + } } } /** - * Removes everything from the queue. + * Pauses the downloader */ - fun clearQueue() { + fun pause() { destroySubscriptions() + queue + .filter { it.status == Download.DOWNLOADING } + .forEach { it.status = Download.QUEUE } + notifier.paused = true + } + + /** + * Removes everything from the queue. + * + * @param isNotification value that determines if status is set (needed for view updates) + */ + fun clearQueue(isNotification: Boolean = false) { + destroySubscriptions() + + //Needed to update the chapter view + if (isNotification) { + queue + .filter { it.status == Download.QUEUE } + .forEach { it.status = Download.NOT_DOWNLOADED } + } queue.clear() notifier.dismiss() } @@ -313,7 +340,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro tmpFile?.delete() // Try to find the image file. - val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.")} + val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } // If the image is already downloaded, do nothing. Otherwise download from network val pageObservable = if (imageFile != null) @@ -377,10 +404,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro private fun getImageExtension(response: Response, file: UniFile): String { // Read content type if available. val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" } - // Else guess from the uri. - ?: context.contentResolver.getType(file.uri) - // Else read magic numbers. - ?: file.openInputStream().buffered().use { + // Else guess from the uri. + ?: context.contentResolver.getType(file.uri) + // Else read magic numbers. + ?: file.openInputStream().buffered().use { URLConnection.guessContentTypeFromStream(it) } @@ -421,6 +448,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro notifier.onProgressChange(queue) } if (areAllDownloadsFinished()) { + if (notifier.isSingleChapter && !notifier.errorThrown) { + notifier.onDownloadCompleted(download, queue) + } DownloadService.stop(context) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 1fc874746..620dac81d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.library import android.app.PendingIntent import android.app.Service -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.graphics.BitmapFactory @@ -18,6 +17,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start +import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.source.SourceManager @@ -69,6 +69,11 @@ class LibraryUpdateService : Service() { */ private var subscription: Subscription? = null + /** + * Pending intent of action that cancels the library update + */ + private val cancelPendingIntent by lazy {NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)} + /** * Id of the library update notification. */ @@ -236,13 +241,10 @@ class LibraryUpdateService : Service() { val newUpdates = ArrayList() val failedUpdates = ArrayList() - val cancelIntent = PendingIntent.getBroadcast(this, 0, - Intent(this, CancelUpdateReceiver::class.java), 0) - // Emit each manga and update it sequentially. return Observable.from(mangaToUpdate) // Notify manga that will update. - .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) } + .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) } // Update the chapters of the manga. .concatMap { manga -> updateManga(manga) @@ -316,13 +318,10 @@ class LibraryUpdateService : Service() { // Initialize the variables holding the progress of the updates. val count = AtomicInteger(0) - val cancelIntent = PendingIntent.getBroadcast(this, 0, - Intent(this, CancelUpdateReceiver::class.java), 0) - // Emit each manga and update it sequentially. return Observable.from(mangaToUpdate) // Notify manga that will update. - .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) } + .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) } // Update the details of the manga. .concatMap { manga -> val source = sourceManager.get(manga.source) as? OnlineSource @@ -459,19 +458,4 @@ class LibraryUpdateService : Service() { intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - - /** - * Class that stops updating the library. - */ - class CancelUpdateReceiver : BroadcastReceiver() { - /** - * Method called when user wants a library update. - * @param context the application context. - * @param intent the intent received. - */ - override fun onReceive(context: Context, intent: Intent) { - LibraryUpdateService.stop(context) - context.notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_ID) - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt new file mode 100644 index 000000000..ae160492e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt @@ -0,0 +1,57 @@ +package eu.kanade.tachiyomi.data.notification + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.support.v4.content.FileProvider +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.ui.download.DownloadActivity +import eu.kanade.tachiyomi.util.getUriCompat +import java.io.File + +/** + * Class that manages [PendingIntent] of activity's + */ +object NotificationHandler { + /** + * Returns [PendingIntent] that starts a download activity. + * + * @param context context of application + */ + internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent { + val intent = Intent(context, DownloadActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + } + return PendingIntent.getActivity(context, 0, intent, 0) + } + + /** + * Returns [PendingIntent] that starts a gallery activity + * + * @param context context of application + * @param file file containing image + */ + internal fun openImagePendingActivity(context: Context, file: File): PendingIntent { + val intent = Intent(Intent.ACTION_VIEW).apply { + val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + setDataAndType(uri, "image/*") + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + /** + * Returns [PendingIntent] that prompts user with apk install intent + * + * @param context context + * @param file file of apk that is installed + */ + fun installApkPendingActivity(context: Context, file: File): PendingIntent { + val intent = Intent(Intent.ACTION_VIEW).apply { + val uri = file.getUriCompat(context) + setDataAndType(uri, "application/vnd.android.package-archive") + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + } + return PendingIntent.getActivity(context, 0, intent, 0) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt new file mode 100644 index 000000000..297024479 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -0,0 +1,277 @@ +package eu.kanade.tachiyomi.data.notification + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Handler +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.library.LibraryUpdateService +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.deleteIfExists +import eu.kanade.tachiyomi.util.getUriCompat +import eu.kanade.tachiyomi.util.notificationManager +import eu.kanade.tachiyomi.util.toast +import uy.kohesive.injekt.injectLazy +import java.io.File +import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID + +/** + * Global [BroadcastReceiver] that runs on UI thread + * Pending Broadcasts should be made from here. + * NOTE: Use local broadcasts if possible. + */ +class NotificationReceiver : BroadcastReceiver() { + /** + * Download manager. + */ + private val downloadManager: DownloadManager by injectLazy() + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + // Dismiss notification + ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) + // Resume the download service + ACTION_RESUME_DOWNLOADS -> DownloadService.start(context) + // Clear the download queue + ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) + // Launch share activity and dismiss notification + ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), + intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) + // Delete image from path and dismiss notification + ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), + intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) + // Cancel library update and dismiss notification + ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, + intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) + // Open reader activity + ACTION_OPEN_CHAPTER -> { + openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), + intent.getLongExtra(EXTRA_CHAPTER_ID, -1)) + } + } + } + + /** + * Dismiss the notification + * + * @param notificationId the id of the notification + */ + private fun dismissNotification(context: Context, notificationId: Int) { + context.notificationManager.cancel(notificationId) + } + + /** + * Called to start share intent to share image + * + * @param context context of application + * @param path path of file + * @param notificationId id of notification + */ + private fun shareImage(context: Context, path: String, notificationId: Int) { + // Create intent + val intent = Intent(Intent.ACTION_SEND).apply { + val uri = File(path).getUriCompat(context) + putExtra(Intent.EXTRA_STREAM, uri) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + type = "image/*" + } + // Dismiss notification + dismissNotification(context, notificationId) + // Launch share activity + context.startActivity(intent) + } + + /** + * Starts reader activity + * + * @param context context of application + * @param mangaId id of manga + * @param chapterId id of chapter + */ + internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) { + val db = DatabaseHelper(context) + val manga = db.getManga(mangaId).executeAsBlocking() + val chapter = db.getChapter(chapterId).executeAsBlocking() + + if (manga != null && chapter != null) { + val intent = ReaderActivity.newIntent(context, manga, chapter).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(intent) + } else { + context.toast(context.getString(R.string.chapter_error)) + } + } + + /** + * Called to delete image + * + * @param path path of file + * @param notificationId id of notification + */ + private fun deleteImage(context: Context, path: String, notificationId: Int) { + // Dismiss notification + dismissNotification(context, notificationId) + + // Delete file + File(path).deleteIfExists() + } + + /** + * Method called when user wants to stop a library update + * + * @param context context of application + * @param notificationId id of notification + */ + private fun cancelLibraryUpdate(context: Context, notificationId: Int) { + LibraryUpdateService.stop(context) + Handler().post { dismissNotification(context, notificationId) } + } + + companion object { + private const val NAME = "NotificationReceiver" + + // Called to launch share intent. + private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE" + + // Called to delete image. + private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE" + + // Called to cancel library update. + private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE" + + // Called to open chapter + private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER" + + // Value containing file location. + private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION" + + // Called to resume downloads. + private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS" + + // Called to clear downloads. + private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS" + + // Called to dismiss notification. + private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION" + + // Value containing notification id. + private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID" + + // Value containing manga id. + private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID" + + // Value containing chapter id. + private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID" + + /** + * Returns a [PendingIntent] that resumes the download of a chapter + * + * @param context context of application + * @return [PendingIntent] + */ + internal fun resumeDownloadsPendingBroadcast(context: Context): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_RESUME_DOWNLOADS + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + } + + /** + * Returns a [PendingIntent] that clears the download queue + * + * @param context context of application + * @return [PendingIntent] + */ + internal fun clearDownloadsPendingBroadcast(context: Context): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CLEAR_DOWNLOADS + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + } + + /** + * Returns [PendingIntent] that starts a service which dismissed the notification + * + * @param context context of application + * @param notificationId id of notification + * @return [PendingIntent] + */ + internal fun dismissNotificationPendingBroadcast(context: Context, notificationId: Int): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_DISMISS_NOTIFICATION + putExtra(EXTRA_NOTIFICATION_ID, notificationId) + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + } + + /** + * Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity + * + * @param context context of application + * @param path location path of file + * @param notificationId id of notification + * @return [PendingIntent] + */ + internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_SHARE_IMAGE + putExtra(EXTRA_FILE_LOCATION, path) + putExtra(EXTRA_NOTIFICATION_ID, notificationId) + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + } + + /** + * Returns [PendingIntent] that starts a service which removes an image from disk + * + * @param context context of application + * @param path location path of file + * @param notificationId id of notification + * @return [PendingIntent] + */ + internal fun deleteImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_DELETE_IMAGE + putExtra(EXTRA_FILE_LOCATION, path) + putExtra(EXTRA_NOTIFICATION_ID, notificationId) + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + } + + /** + * Returns [PendingIntent] that start a reader activity containing chapter. + * + * @param context context of application + * @param manga manga of chapter + * @param chapter chapter that needs to be opened + */ + internal fun openChapterPendingBroadcast(context: Context, manga: Manga, chapter: Chapter): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_OPEN_CHAPTER + putExtra(EXTRA_MANGA_ID, manga.id) + putExtra(EXTRA_CHAPTER_ID, chapter.id) + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + } + + /** + * Returns [PendingIntent] that starts a service which stops the library update + * + * @param context context of application + * @return [PendingIntent] + */ + internal fun cancelLibraryUpdatePendingBroadcast(context: Context): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CANCEL_LIBRARY_UPDATE + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt index 7f444ea69..e3dcb8b84 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateCheckerJob.kt @@ -1,5 +1,7 @@ package eu.kanade.tachiyomi.data.updater +import android.app.PendingIntent +import android.content.Intent import android.support.v4.app.NotificationCompat import com.evernote.android.job.Job import com.evernote.android.job.JobManager @@ -17,6 +19,10 @@ class UpdateCheckerJob : Job() { if (result is GithubUpdateResult.NewUpdate) { val url = result.release.downloadLink + val intent = Intent(context, UpdateDownloaderService::class.java).apply { + putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) + } + NotificationCompat.Builder(context).update { setContentTitle(context.getString(R.string.app_name)) setContentText(context.getString(R.string.update_check_notification_update_available)) @@ -24,7 +30,7 @@ class UpdateCheckerJob : Job() { // Download action addAction(android.R.drawable.stat_sys_download_done, context.getString(R.string.action_download), - UpdateNotificationReceiver.downloadApkIntent(context, url)) + PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) } } Job.Result.SUCCESS diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt new file mode 100644 index 000000000..5174c204f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderReceiver.kt @@ -0,0 +1,144 @@ +package eu.kanade.tachiyomi.data.updater + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.support.v4.app.NotificationCompat +import eu.kanade.tachiyomi.Constants +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.NotificationHandler +import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.util.notificationManager +import java.io.File +import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID + +/** + * Local [BroadcastReceiver] that runs on UI thread + * Notification calls from [UpdateDownloaderService] should be made from here. + */ +internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() { + + companion object { + private const val NAME = "UpdateDownloaderReceiver" + + // Called to show initial notification. + internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL" + + // Called to show progress notification. + internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS" + + // Called to show install notification. + internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL" + + // Called to show error notification + internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR" + + // Value containing action of BroadcastReceiver + internal const val EXTRA_ACTION = "$ID.$NAME.ACTION" + + // Value containing progress + internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS" + + // Value containing apk path + internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH" + + // Value containing apk url + internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL" + } + + /** + * Notification shown to user + */ + private val notification = NotificationCompat.Builder(context) + + override fun onReceive(context: Context, intent: Intent) { + when (intent.getStringExtra(EXTRA_ACTION)) { + NOTIFICATION_UPDATER_INITIAL -> basicNotification() + NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0)) + NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH)) + NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL)) + } + } + + /** + * Called to show basic notification + */ + private fun basicNotification() { + // Create notification + with(notification) { + setContentTitle(context.getString(R.string.app_name)) + setContentText(context.getString(R.string.update_check_notification_download_in_progress)) + setSmallIcon(android.R.drawable.stat_sys_download) + setOngoing(true) + } + notification.show() + } + + /** + * Called to show progress notification + * + * @param progress progress of download + */ + private fun updateProgress(progress: Int) { + with(notification) { + setProgress(100, progress, false) + } + notification.show() + } + + /** + * Called to show install notification + * + * @param path path of file + */ + private fun installNotification(path: String) { + // Prompt the user to install the new update. + with(notification) { + setContentText(context.getString(R.string.update_check_notification_download_complete)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + setProgress(0, 0, false) + // Install action + setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path))) + addAction(R.drawable.ic_system_update_grey_24dp_img, + context.getString(R.string.action_install), + NotificationHandler.installApkPendingActivity(context, File(path))) + // Cancel action + addAction(R.drawable.ic_clear_grey_24dp_img, + context.getString(R.string.action_cancel), + NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) + } + notification.show() + } + + /** + * Called to show error notification + * + * @param url url of apk + */ + private fun errorNotification(url: String) { + // Prompt the user to retry the download. + with(notification) { + setContentText(context.getString(R.string.update_check_notification_download_error)) + setSmallIcon(android.R.drawable.stat_sys_warning) + setProgress(0, 0, false) + // Retry action + addAction(R.drawable.ic_refresh_grey_24dp_img, + context.getString(R.string.action_retry), + UpdateDownloaderService.downloadApkPendingService(context, url)) + // Cancel action + addAction(R.drawable.ic_clear_grey_24dp_img, + context.getString(R.string.action_cancel), + NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) + } + notification.show() + } + + /** + * Shows a notification from this builder. + * + * @param id the id of the notification. + */ + private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) { + context.notificationManager.notify(id, build()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt index 028150b10..8d7e68216 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderService.kt @@ -1,28 +1,160 @@ package eu.kanade.tachiyomi.data.updater import android.app.IntentService +import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.support.v4.app.NotificationCompat -import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID -import eu.kanade.tachiyomi.R +import android.content.IntentFilter +import android.os.Build +import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.ProgressListener import eu.kanade.tachiyomi.data.network.newCallWithProgress -import eu.kanade.tachiyomi.util.notificationManager +import eu.kanade.tachiyomi.util.registerLocalReceiver import eu.kanade.tachiyomi.util.saveTo +import eu.kanade.tachiyomi.util.sendLocalBroadcastSync +import eu.kanade.tachiyomi.util.unregisterLocalReceiver import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) { + /** + * Network helper + */ + private val network: NetworkHelper by injectLazy() + + /** + * Local [BroadcastReceiver] that runs on UI thread + */ + private val updaterNotificationReceiver = UpdateDownloaderReceiver(this) + + + override fun onCreate() { + super.onCreate() + // Register receiver + registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME)) + } + + override fun onDestroy() { + // Unregister receiver + unregisterLocalReceiver(updaterNotificationReceiver) + super.onDestroy() + } + + override fun onHandleIntent(intent: Intent?) { + if (intent == null) return + + val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return + downloadApk(url) + } + + /** + * Called to start downloading apk of new update + * + * @param url url location of file + */ + fun downloadApk(url: String) { + // Show notification download starting. + sendInitialBroadcast() + // Progress of the download + var savedProgress = 0 + + val progressListener = object : ProgressListener { + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { + val progress = (100 * bytesRead / contentLength).toInt() + if (progress > savedProgress) { + savedProgress = progress + sendProgressBroadcast(progress) + } + } + } + + try { + // Download the new update. + val response = network.client.newCallWithProgress(GET(url), progressListener).execute() + + // File where the apk will be saved. + val apkFile = File(externalCacheDir, "update.apk") + + if (response.isSuccessful) { + response.body().source().saveTo(apkFile) + } else { + response.close() + throw Exception("Unsuccessful response") + } + sendInstallBroadcast(apkFile.absolutePath) + } catch (error: Exception) { + Timber.e(error) + sendErrorBroadcast(url) + } + } + + /** + * Show notification download starting. + */ + private fun sendInitialBroadcast() { + val intent = Intent(INTENT_FILTER_NAME).apply { + putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL) + } + sendLocalBroadcastSync(intent) + } + + /** + * Show notification progress changed + * + * @param progress progress of download + */ + private fun sendProgressBroadcast(progress: Int) { + val intent = Intent(INTENT_FILTER_NAME).apply { + putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS) + putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress) + } + // Prevents not showing of install notification TODO weird Android N bug. Find out what goes wrong + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || progress <= 95) { + // Show download progress notification. + sendLocalBroadcastSync(intent) + } + } + + /** + * Show install notification. + * + * @param path location of file + */ + private fun sendInstallBroadcast(path: String){ + val intent = Intent(INTENT_FILTER_NAME).apply { + putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL) + putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path) + } + sendLocalBroadcastSync(intent) + } + + /** + * Show error notification. + * + * @param url url of file + */ + private fun sendErrorBroadcast(url: String){ + val intent = Intent(INTENT_FILTER_NAME).apply { + putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR) + putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url) + } + sendLocalBroadcastSync(intent) + } companion object { + /** + * Name of Local BroadCastReceiver. + */ + private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name + /** * Download url. */ - const val EXTRA_DOWNLOAD_URL = "eu.kanade.APP_DOWNLOAD_URL" + internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.DOWNLOAD_URL" /** * Downloads a new update and let the user install the new version from a notification. @@ -35,102 +167,20 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav } context.startService(intent) } - } - /** - * Network helper - */ - private val network: NetworkHelper by injectLazy() - - override fun onHandleIntent(intent: Intent?) { - if (intent == null) return - - val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return - downloadApk(url) - } - - fun downloadApk(url: String) { - val progressNotification = NotificationCompat.Builder(this) - - progressNotification.update { - setContentTitle(getString(R.string.app_name)) - setContentText(getString(R.string.update_check_notification_download_in_progress)) - setSmallIcon(android.R.drawable.stat_sys_download) - setOngoing(true) - } - - // Progress of the download - var savedProgress = 0 - - val progressListener = object : ProgressListener { - override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { - val progress = (100 * bytesRead / contentLength).toInt() - if (progress > savedProgress) { - savedProgress = progress - - progressNotification.update { setProgress(100, progress, false) } - } - } - } - - // Reference the context for later usage inside apply blocks. - val ctx = this - - try { - // Download the new update. - val response = network.client.newCallWithProgress(GET(url), progressListener).execute() - - // File where the apk will be saved - val apkFile = File(externalCacheDir, "update.apk") - - if (response.isSuccessful) { - response.body().source().saveTo(apkFile) - } else { - response.close() - throw Exception("Unsuccessful response") - } - - val installIntent = UpdateNotificationReceiver.installApkIntent(ctx, apkFile) - - // Prompt the user to install the new update. - NotificationCompat.Builder(this).update { - setContentTitle(getString(R.string.app_name)) - setContentText(getString(R.string.update_check_notification_download_complete)) - setSmallIcon(android.R.drawable.stat_sys_download_done) - // Install action - setContentIntent(installIntent) - addAction(R.drawable.ic_system_update_grey_24dp_img, - getString(R.string.action_install), - installIntent) - // Cancel action - addAction(R.drawable.ic_clear_grey_24dp_img, - getString(R.string.action_cancel), - UpdateNotificationReceiver.cancelNotificationIntent(ctx)) - } - - } catch (error: Exception) { - Timber.e(error) - - // Prompt the user to retry the download. - NotificationCompat.Builder(this).update { - setContentTitle(getString(R.string.app_name)) - setContentText(getString(R.string.update_check_notification_download_error)) - setSmallIcon(android.R.drawable.stat_sys_download_done) - // Retry action - addAction(R.drawable.ic_refresh_grey_24dp_img, - getString(R.string.action_retry), - UpdateNotificationReceiver.downloadApkIntent(ctx, url)) - // Cancel action - addAction(R.drawable.ic_clear_grey_24dp_img, - getString(R.string.action_cancel), - UpdateNotificationReceiver.cancelNotificationIntent(ctx)) + /** + * Returns [PendingIntent] that starts a service which downloads the apk specified in url. + * + * @param url the url to the new update. + * @return [PendingIntent] + */ + internal fun downloadApkPendingService(context: Context, url: String): PendingIntent { + val intent = Intent(context, UpdateDownloaderService::class.java).apply { + putExtra(EXTRA_DOWNLOAD_URL, url) } + return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } } +} - fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { - block() - notificationManager.notify(NOTIFICATION_UPDATER_ID, build()) - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt deleted file mode 100644 index 74c5c9e2c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateNotificationReceiver.kt +++ /dev/null @@ -1,70 +0,0 @@ -package eu.kanade.tachiyomi.data.updater - -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.support.v4.content.FileProvider -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID -import eu.kanade.tachiyomi.util.notificationManager -import java.io.File - -class UpdateNotificationReceiver : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - ACTION_CANCEL_NOTIFICATION -> cancelNotification(context) - } - } - - companion object { - // Cancel notification action - const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION" - - fun cancelNotificationIntent(context: Context): PendingIntent { - val intent = Intent(context, UpdateNotificationReceiver::class.java).apply { - action = ACTION_CANCEL_NOTIFICATION - } - return PendingIntent.getBroadcast(context, 0, intent, 0) - } - - /** - * Prompt user with apk install intent - * - * @param context context - * @param file file of apk that is installed - */ - fun installApkIntent(context: Context, file: File): PendingIntent { - val intent = Intent(Intent.ACTION_VIEW).apply { - val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) - else Uri.fromFile(file) - setDataAndType(uri, "application/vnd.android.package-archive") - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - } - cancelNotification(context) - return PendingIntent.getActivity(context, 0, intent, 0) - } - - /** - * Downloads a new update and let the user install the new version from a notification. - * - * @param context the application context. - * @param url the url to the new update. - */ - fun downloadApkIntent(context: Context, url: String): PendingIntent { - val intent = Intent(context, UpdateDownloaderService::class.java).apply { - putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) - } - return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) - } - - fun cancelNotification(context: Context) { - context.notificationManager.cancel(NOTIFICATION_UPDATER_ID) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt similarity index 81% rename from app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadFragment.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt index 39e4226f9..755b8d0ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadActivity.kt @@ -2,15 +2,17 @@ package eu.kanade.tachiyomi.ui.download import android.os.Bundle import android.support.v7.widget.LinearLayoutManager -import android.view.* +import android.view.Menu +import android.view.MenuItem import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.source.model.Page -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment -import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.util.plusAssign +import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_download_queue.* +import kotlinx.android.synthetic.main.toolbar.* import nucleus.factory.RequiresPresenter import rx.Observable import rx.Subscription @@ -20,19 +22,18 @@ import java.util.* import java.util.concurrent.TimeUnit /** - * Fragment that shows the currently active downloads. + * Activity that shows the currently active downloads. * Uses R.layout.fragment_download_queue. */ @RequiresPresenter(DownloadPresenter::class) -class DownloadFragment : BaseRxFragment() { - +class DownloadActivity : BaseRxActivity() { /** * Adapter containing the active downloads. */ private lateinit var adapter: DownloadAdapter /** - * Subscription list to be cleared during [onDestroyView]. + * Subscription list to be cleared during [onDestroy]. */ private val subscriptions by lazy { CompositeSubscription() } @@ -46,38 +47,22 @@ class DownloadFragment : BaseRxFragment() { */ private var isRunning: Boolean = false - companion object { - /** - * Creates a new instance of this fragment. - * - * @return a new instance of [DownloadFragment]. - */ - fun newInstance(): DownloadFragment { - return DownloadFragment() - } - } - override fun onCreate(savedState: Bundle?) { + setAppTheme() super.onCreate(savedState) - setHasOptionsMenu(true) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View { - return inflater.inflate(R.layout.fragment_download_queue, container, false) - } - - override fun onViewCreated(view: View, savedState: Bundle?) { + setContentView(R.layout.activity_download_manager) + setupToolbar(toolbar) setToolbarTitle(R.string.label_download_queue) // Check if download queue is empty and update information accordingly. setInformationView() // Initialize adapter. - adapter = DownloadAdapter(activity) + adapter = DownloadAdapter(this) recycler.adapter = adapter // Set the layout manager for the recycler and fixed size. - recycler.layoutManager = LinearLayoutManager(activity) + recycler.layoutManager = LinearLayoutManager(this) recycler.setHasFixedSize(true) // Suscribe to changes @@ -94,20 +79,21 @@ class DownloadFragment : BaseRxFragment() { .subscribe { onUpdateDownloadedPages(it) } } - override fun onDestroyView() { + override fun onDestroy() { for (subscription in progressSubscriptions.values) { subscription.unsubscribe() } progressSubscriptions.clear() subscriptions.clear() - super.onDestroyView() + super.onDestroy() } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.download_queue, menu) + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.download_queue, menu) + return true } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun onPrepareOptionsMenu(menu: Menu): Boolean { // Set start button visibility. menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty() @@ -116,14 +102,18 @@ class DownloadFragment : BaseRxFragment() { // Set clear button visibility. menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() + return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.start_queue -> DownloadService.start(activity) - R.id.pause_queue -> DownloadService.stop(activity) + R.id.start_queue -> DownloadService.start(this) + R.id.pause_queue -> { + DownloadService.stop(this) + presenter.pauseDownloads() + } R.id.clear_queue -> { - DownloadService.stop(activity) + DownloadService.stop(this) presenter.clearQueue() } else -> return super.onOptionsItemSelected(item) @@ -198,7 +188,7 @@ class DownloadFragment : BaseRxFragment() { */ private fun onQueueStatusChange(running: Boolean) { isRunning = running - activity.supportInvalidateOptionsMenu() + supportInvalidateOptionsMenu() // Check if download queue is empty and update information accordingly. setInformationView() @@ -210,7 +200,7 @@ class DownloadFragment : BaseRxFragment() { * @param downloads the downloads from the queue. */ fun onNextDownloads(downloads: List) { - activity.supportInvalidateOptionsMenu() + supportInvalidateOptionsMenu() setInformationView() adapter.setItems(downloads) } @@ -247,8 +237,11 @@ class DownloadFragment : BaseRxFragment() { * Set information view when queue is empty */ private fun setInformationView() { - (activity as MainActivity).updateEmptyView(presenter.downloadQueue.isEmpty(), + updateEmptyView(presenter.downloadQueue.isEmpty(), R.string.information_no_downloads, R.drawable.ic_file_download_black_128dp) } + fun updateEmptyView(show: Boolean, textResource: Int, drawable: Int) { + if (show) empty_view.show(drawable, textResource) else empty_view.hide() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt index 6e71eb585..2664a70e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt @@ -12,9 +12,9 @@ import uy.kohesive.injekt.injectLazy import java.util.* /** - * Presenter of [DownloadFragment]. + * Presenter of [DownloadActivity]. */ -class DownloadPresenter : BasePresenter() { +class DownloadPresenter : BasePresenter() { /** * Download manager. @@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter() { downloadQueue.getUpdatedObservable() .observeOn(AndroidSchedulers.mainThread()) .map { ArrayList(it) } - .subscribeLatestCache(DownloadFragment::onNextDownloads, { view, error -> + .subscribeLatestCache(DownloadActivity::onNextDownloads, { view, error -> Timber.e(error) }) } @@ -48,6 +48,13 @@ class DownloadPresenter : BasePresenter() { .onBackpressureBuffer() } + /** + * Pauses the download queue. + */ + fun pauseDownloads() { + downloadManager.pauseDownloads() + } + /** * Clears the download queue. */ @@ -55,4 +62,4 @@ class DownloadPresenter : BasePresenter() { downloadManager.clearQueue() } -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index bd2371111..404c39a3f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.backup.BackupFragment import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment -import eu.kanade.tachiyomi.ui.download.DownloadFragment +import eu.kanade.tachiyomi.ui.download.DownloadActivity import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesFragment import eu.kanade.tachiyomi.ui.library.LibraryFragment import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment @@ -63,7 +63,7 @@ class MainActivity : BaseActivity() { R.id.nav_drawer_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id) R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance(), id) R.id.nav_drawer_latest_updates -> setFragment(LatestUpdatesFragment.newInstance(), id) - R.id.nav_drawer_downloads -> setFragment(DownloadFragment.newInstance(), id) + R.id.nav_drawer_downloads -> startActivity(Intent(this, DownloadActivity::class.java)) R.id.nav_drawer_settings -> { val intent = Intent(this, SettingsActivity::class.java) startActivityForResult(intent, REQUEST_OPEN_SETTINGS) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index d1b64e1ed..e07116d56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackUpdateService import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier +import eu.kanade.tachiyomi.ui.reader.SaveImageNotifier import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.RetryWithDelay import eu.kanade.tachiyomi.util.SharedData @@ -562,7 +562,7 @@ class ReaderPresenter : BasePresenter() { return // Used to show image notification. - val imageNotifier = ImageNotifier(context) + val imageNotifier = SaveImageNotifier(context) // Remove the notification if it already exists (user feedback). imageNotifier.onClear() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt similarity index 82% rename from app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotifier.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt index eeb20695e..9f4f43bd9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.ui.reader.notification +package eu.kanade.tachiyomi.ui.reader import android.content.Context import android.graphics.Bitmap @@ -7,13 +7,15 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.NotificationHandler +import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.util.notificationManager import java.io.File /** * Class used to show BigPictureStyle notifications */ -class ImageNotifier(private val context: Context) { +class SaveImageNotifier(private val context: Context) { /** * Notification builder. */ @@ -58,15 +60,15 @@ class ImageNotifier(private val context: Context) { if (!mActions.isEmpty()) mActions.clear() - setContentIntent(ImageNotificationReceiver.showImageIntent(context, file)) + setContentIntent(NotificationHandler.openImagePendingActivity(context, file)) // Share action addAction(R.drawable.ic_share_grey_24dp, - context.getString(R.string.action_share), - ImageNotificationReceiver.shareImageIntent(context, file)) + context.getString(R.string.action_share), + NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId)) // Delete action addAction(R.drawable.ic_delete_grey_24dp, context.getString(R.string.action_delete), - ImageNotificationReceiver.deleteImageIntent(context, file.absolutePath, notificationId)) + NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId)) updateNotification() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotificationReceiver.kt deleted file mode 100644 index d1b123928..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/notification/ImageNotificationReceiver.kt +++ /dev/null @@ -1,84 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader.notification - -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.support.v4.content.FileProvider -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.util.notificationManager -import java.io.File -import eu.kanade.tachiyomi.Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID as defaultNotification - -/** - * The BroadcastReceiver of [ImageNotifier] - * Intent calls should be made from this class. - */ -class ImageNotificationReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - ACTION_DELETE_IMAGE -> { - deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION)) - context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, defaultNotification)) - } - } - } - - /** - * Called to delete image - * - * @param path path of file - */ - private fun deleteImage(path: String) { - val file = File(path) - if (file.exists()) file.delete() - } - - companion object { - private const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE" - - private const val EXTRA_FILE_LOCATION = "file_location" - - private const val NOTIFICATION_ID = "notification_id" - - /** - * Called to start share intent to share image - * - * @param context context of application - * @param file file that contains image - */ - internal fun shareImageIntent(context: Context, file: File): PendingIntent { - val intent = Intent(Intent.ACTION_SEND).apply { - val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) - putExtra(Intent.EXTRA_STREAM, uri) - type = "image/*" - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - } - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) - } - - /** - * Called to show image in gallery application - * - * @param context context of application - * @param file file that contains image - */ - internal fun showImageIntent(context: Context, file: File): PendingIntent { - val intent = Intent(Intent.ACTION_VIEW).apply { - val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) - setDataAndType(uri, "image/*") - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - } - return PendingIntent.getActivity(context, 0, intent, 0) - } - - internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent { - val intent = Intent(context, ImageNotificationReceiver::class.java).apply { - action = ACTION_DELETE_IMAGE - putExtra(EXTRA_FILE_LOCATION, path) - putExtra(NOTIFICATION_ID, notificationId) - } - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt index 10b3f27b2..7fd84a3d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt @@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.util import android.app.Notification import android.app.NotificationManager +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.content.res.Resources import android.net.ConnectivityManager @@ -10,6 +13,7 @@ import android.os.PowerManager import android.support.annotation.StringRes import android.support.v4.app.NotificationCompat import android.support.v4.content.ContextCompat +import android.support.v4.content.LocalBroadcastManager import android.widget.Toast /** @@ -95,3 +99,40 @@ val Context.connectivityManager: ConnectivityManager val Context.powerManager: PowerManager get() = getSystemService(Context.POWER_SERVICE) as PowerManager +/** + * Function used to send a local broadcast asynchronous + * + * @param intent intent that contains broadcast information + */ +fun Context.sendLocalBroadcast(intent:Intent){ + LocalBroadcastManager.getInstance(this).sendBroadcast(intent) +} + +/** + * Function used to send a local broadcast synchronous + * + * @param intent intent that contains broadcast information + */ +fun Context.sendLocalBroadcastSync(intent: Intent) { + LocalBroadcastManager.getInstance(this).sendBroadcastSync(intent) +} + +/** + * Function used to register local broadcast + * + * @param receiver receiver that gets registered. + */ +fun Context.registerLocalReceiver(receiver: BroadcastReceiver, filter: IntentFilter ){ + LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter) +} + +/** + * Function used to unregister local broadcast + * + * @param receiver receiver that gets unregistered. + */ +fun Context.unregisterLocalReceiver(receiver: BroadcastReceiver){ + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) +} + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt new file mode 100644 index 000000000..7b208c608 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.util + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.support.v4.content.FileProvider +import eu.kanade.tachiyomi.BuildConfig +import java.io.File + +/** + * Returns the uri of a file + * + * @param context context of application + */ +fun File.getUriCompat(context: Context): Uri { + val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this) + else Uri.fromFile(this) + return uri +} + +/** + * Deletes file if exists + * + * @return success of file deletion + */ +fun File.deleteIfExists(): Boolean { + if (this.exists()) { + this.delete() + return true + } + return false +} diff --git a/app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png b/app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png new file mode 100644 index 0000000000000000000000000000000000000000..074bf756216a37648afdd1ebd7ab1034fdd5cfff GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^54z;g5Y zCx&;*PweClWMBWpA{?=0mvHnZ7S5AmrgwRS6n}ma-S_9u6Tu5@^Sicmt$W>Ncub{b zLe0I)CatDga}GYQJhr=ie`@43$HqfD8B`OPj?H{v!8b$UeDMs2hcONJ#DoprnK#wm zSDv**gDYaA-=$KMnj=m#C6d+_R!FSEL_i- zwz&Enun^Wc%_MxKlvh||4)+BWp94GYb3ChT%f4m8yT;=oPZ;05(1ith4!_R`8JuIe zY;sUH>$X_f5}?Rr1IEWb27Sst7fNM)7J!wo^cWvB;4yH#|J|VRkmHPhft~DfjJmJ> bYgaKETCOs?kyo`J7)T7Bu6{1-oD!M^XlK$SZS6v6HVNeB``zz5=N^L5hA+HfeY6EcCcKR(enkC#|0Mw284icKF(Ip_PX>-IEF8^+WKUFHMY?KTiX5Myl2Ie+Fj&aG4`^?hB459o9{ z000PMj3}i@Rn<$3@f*+cDw?L9t_qrSa9u=#6h%1|LhJy*pd2C z!3Q*(O^^ki7CP+C7?Wr_HjRlQ)0?K+OLo5^I(W;ZhDAWl4sVpLUiOesA^2;J3n{pl1k=U|zL0@g~v zN5_@(O@27~=}({4iFV z)@rr54&*#~3jpOpq3}7XZhnzt0^}U(2_g1O)BF=9F5mlrYPA}64h8_QCxqw~i^bn@ zgl}|_bg-a{J_#YpiA3UPt#NHx)^!JRzPu)cRPy=!*JXAc*rqa~iGwi!JedgpSXKB& zMA895=nP~02mtz~X literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_av_pause_grey_24dp_img.png b/app/src/main/res/drawable-mdpi/ic_av_pause_grey_24dp_img.png new file mode 100644 index 0000000000000000000000000000000000000000..e13456677bb9900dec61eb3c26b23eb61f455cb7 GIT binary patch literal 363 zcmV-x0hIoUP)X1^@s6IQ*`u0003pNkl=#Y9Imr^x4e} zRKPF{>v^8G*cU|+H)z`ygm?!~LzDm_*|IEPKnPi{>yixOIOYaT(?|yFFJO$N3>bio zAcp-9sOuVxa0gIFNRosbJO)%%1q{M4lt$?L9_&Gyrre+`ORxt~6aj#`2NFOGhlQvD z+O`EDW?9A!ilP8-;SRt;6yJgbQ1?IuG))6GG0$^u@Z`J!;0N6?{2F=u;H&@u002ov JPDHLkV1m)|k#qn6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_av_play_arrow_grey_img.png b/app/src/main/res/drawable-mdpi/ic_av_play_arrow_grey_img.png new file mode 100644 index 0000000000000000000000000000000000000000..1c2bd317cd998ccc170a5d855d4c5a7a25806720 GIT binary patch literal 421 zcmV;W0b2fvP)X1^@s6IQ*`u0004ONklX;8MKl)effV$(Dq3P2D9y8spda7_s5 zce~xmza!KD6e5nbcCXj_+E_&2_qQ1&i0CP$E-J6l^St#wNGYY~j4{8{>C9>yNdrnj zpSru1QsbRlSxTu5^xgIwjmFL1k4Z}TgouITIB#{2XJZlfO)AG9!JqgGRBX5!$`7j` P00000NkvXXu0mjfBjUI# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png b/app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png new file mode 100644 index 0000000000000000000000000000000000000000..c218aee56f65d4933408f56bb0e46893c4d08c68 GIT binary patch literal 536 zcmeAS@N?(olHy`uVBq!ia0vp^3P5bc!3HF4Q*Nd(FfiWtba4!caDO}FbYGK$h|5cj zU-fDK8dVK-J5CAB-y{O{n8YCCbCcRD^+0;|k~BezJ4FVjD|a+j!!W&FPt`tkQS zGU+gS9}xN>u;TTuKrc(*%NDlx7e24djIX>}`gie^T@99f&nxS%|Krf;TIAU7ws_*V z+Wjwt3+(2n_aA@UmFJe7%XQ-O9>=XVa{ZZ%8P`6rq(rhAKiH+G5)gXz&ELBCiF4<& zTu{8$^!qQL@rDSUGdvuOPo%vce65PTxaJ|J$7PYU1f!V&k^)R8&iOID5Xe6G`DcqH zN7I2b2VRymu`Ii+>FU6;V0I>_0QUvOn@vA!?%i(L)aE1HmUu(0VUo7k2Br%@V=UzQ z4VX8vb~r>%3lT08a*#~WO_)`%GiDaZT=QuRms<`C7g)$7uq|Pq!DHZi!;>M^p~@lq z?rmlvr)8XrHH?fm+<&jgc#=Vz;jWK&Lqr4LNihSa8EP?W!*ZF#k=TGdnT-R8vf0T@ZO#c*EViM4&wajXI~(e4vimaX0V7%!17~ zXGnTXViB64o4{dkzD1WYiBU=6f8Sz_S9h5M=<{XL*!utL?poJm&IqP>^$*gTe~DWM4fzG&AQ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png b/app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png new file mode 100644 index 0000000000000000000000000000000000000000..d5d467c4204e39a8a9b3030bfadb9b44165c1157 GIT binary patch literal 780 zcmV+n1M~ceP)&RUXrjg`F`d*-`{VN#pr@Be8Kw22k;&69q=8nInZb{ zJ_Er0Xf*otyc(1Zpjxd$C=^13kkjtp_R4Nq^f`LE)$+FxY3ZF!#kL#)z>cCQk5_?k z0E}l~+Ginp7SczIu^mm*elG)J11ObBQ!_9v8^K@@Ns`n-2wiG6n{O@4;){Sd0Ngq- zK2DZpQL^tCW7joJJ8}zQ11J`Y3p+6CjCjAfxp^`e3~G`jWh0SDZytyPVBQ2b_a#X} zl+v$+klTu)e3)5Tqc?>EZUo$nIcs2|snuGo)?Lf8x|8NP99T93)8>him1X(3-|yd2 zRrS?4gbkojD6Eu$StTn%iZR~n_xnXn(>@JB8~|?YU3Ltm^aZ8#K_ZcOf8GG67~^eS z*PqYb0k(ohm0|6TIr1f z*+Q7hY8^2gcYm3}b&8;fZ}9o6Wk(Kogu~$*p8q5h0CQGzNC>%X7{>QiRy{QX zGXTU%mg#o8b<46&yb>4RW2*uIFd+k?Z^u)q)c*ryZ2*}}W;6qgbFQ1FS@TkG=2;*B zgad?-H-wOzhG879Q{1ry>2zBB|I3-CDQf#%F8>LM?||=s@4!D2qcl+k5Iw^H0000< KMNUMnLSTZ|{Z?}T literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_av_pause_grey_24dp_img.png b/app/src/main/res/drawable-xxhdpi/ic_av_pause_grey_24dp_img.png new file mode 100644 index 0000000000000000000000000000000000000000..803a258ffbd52f8b7cc389354cc0d87b1a3a221c GIT binary patch literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^20-l1!3HGFp0l(vFfcWFx;TbpIKQ2>J5S3{#Pw~Z zqxeV8o%Zgo2WK-zB~*N?xx071LoZ7opHO&LO8=9mN|PsEUb6B0N4xCz!6}Qb>i@sE z|Ic47t{<6A6CG3pEE+Qw9$;jP?`LUpmxxdh;*d%B@YO@1qNR~_pWu$T^=(&n-+y1v z)_gF2{_fDw+AmdmXP=+@{bt?%|NrmaXNl1hPwokf%6|IU+H=zRhXp^*KQ~lbpwTtk zfByH~#~yEtJ@B)}Y+wKM&r#Vb3gxEtpEEDRsvUnVH<5ZY;W)n}U z1IL8A%P+6YYkd5%!^nYiNoJSPgDn#{6vP&>xG=m7_ihmBVX|3%EsrTh`$=Wrwh1W> zUuK_8`=(oBA#*`&k)+2YmR!Zb%ivV{ZlTyQ%)v69$ZjAGSv&|VdiCWF&yX7>A@lyWAh#T7j zLPImSFY;(Cy&zTuR9|uHW9b|r0mdcqx{N^^0+~A0ci+`J)gYko_td5>=?U9!FXWL( zwmZG)%fHXN4B1i-06puVvt-I4CXfA5OiL0(9$0QI6IO_Jv@^UYFuCsZ(@XnWx82sg zmdK II;Vst0Mdyl7ytkO literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_av_play_arrow_grey_img.png b/app/src/main/res/drawable-xxhdpi/ic_av_play_arrow_grey_img.png new file mode 100644 index 0000000000000000000000000000000000000000..cf825e63e72a04a575ef9fabf9fd5fdac97b31c4 GIT binary patch literal 1308 zcmV+%1>^dOP)Ky+QVbzQ$l2w8L-XUjCr zzeCzdOe_%K+@hvwgfX_q7`x**&Z23WKP2W&2*q^)q)wR)#@GpC>^DNlo3?G=7#tit z31KgGk%j=ytx6Ei`Cp8&uZ0jVX0zEJWA{i=p%OrCS#i$y0pJzabw6xnZv+A$;%Vsw z{g*I?VN@uk_W)qpb=_}G)2t{4F!FIP5Y8kx?C=oH}d&>Sw%M8HG9=D=X{$|`nuyd@+J>`0R{quokESu$??iL z|AR5MUa3@y0|NtcQB?l;0!&X&N1j5B-sGHfIjOb@AtR3Ce5ShP1_CIDB7%T~H4Ni~ zQo8E8?o7E{eqdRa(}>DT0Z`lIhGE?IJnuTC^iE%2-)=pKK!8STIqq|3TkbIcYziTk zT-W_#XlUqI=^~q&nrd1P1oaAI%w>!n=(@gobaXV8%jFLJAbc|*0qUi$8Q-66xz8D6 zJBav_X_|NcZ}(CIoH^@in)Zk>Hr(IezkZbfYTNcsq>D@fG-Jxt*w`V2 zScte;C={Nl*bdBqijt6LWsv?s#8v5hp-_l#nUfh%sM!mj9|h<+7DD_?DV@(`GApsK z(-RXDN%p{FLdXiG^zB?O_b_Dk{uA>pAqfywv5&lbH2*0g-fVAg-;5IVTHVb6kij?r zfNRxi^;TzR=aV>JmNWxk9TA5d$Jw(i%Z@vnJ-zTc8Zi{sUCZb5KPk#a zIvy23KF`^tl+I)_neXBnjVeTe01foO0j0Dkgjg9F8F`v?tt+k5#bWWI9(XQc0r!Pei-z3RPsQSNB&;r9 zfUwG1A(sNQBI0^k=WRV{-Lt^;?a7+ds(H zKYagQlHpiGt8#)+gazXn7GEZ{Mkq&z)d!igZ_0tMRkECCen_`H)IVf(?`cux=i2}L zyTT;e{_lN1oq^{t1M`F5|9)5P{nwipWmj)EKmK@OjpOyMMIQVIEc)iX|9!8#zIxtw zIafj6hUcGuHobAbr`(q;%TRm2|Ha|3)uMA5@7wmAKfh8^fnh<3Fy{uzhIc0=7JOw$ zZtz?nA9#48NXO$D)0p~?Gy65Fc$R1uSk2YYJYaM%QNpf@p-N-2k3j7V!-TDijy~u& zc2H?ZTla-w8aoHWC6lL{K1_@>#rVl0>niEtwQ>8?NQ@%8q_?Fr($c?sOZBke; zGvRva`gzX}US4pEg%xP*1_=cQgLOxH8E1sWbIh zXDwT}gyA;JMZwxhoQwF4*;p9Ya?~DEsM5${>tMMcr{Fu`E#r%MYLyJX44yMOyi}8$ zkSeLmDzVvN-t%7~25b%GO@77?Dr^qfI;=Z#L#^XfP;L_wgELbOzr*(>-x!z=NF)d? z@M*4`ce(1o-@5kyPD?bnV)<9Ro;8U{M>yy9w`3W&U|R_Od^TiCd2#s|IhquuNSUxr=(+$8T`^`B z1~>j0PZ+z|8y+&IMg|2rNF4BJT(#J;c38A4|gwt)6bhfMTD9u! znl)>j#i*9D0QT(JGqz;OlE*@bBaY)7iAJL@78``J0Cw-*Z2+JciA0`CDX%z=a})qB zMx)Wm#R8%%fL*(G5x{>%lu~~q;%!9icO2(zG#b6W7$8^y%$+163PgM+r5un_o|>7N z8B8P+uK|FX6^0c6D}ac^>~AThkWx-5rG}-H$BZ%Ogb+h(*RGvvMi5p23#}}JP)g-l z2}HEt7;|HMe0(IGPUo9R4OIh}oupEVLA;StGK9mUqoenl34#^CT=~y8hdF;6rBqI9 zeMbnf7ZLZjwY5z*gA&RD*tv7(zh1y41l}iRB9REI;hN((ht{oIcOvKzyi&kNNJ0ol zDfIynhqcz{CnqPr&15ovrPJwR5Mg+#fVoOR#5^Lt)>>aBqTYNy-?x7K`nN%YUEOBZzoG2yqGkE{%+gJP(3dSOK`* z0Ns?+T5}NfKuXyog!s`I^DrKdPk6bam9}LCP-%m<@@y7b>uE&13jkkDOiY~4X0w@e zI&Exq>+ydp05>aJiK{HN$L`gML?TmC%6?3o1`enBnB7A}H^;}v-_&b=2poVpr%XOvJqCb-j^p&r z%*+fWlgUZ1a}HJjRcve@lP6VphyNvC__)m*BmnFMfU8QWv9`9hf>)+d7QpuH+w1KG zT*2ZD#+aj#NaREp^-xefU0R&XupBmO3BI;90^%2pH_V)G>H)FQ~@M+~- zU%$;8k0GK_0JtfH=mmf?$z<}8n~BesDGOlh)~!K}1NiCA6cPOi0DX?*?9b=(4?Pcs z89*g8eJYz1LX2{zNCC&aQ;_)~uq2`~a0002J#+U;_h^x6=juWKWnrB{WzzU#&ULXemPmM90 z20I4;_j0-12d}Tt$^zK3WlN*Hz}wQ5*>SD);Id`QUe~hGRlNx^0I#IxrX2uyPegZx z5dDaFHknLbc2lpGGC@&55ddBg(ICG)s0f0$z(WOG27r@NO3vR8`L)Bl z*(t36+%&R48Gbd+-x6FDLj0nX`qgccqJ};zu38TOd}`xU+C%o|MD$IuSbP-(xqpg| z6+nf1K%R)kgb>$===+TuHy*EeNqnwg1yJF5*tK;jrS^GobzNcKRTZ}aaH#;^;SaKV z(^}uH$5WQ7X6K?(Spb_hZL-AyywRN@qQjhyb{vOo-Ltd0i}BZ7#0p@69ypTYgyabT zI4h;RlS-u~ynet`^XyzSPz`{W0Kopd7ZERIvsn&k8uHH9#r&*ADgp40Kab-#|61$Q zsZ?sv+IkZdumRwe<&UOc3n8v>HkRM~`}K^~4}LZP86tWC0Q(T}a7RbSb3X> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_navigation.xml b/app/src/main/res/menu/menu_navigation.xml index a461a5f36..e069a295c 100644 --- a/app/src/main/res/menu/menu_navigation.xml +++ b/app/src/main/res/menu/menu_navigation.xml @@ -26,7 +26,8 @@ + android:title="@string/label_download_queue" + android:checkable="false" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4a71c6add..6665fc0a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -260,6 +260,7 @@ Downloading Downloading (%1$d/%2$d) Error + Paused Error while fetching chapters Show title Show chapter number @@ -383,5 +384,6 @@ A page is not loaded No wifi connection available No network connection available + Download paused