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 000000000..074bf7562 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png b/app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png new file mode 100644 index 000000000..37a4ca1cd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png differ 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 000000000..e13456677 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_av_pause_grey_24dp_img.png differ 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 000000000..1c2bd317c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_av_play_arrow_grey_img.png differ 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 000000000..c218aee56 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png differ 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 000000000..d5d467c42 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png differ 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 000000000..803a258ff Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_av_pause_grey_24dp_img.png differ 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 000000000..cf825e63e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_av_play_arrow_grey_img.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_av_pause_grey_24dp_img.png b/app/src/main/res/drawable-xxxhdpi/ic_av_pause_grey_24dp_img.png new file mode 100644 index 000000000..fdf4261dc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_av_pause_grey_24dp_img.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow_grey_img.png b/app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow_grey_img.png new file mode 100644 index 000000000..7e1eef6c2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow_grey_img.png differ diff --git a/app/src/main/res/layout/activity_download_manager.xml b/app/src/main/res/layout/activity_download_manager.xml new file mode 100644 index 000000000..c99c4f698 --- /dev/null +++ b/app/src/main/res/layout/activity_download_manager.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + \ 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