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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<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.READ_PHONE_STATE"
|
||||||
|
tools:node="remove" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@ -20,9 +21,8 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi" >
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
<activity
|
<activity android:name=".ui.main.MainActivity">
|
||||||
android:name=".ui.main.MainActivity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@ -31,40 +31,40 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.manga.MangaActivity"
|
android:name=".ui.manga.MangaActivity"
|
||||||
android:parentActivityName=".ui.main.MainActivity"
|
android:exported="true"
|
||||||
android:exported="true">
|
android:parentActivityName=".ui.main.MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:theme="@style/Theme.Reader">
|
android:theme="@style/Theme.Reader" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.SettingsActivity"
|
android:name=".ui.setting.SettingsActivity"
|
||||||
android:label="@string/label_settings"
|
android:label="@string/label_settings"
|
||||||
android:parentActivityName=".ui.main.MainActivity" >
|
android:parentActivityName=".ui.main.MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.category.CategoryActivity"
|
android:name=".ui.category.CategoryActivity"
|
||||||
android:label="@string/label_categories"
|
android:label="@string/label_categories"
|
||||||
android:parentActivityName=".ui.main.MainActivity">
|
android:parentActivityName=".ui.main.MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme">
|
android:theme="@style/FilePickerTheme" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.AnilistLoginActivity"
|
android:name=".ui.setting.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="anilist-auth"
|
android:host="anilist-auth"
|
||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.download.DownloadActivity"
|
||||||
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
@ -73,26 +73,27 @@
|
|||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths"/>
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service android:name=".data.library.LibraryUpdateService"
|
<receiver
|
||||||
android:exported="false"/>
|
android:name=".data.notification.NotificationReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".data.download.DownloadService"
|
<service
|
||||||
android:exported="false"/>
|
android:name=".data.library.LibraryUpdateService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".data.track.TrackUpdateService"
|
<service
|
||||||
android:exported="false"/>
|
android:name=".data.download.DownloadService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".data.updater.UpdateDownloaderService"
|
<service
|
||||||
android:exported="false"/>
|
android:name=".data.track.TrackUpdateService"
|
||||||
|
android:exported="false" />
|
||||||
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
|
<service
|
||||||
|
android:name=".data.updater.UpdateDownloaderService"
|
||||||
<receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name=".ui.reader.notification.ImageNotificationReceiver" />
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
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() {
|
fun pauseDownloads() {
|
||||||
downloader.clearQueue()
|
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) {
|
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
|
||||||
provider.findChapterDir(source, manga, chapter)?.delete()
|
provider.findChapterDir(source, manga, chapter)?.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.Constants
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
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.chop
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
|
||||||
@ -33,12 +35,34 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* The size of queue on start download.
|
* The size of queue on start download.
|
||||||
*/
|
*/
|
||||||
var initialQueueSize = 0
|
var initialQueueSize = 0
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
if (value != 0){
|
||||||
|
isSingleChapter = (value == 1)
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simultaneous download setting > 1.
|
* Simultaneous download setting > 1.
|
||||||
*/
|
*/
|
||||||
var multipleDownloadThreads = false
|
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.
|
* Shows a notification from this builder.
|
||||||
*
|
*
|
||||||
@ -48,6 +72,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
context.notificationManager.notify(id, build())
|
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
|
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||||
* those can only be dismissed by the user.
|
* 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.
|
* @param queue the queue containing downloads.
|
||||||
*/
|
*/
|
||||||
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
|
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
|
// Create notification
|
||||||
with(notification) {
|
with(notification) {
|
||||||
// Check if icon needs refresh
|
// Check if first call.
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
setAutoCancel(false)
|
||||||
|
clearActions()
|
||||||
|
// Open download manager when clicked
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
isDownloading = true
|
isDownloading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +144,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
||||||
} else {
|
} else {
|
||||||
download?.let {
|
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)
|
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||||
.format(it.downloadedImages, it.pages!!.size))
|
.format(it.downloadedImages, it.pages!!.size))
|
||||||
setProgress(it.pages!!.size, it.downloadedImages, false)
|
setProgress(it.pages!!.size, it.downloadedImages, false)
|
||||||
@ -133,17 +158,57 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
notification.show()
|
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.
|
* Called when chapter is downloaded.
|
||||||
*
|
*
|
||||||
* @param download download object containing download information.
|
* @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.
|
// Create notification.
|
||||||
with(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))
|
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
setAutoCancel(true)
|
||||||
|
clearActions()
|
||||||
|
setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,9 +230,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setContentText(reason)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
setAutoCancel(true)
|
||||||
|
clearActions()
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notification.show()
|
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))
|
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
clearActions()
|
||||||
|
setAutoCancel(false)
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
|
errorThrown = true
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,15 +133,42 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
if (reason != null) {
|
if (reason != null) {
|
||||||
notifier.onWarning(reason)
|
notifier.onWarning(reason)
|
||||||
} else {
|
} 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()
|
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()
|
queue.clear()
|
||||||
notifier.dismiss()
|
notifier.dismiss()
|
||||||
}
|
}
|
||||||
@ -313,7 +340,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
tmpFile?.delete()
|
tmpFile?.delete()
|
||||||
|
|
||||||
// Try to find the image file.
|
// 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
|
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||||
val pageObservable = if (imageFile != null)
|
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 {
|
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||||
// Read content type if available.
|
// Read content type if available.
|
||||||
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||||
// Else guess from the uri.
|
// Else guess from the uri.
|
||||||
?: context.contentResolver.getType(file.uri)
|
?: context.contentResolver.getType(file.uri)
|
||||||
// Else read magic numbers.
|
// Else read magic numbers.
|
||||||
?: file.openInputStream().buffered().use {
|
?: file.openInputStream().buffered().use {
|
||||||
URLConnection.guessContentTypeFromStream(it)
|
URLConnection.guessContentTypeFromStream(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,6 +448,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
notifier.onProgressChange(queue)
|
notifier.onProgressChange(queue)
|
||||||
}
|
}
|
||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
|
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||||
|
notifier.onDownloadCompleted(download, queue)
|
||||||
|
}
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.library
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
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.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
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.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
@ -69,6 +69,11 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
private var subscription: Subscription? = null
|
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.
|
* Id of the library update notification.
|
||||||
*/
|
*/
|
||||||
@ -236,13 +241,10 @@ class LibraryUpdateService : Service() {
|
|||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Manga>()
|
||||||
val failedUpdates = 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.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// 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.
|
// Update the chapters of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
@ -316,13 +318,10 @@ class LibraryUpdateService : Service() {
|
|||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
|
|
||||||
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
|
||||||
Intent(this, CancelUpdateReceiver::class.java), 0)
|
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// 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.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource
|
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
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
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
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import com.evernote.android.job.Job
|
import com.evernote.android.job.Job
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
@ -17,6 +19,10 @@ class UpdateCheckerJob : Job() {
|
|||||||
if (result is GithubUpdateResult.NewUpdate) {
|
if (result is GithubUpdateResult.NewUpdate) {
|
||||||
val url = result.release.downloadLink
|
val url = result.release.downloadLink
|
||||||
|
|
||||||
|
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
||||||
|
}
|
||||||
|
|
||||||
NotificationCompat.Builder(context).update {
|
NotificationCompat.Builder(context).update {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||||
@ -24,7 +30,7 @@ class UpdateCheckerJob : Job() {
|
|||||||
// Download action
|
// Download action
|
||||||
addAction(android.R.drawable.stat_sys_download_done,
|
addAction(android.R.drawable.stat_sys_download_done,
|
||||||
context.getString(R.string.action_download),
|
context.getString(R.string.action_download),
|
||||||
UpdateNotificationReceiver.downloadApkIntent(context, url))
|
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Job.Result.SUCCESS
|
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
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import android.app.IntentService
|
import android.app.IntentService
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.content.IntentFilter
|
||||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
import android.os.Build
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.data.network.ProgressListener
|
import eu.kanade.tachiyomi.data.network.ProgressListener
|
||||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
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.saveTo
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
|
||||||
|
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
|
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 {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Name of Local BroadCastReceiver.
|
||||||
|
*/
|
||||||
|
private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download url.
|
* 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.
|
* 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)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Network helper
|
* Returns [PendingIntent] that starts a service which downloads the apk specified in url.
|
||||||
*/
|
*
|
||||||
private val network: NetworkHelper by injectLazy()
|
* @param url the url to the new update.
|
||||||
|
* @return [PendingIntent]
|
||||||
override fun onHandleIntent(intent: Intent?) {
|
*/
|
||||||
if (intent == null) return
|
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
|
||||||
|
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
||||||
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
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.os.Bundle
|
||||||
import android.support.v7.widget.LinearLayoutManager
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
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.fragment_download_queue.*
|
||||||
|
import kotlinx.android.synthetic.main.toolbar.*
|
||||||
import nucleus.factory.RequiresPresenter
|
import nucleus.factory.RequiresPresenter
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -20,19 +22,18 @@ import java.util.*
|
|||||||
import java.util.concurrent.TimeUnit
|
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.
|
* Uses R.layout.fragment_download_queue.
|
||||||
*/
|
*/
|
||||||
@RequiresPresenter(DownloadPresenter::class)
|
@RequiresPresenter(DownloadPresenter::class)
|
||||||
class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
|
class DownloadActivity : BaseRxActivity<DownloadPresenter>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter containing the active downloads.
|
* Adapter containing the active downloads.
|
||||||
*/
|
*/
|
||||||
private lateinit var adapter: DownloadAdapter
|
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() }
|
private val subscriptions by lazy { CompositeSubscription() }
|
||||||
|
|
||||||
@ -46,38 +47,22 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
|
|||||||
*/
|
*/
|
||||||
private var isRunning: Boolean = false
|
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?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
setAppTheme()
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
setHasOptionsMenu(true)
|
setContentView(R.layout.activity_download_manager)
|
||||||
}
|
setupToolbar(toolbar)
|
||||||
|
|
||||||
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?) {
|
|
||||||
setToolbarTitle(R.string.label_download_queue)
|
setToolbarTitle(R.string.label_download_queue)
|
||||||
|
|
||||||
// Check if download queue is empty and update information accordingly.
|
// Check if download queue is empty and update information accordingly.
|
||||||
setInformationView()
|
setInformationView()
|
||||||
|
|
||||||
// Initialize adapter.
|
// Initialize adapter.
|
||||||
adapter = DownloadAdapter(activity)
|
adapter = DownloadAdapter(this)
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
|
|
||||||
// Set the layout manager for the recycler and fixed size.
|
// Set the layout manager for the recycler and fixed size.
|
||||||
recycler.layoutManager = LinearLayoutManager(activity)
|
recycler.layoutManager = LinearLayoutManager(this)
|
||||||
recycler.setHasFixedSize(true)
|
recycler.setHasFixedSize(true)
|
||||||
|
|
||||||
// Suscribe to changes
|
// Suscribe to changes
|
||||||
@ -94,20 +79,21 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
|
|||||||
.subscribe { onUpdateDownloadedPages(it) }
|
.subscribe { onUpdateDownloadedPages(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroy() {
|
||||||
for (subscription in progressSubscriptions.values) {
|
for (subscription in progressSubscriptions.values) {
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
progressSubscriptions.clear()
|
progressSubscriptions.clear()
|
||||||
subscriptions.clear()
|
subscriptions.clear()
|
||||||
super.onDestroyView()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
inflater.inflate(R.menu.download_queue, menu)
|
menuInflater.inflate(R.menu.download_queue, menu)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||||
// Set start button visibility.
|
// Set start button visibility.
|
||||||
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
|
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
|
||||||
|
|
||||||
@ -116,14 +102,18 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
|
|||||||
|
|
||||||
// Set clear button visibility.
|
// Set clear button visibility.
|
||||||
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
|
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.start_queue -> DownloadService.start(activity)
|
R.id.start_queue -> DownloadService.start(this)
|
||||||
R.id.pause_queue -> DownloadService.stop(activity)
|
R.id.pause_queue -> {
|
||||||
|
DownloadService.stop(this)
|
||||||
|
presenter.pauseDownloads()
|
||||||
|
}
|
||||||
R.id.clear_queue -> {
|
R.id.clear_queue -> {
|
||||||
DownloadService.stop(activity)
|
DownloadService.stop(this)
|
||||||
presenter.clearQueue()
|
presenter.clearQueue()
|
||||||
}
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
@ -198,7 +188,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
|
|||||||
*/
|
*/
|
||||||
private fun onQueueStatusChange(running: Boolean) {
|
private fun onQueueStatusChange(running: Boolean) {
|
||||||
isRunning = running
|
isRunning = running
|
||||||
activity.supportInvalidateOptionsMenu()
|
supportInvalidateOptionsMenu()
|
||||||
|
|
||||||
// Check if download queue is empty and update information accordingly.
|
// Check if download queue is empty and update information accordingly.
|
||||||
setInformationView()
|
setInformationView()
|
||||||
@ -210,7 +200,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
|
|||||||
* @param downloads the downloads from the queue.
|
* @param downloads the downloads from the queue.
|
||||||
*/
|
*/
|
||||||
fun onNextDownloads(downloads: List<Download>) {
|
fun onNextDownloads(downloads: List<Download>) {
|
||||||
activity.supportInvalidateOptionsMenu()
|
supportInvalidateOptionsMenu()
|
||||||
setInformationView()
|
setInformationView()
|
||||||
adapter.setItems(downloads)
|
adapter.setItems(downloads)
|
||||||
}
|
}
|
||||||
@ -247,8 +237,11 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
|
|||||||
* Set information view when queue is empty
|
* Set information view when queue is empty
|
||||||
*/
|
*/
|
||||||
private fun setInformationView() {
|
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)
|
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.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [DownloadFragment].
|
* Presenter of [DownloadActivity].
|
||||||
*/
|
*/
|
||||||
class DownloadPresenter : BasePresenter<DownloadFragment>() {
|
class DownloadPresenter : BasePresenter<DownloadActivity>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download manager.
|
* Download manager.
|
||||||
@ -33,7 +33,7 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
|
|||||||
downloadQueue.getUpdatedObservable()
|
downloadQueue.getUpdatedObservable()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.map { ArrayList(it) }
|
.map { ArrayList(it) }
|
||||||
.subscribeLatestCache(DownloadFragment::onNextDownloads, { view, error ->
|
.subscribeLatestCache(DownloadActivity::onNextDownloads, { view, error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -48,6 +48,13 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
|
|||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses the download queue.
|
||||||
|
*/
|
||||||
|
fun pauseDownloads() {
|
||||||
|
downloadManager.pauseDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the download queue.
|
* Clears the download queue.
|
||||||
*/
|
*/
|
||||||
@ -55,4 +62,4 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
|
|||||||
downloadManager.clearQueue()
|
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.backup.BackupFragment
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
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.latest_updates.LatestUpdatesFragment
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryFragment
|
import eu.kanade.tachiyomi.ui.library.LibraryFragment
|
||||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
|
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_recently_read -> setFragment(RecentlyReadFragment.newInstance(), id)
|
||||||
R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.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_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 -> {
|
R.id.nav_drawer_settings -> {
|
||||||
val intent = Intent(this, SettingsActivity::class.java)
|
val intent = Intent(this, SettingsActivity::class.java)
|
||||||
startActivityForResult(intent, REQUEST_OPEN_SETTINGS)
|
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.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackUpdateService
|
import eu.kanade.tachiyomi.data.track.TrackUpdateService
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
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.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.RetryWithDelay
|
import eu.kanade.tachiyomi.util.RetryWithDelay
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
@ -562,7 +562,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
|||||||
return
|
return
|
||||||
|
|
||||||
// Used to show image notification.
|
// Used to show image notification.
|
||||||
val imageNotifier = ImageNotifier(context)
|
val imageNotifier = SaveImageNotifier(context)
|
||||||
|
|
||||||
// Remove the notification if it already exists (user feedback).
|
// Remove the notification if it already exists (user feedback).
|
||||||
imageNotifier.onClear()
|
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.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@ -7,13 +7,15 @@ import com.bumptech.glide.Glide
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import eu.kanade.tachiyomi.Constants
|
import eu.kanade.tachiyomi.Constants
|
||||||
import eu.kanade.tachiyomi.R
|
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 eu.kanade.tachiyomi.util.notificationManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to show BigPictureStyle notifications
|
* Class used to show BigPictureStyle notifications
|
||||||
*/
|
*/
|
||||||
class ImageNotifier(private val context: Context) {
|
class SaveImageNotifier(private val context: Context) {
|
||||||
/**
|
/**
|
||||||
* Notification builder.
|
* Notification builder.
|
||||||
*/
|
*/
|
||||||
@ -58,15 +60,15 @@ class ImageNotifier(private val context: Context) {
|
|||||||
if (!mActions.isEmpty())
|
if (!mActions.isEmpty())
|
||||||
mActions.clear()
|
mActions.clear()
|
||||||
|
|
||||||
setContentIntent(ImageNotificationReceiver.showImageIntent(context, file))
|
setContentIntent(NotificationHandler.openImagePendingActivity(context, file))
|
||||||
// Share action
|
// Share action
|
||||||
addAction(R.drawable.ic_share_grey_24dp,
|
addAction(R.drawable.ic_share_grey_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
ImageNotificationReceiver.shareImageIntent(context, file))
|
NotificationReceiver.shareImagePendingBroadcast(context, file.absolutePath, notificationId))
|
||||||
// Delete action
|
// Delete action
|
||||||
addAction(R.drawable.ic_delete_grey_24dp,
|
addAction(R.drawable.ic_delete_grey_24dp,
|
||||||
context.getString(R.string.action_delete),
|
context.getString(R.string.action_delete),
|
||||||
ImageNotificationReceiver.deleteImageIntent(context, file.absolutePath, notificationId))
|
NotificationReceiver.deleteImagePendingBroadcast(context, file.absolutePath, notificationId))
|
||||||
updateNotification()
|
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.Notification
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@ -10,6 +13,7 @@ import android.os.PowerManager
|
|||||||
import android.support.annotation.StringRes
|
import android.support.annotation.StringRes
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v4.content.LocalBroadcastManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -95,3 +99,40 @@ val Context.connectivityManager: ConnectivityManager
|
|||||||
val Context.powerManager: PowerManager
|
val Context.powerManager: PowerManager
|
||||||
get() = getSystemService(Context.POWER_SERVICE) as 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
|
<item
|
||||||
android:id="@+id/nav_drawer_downloads"
|
android:id="@+id/nav_drawer_downloads"
|
||||||
android:icon="@drawable/ic_file_download_black_24dp"
|
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>
|
||||||
<group android:id="@+id/group_settings"
|
<group android:id="@+id/group_settings"
|
||||||
android:checkableBehavior="single">
|
android:checkableBehavior="single">
|
||||||
|
@ -260,6 +260,7 @@
|
|||||||
<string name="chapter_downloading">Downloading</string>
|
<string name="chapter_downloading">Downloading</string>
|
||||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
||||||
<string name="chapter_error">Error</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="fetch_chapters_error">Error while fetching chapters</string>
|
||||||
<string name="show_title">Show title</string>
|
<string name="show_title">Show title</string>
|
||||||
<string name="show_chapter_number">Show chapter number</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_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_text_only_wifi">No wifi connection available</string>
|
||||||
<string name="download_notifier_no_network">No network connection available</string>
|
<string name="download_notifier_no_network">No network connection available</string>
|
||||||
|
<string name="download_notifier_download_paused">Download paused</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|