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:
Bram van de Kerkhof 2017-01-20 21:18:15 +01:00 committed by inorichi
parent 52c50398b8
commit c445ea90ba
32 changed files with 993 additions and 394 deletions

View File

@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.kanade.tachiyomi">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application
@ -20,9 +21,8 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:theme="@style/Theme.Tachiyomi" >
<activity
android:name=".ui.main.MainActivity">
android:theme="@style/Theme.Tachiyomi">
<activity android:name=".ui.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -31,40 +31,40 @@
</activity>
<activity
android:name=".ui.manga.MangaActivity"
android:parentActivityName=".ui.main.MainActivity"
android:exported="true">
</activity>
android:exported="true"
android:parentActivityName=".ui.main.MainActivity" />
<activity
android:name=".ui.reader.ReaderActivity"
android:theme="@style/Theme.Reader">
</activity>
android:theme="@style/Theme.Reader" />
<activity
android:name=".ui.setting.SettingsActivity"
android:label="@string/label_settings"
android:parentActivityName=".ui.main.MainActivity" >
</activity>
android:parentActivityName=".ui.main.MainActivity" />
<activity
android:name=".ui.category.CategoryActivity"
android:label="@string/label_categories"
android:parentActivityName=".ui.main.MainActivity">
</activity>
android:parentActivityName=".ui.main.MainActivity" />
<activity
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
</activity>
android:theme="@style/FilePickerTheme" />
<activity
android:name=".ui.setting.AnilistLoginActivity"
android:label="Anilist">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="anilist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.download.DownloadActivity"
android:launchMode="singleTop" />
<provider
android:name="android.support.v4.content.FileProvider"
@ -73,26 +73,27 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
android:resource="@xml/provider_paths" />
</provider>
<service android:name=".data.library.LibraryUpdateService"
android:exported="false"/>
<receiver
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<service android:name=".data.download.DownloadService"
android:exported="false"/>
<service
android:name=".data.library.LibraryUpdateService"
android:exported="false" />
<service android:name=".data.track.TrackUpdateService"
android:exported="false"/>
<service
android:name=".data.download.DownloadService"
android:exported="false" />
<service android:name=".data.updater.UpdateDownloaderService"
android:exported="false"/>
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
<receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
<receiver android:name=".ui.reader.notification.ImageNotificationReceiver" />
<service
android:name=".data.track.TrackUpdateService"
android:exported="false" />
<service
android:name=".data.updater.UpdateDownloaderService"
android:exported="false" />
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"

View File

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

View File

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

View File

@ -132,16 +132,43 @@ class Downloader(private val context: Context, private val provider: DownloadPro
if (reason != null) {
notifier.onWarning(reason)
} else {
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)
@ -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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
* Returns [PendingIntent] that starts a service which downloads the apk specified in url.
*
* @param url the url to the new update.
* @return [PendingIntent]
*/
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)
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, 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) }
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
}
// 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))
}
}
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
}
}

View File

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

View File

@ -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<DownloadPresenter>() {
class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
/**
* 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<DownloadPresenter>() {
*/
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<DownloadPresenter>() {
.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<DownloadPresenter>() {
// 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<DownloadPresenter>() {
*/
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<DownloadPresenter>() {
* @param downloads the downloads from the queue.
*/
fun onNextDownloads(downloads: List<Download>) {
activity.supportInvalidateOptionsMenu()
supportInvalidateOptionsMenu()
setInformationView()
adapter.setItems(downloads)
}
@ -247,8 +237,11 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
* 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()
}
}

View File

@ -12,9 +12,9 @@ import uy.kohesive.injekt.injectLazy
import java.util.*
/**
* Presenter of [DownloadFragment].
* Presenter of [DownloadActivity].
*/
class DownloadPresenter : BasePresenter<DownloadFragment>() {
class DownloadPresenter : BasePresenter<DownloadActivity>() {
/**
* Download manager.
@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
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<DownloadFragment>() {
.onBackpressureBuffer()
}
/**
* Pauses the download queue.
*/
fun pauseDownloads() {
downloadManager.pauseDownloads()
}
/**
* Clears the download queue.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/toolbar"/>
</android.support.design.widget.AppBarLayout>
<FrameLayout
android:id="@+id/frame_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/recycler"
tools:listitem="@layout/item_download"/>
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
</FrameLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>

View File

@ -26,7 +26,8 @@
<item
android:id="@+id/nav_drawer_downloads"
android:icon="@drawable/ic_file_download_black_24dp"
android:title="@string/label_download_queue" />
android:title="@string/label_download_queue"
android:checkable="false" />
</group>
<group android:id="@+id/group_settings"
android:checkableBehavior="single">

View File

@ -260,6 +260,7 @@
<string name="chapter_downloading">Downloading</string>
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
<string name="chapter_error">Error</string>
<string name="chapter_paused">Paused</string>
<string name="fetch_chapters_error">Error while fetching chapters</string>
<string name="show_title">Show title</string>
<string name="show_chapter_number">Show chapter number</string>
@ -383,5 +384,6 @@
<string name="download_notifier_page_ready_error">A page is not loaded</string>
<string name="download_notifier_text_only_wifi">No wifi connection available</string>
<string name="download_notifier_no_network">No network connection available</string>
<string name="download_notifier_download_paused">Download paused</string>
</resources>