mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 22:37:56 +01:00 
			
		
		
		
	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
This commit is contained in:
		
				
					committed by
					
						 inorichi
						inorichi
					
				
			
			
				
	
			
			
			
						parent
						
							52c50398b8
						
					
				
				
					commit
					c445ea90ba
				
			| @@ -60,10 +60,19 @@ class DownloadManager(context: Context) { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Empties the download queue. | ||||
|      * Tells the downloader to pause downloads. | ||||
|      */ | ||||
|     fun clearQueue() { | ||||
|         downloader.clearQueue() | ||||
|     fun pauseDownloads() { | ||||
|         downloader.pause() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Empties the download queue. | ||||
|      * | ||||
|      * @param isNotification value that determines if status is set (needed for view updates) | ||||
|      */ | ||||
|     fun clearQueue(isNotification: Boolean = false) { | ||||
|         downloader.clearQueue(isNotification) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -168,5 +177,4 @@ class DownloadManager(context: Context) { | ||||
|     fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) { | ||||
|         provider.findChapterDir(source, manga, chapter)?.delete() | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.Constants | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.download.model.DownloadQueue | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationHandler | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.util.chop | ||||
| import eu.kanade.tachiyomi.util.notificationManager | ||||
|  | ||||
| @@ -33,12 +35,34 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|      * The size of queue on start download. | ||||
|      */ | ||||
|     var initialQueueSize = 0 | ||||
|         get() = field | ||||
|         set(value) { | ||||
|             if (value != 0){ | ||||
|                 isSingleChapter = (value == 1) | ||||
|             } | ||||
|             field = value | ||||
|         } | ||||
|  | ||||
|     /** | ||||
|      * Simultaneous download setting > 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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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<Manga>() | ||||
|         val failedUpdates = ArrayList<Manga>() | ||||
|  | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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()) | ||||
|     } | ||||
| } | ||||
| @@ -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()) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user