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
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
@ -55,4 +62,4 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
|
|||
downloadManager.clearQueue()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.reader.notification
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
|
@ -7,13 +7,15 @@ import com.bumptech.glide.Glide
|
|||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Class used to show BigPictureStyle notifications
|
||||
*/
|
||||
class ImageNotifier(private val context: Context) {
|
||||
class SaveImageNotifier(private val context: Context) {
|
||||
/**
|
||||
* Notification builder.
|
||||
*/
|
||||
|
@ -58,15 +60,15 @@ class ImageNotifier(private val context: Context) {
|
|||
if (!mActions.isEmpty())
|
||||
mActions.clear()
|
||||
|
||||
setContentIntent(ImageNotificationReceiver.showImageIntent(context, file))
|
||||
setContentIntent(NotificationHandler.openImagePendingActivity(context, file))
|
||||
// Share action
|
||||
addAction(R.drawable.ic_share_grey_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
ImageNotificationReceiver.shareImageIntent(context, file))
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId))
|
||||
// Delete action
|
||||
addAction(R.drawable.ic_delete_grey_24dp,
|
||||
context.getString(R.string.action_delete),
|
||||
ImageNotificationReceiver.deleteImageIntent(context, file.absolutePath, notificationId))
|
||||
NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId))
|
||||
updateNotification()
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
33
app/src/main/java/eu/kanade/tachiyomi/util/FileExtensions.kt
Normal 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
|
||||
}
|
BIN
app/src/main/res/drawable-hdpi/ic_av_pause_grey_24dp_img.png
Normal file
After Width: | Height: | Size: 331 B |
BIN
app/src/main/res/drawable-hdpi/ic_av_play_arrow_grey_img.png
Normal file
After Width: | Height: | Size: 631 B |
BIN
app/src/main/res/drawable-mdpi/ic_av_pause_grey_24dp_img.png
Normal file
After Width: | Height: | Size: 363 B |
BIN
app/src/main/res/drawable-mdpi/ic_av_play_arrow_grey_img.png
Normal file
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-xhdpi/ic_av_pause_grey_24dp_img.png
Normal file
After Width: | Height: | Size: 536 B |
BIN
app/src/main/res/drawable-xhdpi/ic_av_play_arrow_grey_img.png
Normal file
After Width: | Height: | Size: 780 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_av_pause_grey_24dp_img.png
Normal file
After Width: | Height: | Size: 699 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_av_play_arrow_grey_img.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/ic_av_pause_grey_24dp_img.png
Normal file
After Width: | Height: | Size: 850 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_av_play_arrow_grey_img.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
42
app/src/main/res/layout/activity_download_manager.xml
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|