diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7f9ce9ad97..00da616d5c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - + @@ -68,7 +68,19 @@ + android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected" + android:enabled="false"> + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 3aaaf00ce7..f85e5a48c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -9,6 +9,8 @@ import android.os.IBinder import android.os.PowerManager import android.support.v4.app.NotificationCompat import android.util.Pair +import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus +import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -16,42 +18,14 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.AndroidComponentUtil -import eu.kanade.tachiyomi.util.DeviceUtil -import eu.kanade.tachiyomi.util.notification -import eu.kanade.tachiyomi.util.notificationManager +import eu.kanade.tachiyomi.util.* import rx.Observable import rx.Subscription import rx.schedulers.Schedulers -import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicInteger import javax.inject.Inject -// Intent key for forced library update -val UPDATE_IS_FORCED = "is_forced" - -/** - * Get the start intent for [LibraryUpdateService]. - * @param context the application context. - * @param isForced true when forcing library update - * @return the intent of the service. - */ -fun getIntent(context: Context, isForced: Boolean = false): Intent { - return Intent(context, LibraryUpdateService::class.java).apply { - putExtra(UPDATE_IS_FORCED, isForced) - } -} - -/** - * Returns the status of the service. - * @param context the application context. - * @return true if the service is running, false otherwise. - */ -fun isRunning(context: Context): Boolean { - return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java) -} - /** * This class will take care of updating the chapters of the manga from the library. It can be * started calling the [start] method. If it's already running, it won't do anything. @@ -77,6 +51,30 @@ class LibraryUpdateService : Service() { companion object { val UPDATE_NOTIFICATION_ID = 1 + // Intent key for manual library update + val UPDATE_IS_MANUAL = "is_manual" + + /** + * Get the start intent for [LibraryUpdateService]. + * @param context the application context. + * @param isManual true when user triggers library update. + * @return the intent of the service. + */ + fun getIntent(context: Context, isManual: Boolean = false): Intent { + return Intent(context, LibraryUpdateService::class.java).apply { + putExtra(UPDATE_IS_MANUAL, isManual) + } + } + + /** + * Returns the status of the service. + * @param context the application context. + * @return true if the service is running, false otherwise. + */ + fun isRunning(context: Context): Boolean { + return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java) + } + /** * Static method to start the service. It will be started only if there isn't another * instance already running. @@ -125,36 +123,52 @@ class LibraryUpdateService : Service() { /** - * Method called when the service receives an intent. In this case, the content of the intent - * is irrelevant, because everything required is fetched in [updateLibrary]. - * @param intent the intent from [start]. + * Method called when the service receives an intent. + * @param intent the start intent from. * @param flags the flags of the command. * @param startId the start id of this command. * @return the start value of the command. */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // If there's no network available, set a component to start this service again when - // a connection is available. - if (!DeviceUtil.isNetworkConnected(this)) { - Timber.i("Sync canceled, connection not available") - showWarningNotification(getString(R.string.notification_no_connection_title), - getString(R.string.notification_no_connection_body)) + + // Get connectivity status + val connection = ReactiveNetwork().getConnectivityStatus(this, true) + + // Get library update restrictions + val restrictions = preferences.libraryUpdateRestriction() + + // Check if users updates library manual + val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false + + // Whether to cancel the update. + var cancelUpdate = false + + // Check if device has internet connection + // Check if device has wifi connection if only wifi is enabled + if (connection == ConnectivityStatus.OFFLINE || ("wifi" in restrictions + && connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) { + + if (isManualUpdate) { + toast(R.string.notification_no_connection_title) + } + + // Enable library update when connection available AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true) + cancelUpdate = true + } + if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) { + AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true) + cancelUpdate = true + } + + if (cancelUpdate) { stopSelf(startId) return Service.START_NOT_STICKY } - // If user doesn't want to update while phone is not charging, cancel sync - else if (preferences.updateOnlyWhenCharging() && !(intent?.getBooleanExtra(UPDATE_IS_FORCED, false) ?: false) && !DeviceUtil.isPowerConnected(this)) { - Timber.i("Sync canceled, not connected to ac power") - // Create force library update intent - val forceIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.FORCE_LIBRARY_UPDATE) - // Show warning - showWarningNotification(getString(R.string.notification_not_connected_to_ac_title), - getString(R.string.notification_not_connected_to_ac_body), forceIntent) - stopSelf(startId) - return Service.START_NOT_STICKY - } + // Stop enabled components. + AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false) + AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false) // Unsubscribe from any previous subscription if needed. subscription?.unsubscribe() @@ -168,20 +182,11 @@ class LibraryUpdateService : Service() { stopSelf(startId) }, { stopSelf(startId) - }) + }) return Service.START_STICKY } - /** - * Creates a PendingIntent for LibraryUpdate broadcast class - * @param action id of action - */ - fun getLibraryUpdateReceiverIntent(action: String): PendingIntent { - return PendingIntent.getBroadcast(this, 0, - Intent(this, LibraryUpdateReceiver::class.java).apply { this.action = action }, 0) - } - /** * Method that updates the library. It's called in a background thread, so it's safe to do * heavy operations or network calls here. @@ -195,7 +200,8 @@ class LibraryUpdateService : Service() { val newUpdates = ArrayList() val failedUpdates = ArrayList() - val cancelIntent = getLibraryUpdateReceiverIntent(LibraryUpdateReceiver.CANCEL_LIBRARY_UPDATE) + val cancelIntent = PendingIntent.getBroadcast(this, 0, + Intent(this, CancelUpdateReceiver::class.java), 0) // Get the manga list that is going to be updated. val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking() @@ -299,12 +305,11 @@ class LibraryUpdateService : Service() { * @param body the body of the notification. */ private fun showNotification(title: String, body: String) { - val n = notification() { + notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() { setSmallIcon(R.drawable.ic_refresh_white_24dp_img) setContentTitle(title) setContentText(body) - } - notificationManager.notify(UPDATE_NOTIFICATION_ID, n) + }) } /** @@ -314,35 +319,15 @@ class LibraryUpdateService : Service() { * @param total the total progress. */ private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) { - val n = notification() { + notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() { setSmallIcon(R.drawable.ic_refresh_white_24dp_img) setContentTitle(manga.title) setProgress(total, current, false) setOngoing(true) addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) - } - notificationManager.notify(UPDATE_NOTIFICATION_ID, n) + }) } - /** - * Show warning message when library can't be updated - * @param warningTitle title of warning - * @param warningBody warning information - * @param pendingIntent Intent called when action clicked - */ - private fun showWarningNotification(warningTitle: String, warningBody: String, pendingIntent: PendingIntent? = null) { - val n = notification() { - setSmallIcon(R.drawable.ic_warning_white_24dp_img) - setContentTitle(warningTitle) - setStyle(NotificationCompat.BigTextStyle().bigText(warningBody)) - setContentIntent(notificationIntent) - if (pendingIntent != null) { - addAction(R.drawable.ic_refresh_grey_24dp_img, getString(R.string.action_force), pendingIntent) - } - setAutoCancel(true) - } - notificationManager.notify(UPDATE_NOTIFICATION_ID, n) - } /** * Shows the notification containing the result of the update done by the service. @@ -353,14 +338,13 @@ class LibraryUpdateService : Service() { val title = getString(R.string.notification_update_completed) val body = getUpdatedMangasBody(updates, failed) - val n = notification() { + notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() { setSmallIcon(R.drawable.ic_refresh_white_24dp_img) setContentTitle(title) setStyle(NotificationCompat.BigTextStyle().bigText(body)) setContentIntent(notificationIntent) setAutoCancel(true) - } - notificationManager.notify(UPDATE_NOTIFICATION_ID, n) + }) } /** @@ -385,7 +369,6 @@ class LibraryUpdateService : Service() { * network changes. */ class SyncOnConnectionAvailable : BroadcastReceiver() { - /** * Method called when a network change occurs. * @param context the application context. @@ -394,35 +377,38 @@ class LibraryUpdateService : Service() { override fun onReceive(context: Context, intent: Intent) { if (DeviceUtil.isNetworkConnected(context)) { AndroidComponentUtil.toggleComponent(context, this.javaClass, false) - context.startService(getIntent(context)) + start(context) } } } /** - * Class that triggers the library to update. + * Class that triggers the library to update when connected to power. */ - class LibraryUpdateReceiver : BroadcastReceiver() { - companion object { - // Cancel library update action - val CANCEL_LIBRARY_UPDATE = "eu.kanade.CANCEL_LIBRARY_UPDATE" - // Force library update - val FORCE_LIBRARY_UPDATE = "eu.kanade.FORCE_LIBRARY_UPDATE" + class SyncOnPowerConnected: BroadcastReceiver() { + /** + * Method called when AC is connected. + * @param context the application context. + * @param intent the intent received. + */ + override fun onReceive(context: Context, intent: Intent) { + AndroidComponentUtil.toggleComponent(context, this.javaClass, false) + start(context) } + } + /** + * 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) { - when (intent.action) { - CANCEL_LIBRARY_UPDATE -> { - LibraryUpdateService.stop(context) - context.notificationManager.cancel(UPDATE_NOTIFICATION_ID) - } - FORCE_LIBRARY_UPDATE -> LibraryUpdateService.start(context, true) - } + LibraryUpdateService.stop(context) + context.notificationManager.cancel(UPDATE_NOTIFICATION_ID) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 1a6ae9ade9..f17b15d263 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -70,16 +70,17 @@ class PreferenceKeys(context: Context) { val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key) - val updateOnlyWhenCharging = context.getString(R.string.pref_update_only_when_charging_key) - val libraryUpdateInterval = context.getString(R.string.pref_library_update_interval_key) + val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key) + val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key) val filterUnread = context.getString(R.string.pref_filter_unread_key) fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId" + fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId" fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 01e91736e2..335302de93 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -39,6 +39,11 @@ class PreferencesHelper(private val context: Context) { context.getString(R.string.pref_library_update_interval_key), 0) } + fun getAutomaticUpdateStatus(context: Context): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + context.getString(R.string.pref_enable_automatic_updates), false) + } + @JvmStatic fun getTheme(context: Context): Int { return PreferenceManager.getDefaultSharedPreferences(context).getInt( @@ -132,10 +137,10 @@ class PreferencesHelper(private val context: Context) { fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false) - fun updateOnlyWhenCharging() = prefs.getBoolean(keys.updateOnlyWhenCharging, false) - fun libraryUpdateInterval() = rxPrefs.getInteger(keys.libraryUpdateInterval, 0) + fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet()) + fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false) fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.java b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.java deleted file mode 100644 index e86098e070..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.java +++ /dev/null @@ -1,99 +0,0 @@ -package eu.kanade.tachiyomi.data.updater; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.AsyncTask; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; - -import javax.inject.Inject; - -import eu.kanade.tachiyomi.App; -import eu.kanade.tachiyomi.data.preference.PreferencesHelper; - -public class UpdateDownloader extends AsyncTask { - /** - * Name of cache directory. - */ - private static final String PARAMETER_CACHE_DIRECTORY = "apk_downloads"; - /** - * Interface to global information about an application environment. - */ - private final Context context; - /** - * Cache directory used for cache management. - */ - private final File cacheDir; - @Inject PreferencesHelper preferencesHelper; - - /** - * Constructor of UpdaterCache. - * - * @param context application environment interface. - */ - public UpdateDownloader(Context context) { - App.get(context).getComponent().inject(this); - this.context = context; - - // Get cache directory from parameter. - cacheDir = new File(preferencesHelper.downloadsDirectory().get(), PARAMETER_CACHE_DIRECTORY); - - // Create cache directory. - createCacheDir(); - } - - /** - * Create cache directory if it doesn't exist - * - * @return true if cache dir is created otherwise false. - */ - @SuppressWarnings("UnusedReturnValue") - private boolean createCacheDir() { - return !cacheDir.exists() && cacheDir.mkdirs(); - } - - - @Override - protected Void doInBackground(String... args) { - try { - createCacheDir(); - - URL url = new URL(args[0]); - HttpURLConnection c = (HttpURLConnection) url.openConnection(); - c.connect(); - - File outputFile = new File(cacheDir, "update.apk"); - if (outputFile.exists()) { - //noinspection ResultOfMethodCallIgnored - outputFile.delete(); - } - FileOutputStream fos = new FileOutputStream(outputFile); - - InputStream is = c.getInputStream(); - - byte[] buffer = new byte[1024]; - int len1; - while ((len1 = is.read(buffer)) != -1) { - fos.write(buffer, 0, len1); - } - fos.close(); - is.close(); - - // Prompt install interface - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(outputFile), "application/vnd.android.package-archive"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // without this flag android returned a intent error! - context.startActivity(intent); - - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } -} - diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt new file mode 100644 index 0000000000..145c3bdeed --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloader.kt @@ -0,0 +1,199 @@ +package eu.kanade.tachiyomi.data.updater + +import android.app.Notification +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.AsyncTask +import android.support.v4.app.NotificationCompat +import eu.kanade.tachiyomi.App +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.network.NetworkHelper +import eu.kanade.tachiyomi.data.network.ProgressListener +import eu.kanade.tachiyomi.data.network.get +import eu.kanade.tachiyomi.util.notificationManager +import eu.kanade.tachiyomi.util.saveTo +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +class UpdateDownloader(private val context: Context) : + AsyncTask() { + + companion object { + /** + * Prompt user with apk install intent + * @param context context + * @param file file of apk that is installed + */ + fun installAPK(context: Context, file: File) { + // Prompt install interface + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive") + // Without this flag android returned a intent error! + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } + } + + @Inject lateinit var network: NetworkHelper + + /** + * Default download dir + */ + val apkFile = File(context.externalCacheDir, "update.apk") + + + /** + * Notification builder + */ + val notificationBuilder = NotificationCompat.Builder(context) + + init { + App.get(context).component.inject(this) + } + + /** + * Class containing download result + * @param url url of file + * @param successful status of download + */ + class DownloadResult(var url: String, var successful: Boolean) + + /** + * Called before downloading + */ + override fun onPreExecute() { + // Create download notification + with(notificationBuilder) { + setContentTitle(context.getString(R.string.update_check_notification_file_download)) + setContentText(context.getString(R.string.update_check_notification_download_in_progress)) + setSmallIcon(android.R.drawable.stat_sys_download) + } + } + + override fun doInBackground(vararg params: String?): DownloadResult { + // Initialize information array containing path and url to file. + val result = DownloadResult(params[0]!!, false) + + // 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 + publishProgress(progress) + } + } + } + + try { + // Make the request and download the file + val response = network.requestBodyProgressBlocking(get(result.url), progressListener) + + if (response.isSuccessful) { + response.body().source().saveTo(apkFile) + // Set download successful + result.successful = true + } + } catch (e: Exception) { + Timber.e(e, e.message) + } + + return result + } + + /** + * Called when progress is updated + * @param values values containing progress + */ + override fun onProgressUpdate(vararg values: Int?) { + // Notify notification manager to update notification + values.getOrNull(0)?.let { + notificationBuilder.setProgress(100, it, false) + // Displays the progress bar on notification + context.notificationManager.notify(InstallOnReceived.notificationId, notificationBuilder.build()) + } + } + + /** + * Called when download done + * @param result string containing download information + */ + override fun onPostExecute(result: DownloadResult) { + with(notificationBuilder) { + if (result.successful) { + setContentTitle(context.getString(R.string.app_name)) + setContentText(context.getString(R.string.update_check_notification_download_complete)) + addAction(R.drawable.ic_system_update_grey_24dp_img, context.getString(R.string.action_install), + getInstallOnReceivedIntent(InstallOnReceived.INSTALL_APK, apkFile.absolutePath)) + addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel), + getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION)) + } else { + setContentText(context.getString(R.string.update_check_notification_download_error)) + addAction(R.drawable.ic_refresh_grey_24dp_img, context.getString(R.string.action_retry), + getInstallOnReceivedIntent(InstallOnReceived.RETRY_DOWNLOAD, result.url)) + addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel), + getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION)) + } + setSmallIcon(android.R.drawable.stat_sys_download_done) + setProgress(0, 0, false) + } + val notification = notificationBuilder.build() + notification.flags = Notification.FLAG_NO_CLEAR + context.notificationManager.notify(InstallOnReceived.notificationId, notification) + } + + /** + * Returns broadcast intent + * @param action action name of broadcast intent + * @param path path of file | url of file + * @return broadcast intent + */ + fun getInstallOnReceivedIntent(action: String, path: String = ""): PendingIntent { + val intent = Intent(context, InstallOnReceived::class.java).apply { + this.action = action + putExtra(InstallOnReceived.FILE_LOCATION, path) + } + return PendingIntent.getBroadcast(context, 0, intent, 0) + } + + + /** + * BroadcastEvent used to install apk or retry download + */ + class InstallOnReceived : BroadcastReceiver() { + companion object { + // Install apk action + val INSTALL_APK = "eu.kanade.INSTALL_APK" + + // Retry download action + val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD" + + // Retry download action + val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION" + + // Absolute path of file || URL of file + val FILE_LOCATION = "file_location" + + // Id of the notification + val notificationId = 2 + } + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + // Install apk. + INSTALL_APK -> UpdateDownloader.installAPK(context, File(intent.getStringExtra(FILE_LOCATION))) + // Retry download. + RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION)) + + CANCEL_NOTIFICATION -> context.notificationManager.cancel(notificationId) + } + } + + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderAlarm.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderAlarm.kt new file mode 100644 index 0000000000..b0258fd076 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateDownloaderAlarm.kt @@ -0,0 +1,109 @@ +package eu.kanade.tachiyomi.data.updater + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.SystemClock +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.DeviceUtil +import eu.kanade.tachiyomi.util.alarmManager +import eu.kanade.tachiyomi.util.notification +import eu.kanade.tachiyomi.util.notificationManager +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class UpdateDownloaderAlarm : BroadcastReceiver() { + + companion object { + const val CHECK_UPDATE_ACTION = "eu.kanade.CHECK_UPDATE" + + /** + * Sets the alarm to run the intent that checks for update + * @param context the application context. + * @param intervalInHours the time in hours when it will be executed. + */ + @JvmStatic + @JvmOverloads + fun startAlarm(context: Context, intervalInHours: Int = 12, isEnabled: Boolean = PreferencesHelper.getAutomaticUpdateStatus(context)) { + // Stop previous running alarms if needed, and do not restart it if the interval is 0. + UpdateDownloaderAlarm.stopAlarm(context) + if (intervalInHours == 0 || !isEnabled) + return + + // Get the time the alarm should fire the event to update. + val intervalInMillis = intervalInHours * 60 * 60 * 1000 + val nextRun = SystemClock.elapsedRealtime() + intervalInMillis + + // Start the alarm. + val pendingIntent = getPendingIntent(context) + context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + nextRun, intervalInMillis.toLong(), pendingIntent) + } + + /** + * Stops the alarm if it's running. + * @param context the application context. + */ + fun stopAlarm(context: Context) { + val pendingIntent = getPendingIntent(context) + context.alarmManager.cancel(pendingIntent) + } + + /** + * Returns broadcast intent + * @param context the application context. + * @return broadcast intent + */ + fun getPendingIntent(context: Context): PendingIntent { + return PendingIntent.getBroadcast(context, 0, + Intent(context, UpdateDownloaderAlarm::class.java).apply { + this.action = CHECK_UPDATE_ACTION + }, PendingIntent.FLAG_UPDATE_CURRENT) + } + } + + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + // Start the alarm when the system is booted. + Intent.ACTION_BOOT_COMPLETED -> startAlarm(context) + // Update the library when the alarm fires an event. + CHECK_UPDATE_ACTION -> checkVersion(context) + } + } + + fun checkVersion(context: Context) { + if (DeviceUtil.isNetworkConnected(context)) { + val updateChecker = GithubUpdateChecker(context) + updateChecker.checkForApplicationUpdate() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ release -> + //Get version of latest release + var newVersion = release.version + newVersion = newVersion.replace("[^\\d.]".toRegex(), "") + + //Check if latest version is different from current version + if (newVersion != BuildConfig.VERSION_NAME) { + val downloadLink = release.downloadLink + + val n = context.notification() { + setContentTitle(context.getString(R.string.update_check_notification_update_available)) + addAction(android.R.drawable.stat_sys_download_done, context.getString(eu.kanade.tachiyomi.R.string.action_download), + UpdateDownloader(context).getInstallOnReceivedIntent(UpdateDownloader.InstallOnReceived.RETRY_DOWNLOAD, downloadLink)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + } + // Displays the progress bar on notification + context.notificationManager.notify(0, n); + } + }, { + it.printStackTrace() + }) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt index 06bf3a7146..efe2218dfd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt @@ -73,5 +73,4 @@ open class BaseActivity : AppCompatActivity() { snack.f() snack.show() } - -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt index e094d56245..edefee6b20 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt @@ -1,12 +1,14 @@ package eu.kanade.tachiyomi.ui.setting import android.os.Bundle +import android.support.v7.preference.SwitchPreferenceCompat import android.view.View import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.UpdateDownloader +import eu.kanade.tachiyomi.data.updater.UpdateDownloaderAlarm import eu.kanade.tachiyomi.util.toast import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -27,6 +29,10 @@ class SettingsAboutFragment : SettingsNestedFragment() { */ private var releaseSubscription: Subscription? = null + val automaticUpdateToggle by lazy { + findPreference(getString(R.string.pref_enable_automatic_updates_key)) as SwitchPreferenceCompat + } + companion object { fun newInstance(resourcePreference: Int, resourceTitle: Int): SettingsNestedFragment { @@ -45,11 +51,25 @@ class SettingsAboutFragment : SettingsNestedFragment() { else BuildConfig.VERSION_NAME - //Set onClickListener to check for new version - version.setOnPreferenceClickListener { - if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) - checkVersion() - true + if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER) { + //Set onClickListener to check for new version + version.setOnPreferenceClickListener { + true + } + + automaticUpdateToggle.isEnabled = true + automaticUpdateToggle.setOnPreferenceChangeListener { preference, any -> + val status = any as Boolean + UpdateDownloaderAlarm.startAlarm(activity, 12, status) + true + } + + automaticUpdateToggle.isEnabled = true + automaticUpdateToggle.setOnPreferenceChangeListener { preference, any -> + val status = any as Boolean + UpdateDownloaderAlarm.startAlarm(activity, 12, status) + true + } } buildTime.summary = getFormattedBuildTime() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt index d99741fc8b..2e81d577b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralFragment.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.setting import android.content.Intent import android.os.Bundle +import android.support.v14.preference.MultiSelectListPreference import android.support.v4.app.TaskStackBuilder import android.support.v7.preference.Preference import android.view.View @@ -33,13 +34,28 @@ class SettingsGeneralFragment : SettingsNestedFragment() { findPreference(getString(R.string.pref_library_update_interval_key)) as IntListPreference } + val updateRestriction by lazy { + findPreference(getString(R.string.pref_library_update_restriction_key)) as MultiSelectListPreference + } + val themePreference by lazy { findPreference(getString(R.string.pref_theme_key)) as IntListPreference } + var updateIntervalSubscription: Subscription? = null + var columnsSubscription: Subscription? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + updateIntervalSubscription = preferences.libraryUpdateInterval().asObservable() + .subscribe { updateRestriction.isVisible = it > 0 } + + columnsSubscription = Observable.combineLatest( + preferences.portraitColumns().asObservable(), + preferences.landscapeColumns().asObservable()) + { portraitColumns, landscapeColumns -> Pair(portraitColumns, landscapeColumns) } + .subscribe { updateColumnsSummary(it.first, it.second) } + updateInterval.setOnPreferenceChangeListener { preference, newValue -> LibraryUpdateAlarm.startAlarm(activity, (newValue as String).toInt()) true @@ -57,17 +73,11 @@ class SettingsGeneralFragment : SettingsNestedFragment() { } } - override fun onResume() { - super.onResume() - columnsSubscription = Observable.combineLatest(preferences.portraitColumns().asObservable(), - preferences.landscapeColumns().asObservable(), - { portraitColumns, landscapeColumns -> Pair(portraitColumns, landscapeColumns) }) - .subscribe { updateColumnsSummary(it.first, it.second) } - } - - override fun onPause() { + override fun onDestroyView() { + updateIntervalSubscription?.unsubscribe() columnsSubscription?.unsubscribe() - super.onPause() + super.onDestroyView() + } override fun onDisplayPreferenceDialog(preference: Preference) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/DeviceUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/DeviceUtil.kt index beab475dd6..e7dc9e5596 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/DeviceUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/DeviceUtil.kt @@ -11,7 +11,7 @@ object DeviceUtil { val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) intent?.let { val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) - return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB + return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS } return false } diff --git a/app/src/main/res/drawable-hdpi/ic_system_update_grey_24dp_img.png b/app/src/main/res/drawable-hdpi/ic_system_update_grey_24dp_img.png new file mode 100644 index 0000000000..51cf94d80c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_system_update_grey_24dp_img.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_system_update_grey_24dp_img.png b/app/src/main/res/drawable-mdpi/ic_system_update_grey_24dp_img.png new file mode 100644 index 0000000000..33cca7074e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_system_update_grey_24dp_img.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_system_update_grey_24dp_img.png b/app/src/main/res/drawable-xhdpi/ic_system_update_grey_24dp_img.png new file mode 100644 index 0000000000..37db7d1d40 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_system_update_grey_24dp_img.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_system_update_grey_24dp_img.png b/app/src/main/res/drawable-xxhdpi/ic_system_update_grey_24dp_img.png new file mode 100644 index 0000000000..d24aba7b29 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_system_update_grey_24dp_img.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_system_update_grey_24dp_img.png b/app/src/main/res/drawable-xxxhdpi/ic_system_update_grey_24dp_img.png new file mode 100644 index 0000000000..8fa116da79 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_system_update_grey_24dp_img.png differ diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index ce38a3ebad..40b2c301cd 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -126,4 +126,14 @@ 48 + + @string/wifi + @string/charging + + + + wifi + ac + + \ No newline at end of file diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index d7cb6da960..c0f8b2ecc2 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -13,10 +13,10 @@ pref_library_columns_landscape_key pref_library_update_interval_key pref_update_only_non_completed_key - pref_update_only_when_charging_key pref_auto_update_manga_sync_key pref_ask_update_manga_sync_key pref_theme_key + library_update_restriction pref_default_viewer_key pref_image_scale_type_key @@ -52,8 +52,10 @@ pref_clear_chapter_cache_key pref_clear_database_key + pref_version pref_build_time + pref_enable_automatic_updates_key pref_display_catalogue_as_list pref_last_catalogue_source_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b57a2edabb..fec7a6ee5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Cancel Sort Force refresh + Install Deleting… @@ -72,8 +73,6 @@ Landscape Default Library update frequency - Only update incomplete manga - Only update when charging Manual Hourly Every 2 hours @@ -82,13 +81,17 @@ Every 12 hours Daily Every 2 days + Library update restrictions + Update only when the conditions are met + Wi-Fi + Charging + Only update incomplete manga Sync chapters after reading Confirm before updating Application theme Main theme Dark theme - Hide status bar Lock orientation @@ -166,7 +169,8 @@ Version Build time - + Check for updates + Automatically check for application updates Send crash reports Helps fix any bugs. No sensitive data will be sent @@ -282,6 +286,13 @@ Download started Looking for updates + + Download update + Download in progress + Download complete + Download error + Update available + Backdrop image of selected manga Cover of selected manga diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml index d15d58c35b..8012999aa1 100644 --- a/app/src/main/res/xml/pref_about.xml +++ b/app/src/main/res/xml/pref_about.xml @@ -3,19 +3,26 @@ xmlns:android="http://schemas.android.com/apk/res/android"> + android:title="@string/pref_enable_acra"/> + + + android:persistent="false" + android:title="@string/version"/> + android:persistent="false" + android:title="@string/build_time"/> \ No newline at end of file diff --git a/app/src/main/res/xml/pref_advanced.xml b/app/src/main/res/xml/pref_advanced.xml index 035a98d8cf..ae20a4843a 100644 --- a/app/src/main/res/xml/pref_advanced.xml +++ b/app/src/main/res/xml/pref_advanced.xml @@ -3,13 +3,13 @@ xmlns:android="http://schemas.android.com/apk/res/android"> + android:key="@string/pref_clear_chapter_cache_key" + android:title="@string/pref_clear_chapter_cache"/> + android:summary="@string/pref_clear_database_summary" + android:title="@string/pref_clear_database"/> + + - - \ No newline at end of file diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarmTest.java b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarmTest.java index d6f7115fe6..bee54844a6 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarmTest.java +++ b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateAlarmTest.java @@ -101,7 +101,7 @@ public class LibraryUpdateAlarmTest { @Test public void testLibraryUpdateServiceIsStartedWhenUpdateIntentIsReceived() { Intent intent = new Intent(context, LibraryUpdateService.class); - intent.putExtra("is_forced", false); + intent.putExtra("is_manual", false); assertThat(app.getNextStartedService()).isNotEqualTo(intent); LibraryUpdateAlarm alarm = new LibraryUpdateAlarm();