Notification Improvements (#594)

* Download notifier improvements

* Notification improvements

Added a Notification Service.

Added a Notification Activity Handler.

* Removed service. Everything is now managed by single broadcast

* Fixed some flags

* Fixed ReaderActivity call

* Code review

* Added Handler. Removed dismiss onDestroy
This commit is contained in:
Bram van de Kerkhof 2017-01-20 21:18:15 +01:00 committed by inorichi
parent 52c50398b8
commit c445ea90ba
32 changed files with 993 additions and 394 deletions

View file

@ -1,16 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?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"

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.support.v4.content.FileProvider
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.ui.download.DownloadActivity
import eu.kanade.tachiyomi.util.getUriCompat
import java.io.File
/**
* Class that manages [PendingIntent] of activity's
*/
object NotificationHandler {
/**
* Returns [PendingIntent] that starts a download activity.
*
* @param context context of application
*/
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
val intent = Intent(context, DownloadActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
}
return PendingIntent.getActivity(context, 0, intent, 0)
}
/**
* Returns [PendingIntent] that starts a gallery activity
*
* @param context context of application
* @param file file containing image
*/
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
setDataAndType(uri, "image/*")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Returns [PendingIntent] that prompts user with apk install intent
*
* @param context context
* @param file file of apk that is installed
*/
fun installApkPendingActivity(context: Context, file: File): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = file.getUriCompat(context)
setDataAndType(uri, "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(context, 0, intent, 0)
}
}

View file

@ -0,0 +1,277 @@
package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Handler
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.deleteIfExists
import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.toast
import uy.kohesive.injekt.injectLazy
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Global [BroadcastReceiver] that runs on UI thread
* Pending Broadcasts should be made from here.
* NOTE: Use local broadcasts if possible.
*/
class NotificationReceiver : BroadcastReceiver() {
/**
* Download manager.
*/
private val downloadManager: DownloadManager by injectLazy()
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// Dismiss notification
ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
// Resume the download service
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
// Clear the download queue
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
// Launch share activity and dismiss notification
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
// Delete image from path and dismiss notification
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
// Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
// Open reader activity
ACTION_OPEN_CHAPTER -> {
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
}
}
}
/**
* Dismiss the notification
*
* @param notificationId the id of the notification
*/
private fun dismissNotification(context: Context, notificationId: Int) {
context.notificationManager.cancel(notificationId)
}
/**
* Called to start share intent to share image
*
* @param context context of application
* @param path path of file
* @param notificationId id of notification
*/
private fun shareImage(context: Context, path: String, notificationId: Int) {
// Create intent
val intent = Intent(Intent.ACTION_SEND).apply {
val uri = File(path).getUriCompat(context)
putExtra(Intent.EXTRA_STREAM, uri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
type = "image/*"
}
// Dismiss notification
dismissNotification(context, notificationId)
// Launch share activity
context.startActivity(intent)
}
/**
* Starts reader activity
*
* @param context context of application
* @param mangaId id of manga
* @param chapterId id of chapter
*/
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
val db = DatabaseHelper(context)
val manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking()
if (manga != null && chapter != null) {
val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
context.startActivity(intent)
} else {
context.toast(context.getString(R.string.chapter_error))
}
}
/**
* Called to delete image
*
* @param path path of file
* @param notificationId id of notification
*/
private fun deleteImage(context: Context, path: String, notificationId: Int) {
// Dismiss notification
dismissNotification(context, notificationId)
// Delete file
File(path).deleteIfExists()
}
/**
* Method called when user wants to stop a library update
*
* @param context context of application
* @param notificationId id of notification
*/
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
LibraryUpdateService.stop(context)
Handler().post { dismissNotification(context, notificationId) }
}
companion object {
private const val NAME = "NotificationReceiver"
// Called to launch share intent.
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
// Called to delete image.
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
// Called to cancel library update.
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
// Called to open chapter
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
// Value containing file location.
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
// Called to resume downloads.
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
// Called to clear downloads.
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
// Called to dismiss notification.
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
// Value containing notification id.
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
// Value containing manga id.
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
// Value containing chapter id.
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
/**
* Returns a [PendingIntent] that resumes the download of a chapter
*
* @param context context of application
* @return [PendingIntent]
*/
internal fun resumeDownloadsPendingBroadcast(context: Context): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_RESUME_DOWNLOADS
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
/**
* Returns a [PendingIntent] that clears the download queue
*
* @param context context of application
* @return [PendingIntent]
*/
internal fun clearDownloadsPendingBroadcast(context: Context): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_CLEAR_DOWNLOADS
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
/**
* Returns [PendingIntent] that starts a service which dismissed the notification
*
* @param context context of application
* @param notificationId id of notification
* @return [PendingIntent]
*/
internal fun dismissNotificationPendingBroadcast(context: Context, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_DISMISS_NOTIFICATION
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
/**
* Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
*
* @param context context of application
* @param path location path of file
* @param notificationId id of notification
* @return [PendingIntent]
*/
internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHARE_IMAGE
putExtra(EXTRA_FILE_LOCATION, path)
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
/**
* Returns [PendingIntent] that starts a service which removes an image from disk
*
* @param context context of application
* @param path location path of file
* @param notificationId id of notification
* @return [PendingIntent]
*/
internal fun deleteImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_DELETE_IMAGE
putExtra(EXTRA_FILE_LOCATION, path)
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
/**
* Returns [PendingIntent] that start a reader activity containing chapter.
*
* @param context context of application
* @param manga manga of chapter
* @param chapter chapter that needs to be opened
*/
internal fun openChapterPendingBroadcast(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_OPEN_CHAPTER
putExtra(EXTRA_MANGA_ID, manga.id)
putExtra(EXTRA_CHAPTER_ID, chapter.id)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
/**
* Returns [PendingIntent] that starts a service which stops the library update
*
* @param context context of application
* @return [PendingIntent]
*/
internal fun cancelLibraryUpdatePendingBroadcast(context: Context): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_CANCEL_LIBRARY_UPDATE
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
}
}

View file

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.data.updater 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

View file

@ -0,0 +1,144 @@
package eu.kanade.tachiyomi.data.updater
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Local [BroadcastReceiver] that runs on UI thread
* Notification calls from [UpdateDownloaderService] should be made from here.
*/
internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() {
companion object {
private const val NAME = "UpdateDownloaderReceiver"
// Called to show initial notification.
internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL"
// Called to show progress notification.
internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS"
// Called to show install notification.
internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL"
// Called to show error notification
internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR"
// Value containing action of BroadcastReceiver
internal const val EXTRA_ACTION = "$ID.$NAME.ACTION"
// Value containing progress
internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS"
// Value containing apk path
internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH"
// Value containing apk url
internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL"
}
/**
* Notification shown to user
*/
private val notification = NotificationCompat.Builder(context)
override fun onReceive(context: Context, intent: Intent) {
when (intent.getStringExtra(EXTRA_ACTION)) {
NOTIFICATION_UPDATER_INITIAL -> basicNotification()
NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0))
NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH))
NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL))
}
}
/**
* Called to show basic notification
*/
private fun basicNotification() {
// Create notification
with(notification) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
}
notification.show()
}
/**
* Called to show progress notification
*
* @param progress progress of download
*/
private fun updateProgress(progress: Int) {
with(notification) {
setProgress(100, progress, false)
}
notification.show()
}
/**
* Called to show install notification
*
* @param path path of file
*/
private fun installNotification(path: String) {
// Prompt the user to install the new update.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
addAction(R.drawable.ic_system_update_grey_24dp_img,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, File(path)))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
}
notification.show()
}
/**
* Called to show error notification
*
* @param url url of apk
*/
private fun errorNotification(url: String) {
// Prompt the user to retry the download.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setProgress(0, 0, false)
// Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img,
context.getString(R.string.action_retry),
UpdateDownloaderService.downloadApkPendingService(context, url))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
}
notification.show()
}
/**
* Shows a notification from this builder.
*
* @param id the id of the notification.
*/
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) {
context.notificationManager.notify(id, build())
}
}

View file

@ -1,28 +1,160 @@
package eu.kanade.tachiyomi.data.updater 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())
}
}

View file

@ -1,70 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.support.v4.content.FileProvider
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
class UpdateNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_CANCEL_NOTIFICATION -> cancelNotification(context)
}
}
companion object {
// Cancel notification action
const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
fun cancelNotificationIntent(context: Context): PendingIntent {
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
action = ACTION_CANCEL_NOTIFICATION
}
return PendingIntent.getBroadcast(context, 0, intent, 0)
}
/**
* Prompt user with apk install intent
*
* @param context context
* @param file file of apk that is installed
*/
fun installApkIntent(context: Context, file: File): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
else Uri.fromFile(file)
setDataAndType(uri, "application/vnd.android.package-archive")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
cancelNotification(context)
return PendingIntent.getActivity(context, 0, intent, 0)
}
/**
* Downloads a new update and let the user install the new version from a notification.
*
* @param context the application context.
* @param url the url to the new update.
*/
fun downloadApkIntent(context: Context, url: String): PendingIntent {
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun cancelNotification(context: Context) {
context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
}
}
}

View file

@ -2,15 +2,17 @@ package eu.kanade.tachiyomi.ui.download
import android.os.Bundle import android.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()
}
} }

View file

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

View file

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.backup.BackupFragment import eu.kanade.tachiyomi.ui.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)

View file

@ -19,7 +19,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.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()

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.reader.notification package eu.kanade.tachiyomi.ui.reader
import android.content.Context import android.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()
} }

View file

@ -1,84 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.support.v4.content.FileProvider
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
import eu.kanade.tachiyomi.Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID as defaultNotification
/**
* The BroadcastReceiver of [ImageNotifier]
* Intent calls should be made from this class.
*/
class ImageNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_DELETE_IMAGE -> {
deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION))
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, defaultNotification))
}
}
}
/**
* Called to delete image
*
* @param path path of file
*/
private fun deleteImage(path: String) {
val file = File(path)
if (file.exists()) file.delete()
}
companion object {
private const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
private const val EXTRA_FILE_LOCATION = "file_location"
private const val NOTIFICATION_ID = "notification_id"
/**
* Called to start share intent to share image
*
* @param context context of application
* @param file file that contains image
*/
internal fun shareImageIntent(context: Context, file: File): PendingIntent {
val intent = Intent(Intent.ACTION_SEND).apply {
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
putExtra(Intent.EXTRA_STREAM, uri)
type = "image/*"
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Called to show image in gallery application
*
* @param context context of application
* @param file file that contains image
*/
internal fun showImageIntent(context: Context, file: File): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
setDataAndType(uri, "image/*")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(context, 0, intent, 0)
}
internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
action = ACTION_DELETE_IMAGE
putExtra(EXTRA_FILE_LOCATION, path)
putExtra(NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
}

View file

@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.util
import android.app.Notification import android.app.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)
}

View file

@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.util
import android.content.Context
import android.net.Uri
import android.os.Build
import android.support.v4.content.FileProvider
import eu.kanade.tachiyomi.BuildConfig
import java.io.File
/**
* Returns the uri of a file
*
* @param context context of application
*/
fun File.getUriCompat(context: Context): Uri {
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", this)
else Uri.fromFile(this)
return uri
}
/**
* Deletes file if exists
*
* @return success of file deletion
*/
fun File.deleteIfExists(): Boolean {
if (this.exists()) {
this.delete()
return true
}
return false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

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

View file

@ -26,7 +26,8 @@
<item <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">

View file

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