diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt index c04e3d47d6..a154e2a64e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -47,11 +47,14 @@ object Notifications { /** * Notification channel and ids used by the library updater. */ + private const val GROUP_EXTENSION_UPDATES = "group_extension_updates" const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel" const val ID_UPDATES_TO_EXTS = -401 const val CHANNEL_EXT_PROGRESS = "ext_update_progress_channel" const val ID_EXTENSION_PROGRESS = -402 + const val CHANNEL_EXT_UPDATED = "ext_updated_channel" + const val ID_UPDATED_EXTS = -403 private const val GROUP_BACKUP_RESTORE = "group_backup_restore" const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel" @@ -84,6 +87,7 @@ object Notifications { listOf( NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.backup_and_restore)), + NotificationChannelGroup(GROUP_EXTENSION_UPDATES, context.getString(R.string.extension_updates)), ).forEach(context.notificationManager::createNotificationChannelGroup) val channels = listOf( @@ -108,9 +112,11 @@ object Notifications { }, NotificationChannel( CHANNEL_UPDATES_TO_EXTS, - context.getString(R.string.extension_updates), + context.getString(R.string.extension_updates_pending), NotificationManager.IMPORTANCE_DEFAULT - ), + ).apply { + group = GROUP_EXTENSION_UPDATES + }, NotificationChannel( CHANNEL_NEW_CHAPTERS, context.getString(R.string.new_chapters), @@ -147,9 +153,17 @@ object Notifications { context.getString(R.string.updating_extensions), NotificationManager.IMPORTANCE_LOW ).apply { + group = GROUP_EXTENSION_UPDATES setShowBadge(false) setSound(null, null) }, + NotificationChannel( + CHANNEL_EXT_UPDATED, + context.getString(R.string.extensions_updated), + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + group = GROUP_EXTENSION_UPDATES + }, NotificationChannel( CHANNEL_UPDATED, context.getString(R.string.update_completed), 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 a8320b8fd7..bed9ffce5a 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 @@ -231,6 +231,8 @@ object PreferenceKeys { const val shouldAutoUpdate = "should_auto_update" + const val autoUpdateExtensions = "auto_update_extensions" + const val defaultChapterFilterByRead = "default_chapter_filter_by_read" const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded" 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 87975c25fd..457b40968f 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 @@ -431,6 +431,8 @@ class PreferencesHelper(val context: Context) { fun shouldAutoUpdate() = prefs.getInt(Keys.shouldAutoUpdate, AutoUpdaterJob.ONLY_ON_UNMETERED) + fun autoUpdateExtensions() = prefs.getInt(Keys.autoUpdateExtensions, AutoUpdaterJob.ONLY_ON_UNMETERED) + fun filterChapterByRead() = flowPrefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL) fun filterChapterByDownloaded() = flowPrefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt index f2109ff42e..c6071d2225 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt @@ -29,7 +29,7 @@ class ExtensionInstallNotifier(private val context: Context) { * Cached progress notification to avoid creating a lot. */ val progressNotificationBuilder by lazy { - context.notificationBuilder(Notifications.CHANNEL_UPDATES_TO_EXTS) { + context.notificationBuilder(Notifications.CHANNEL_EXT_PROGRESS) { setContentTitle(context.getString(R.string.app_name)) setSmallIcon(android.R.drawable.stat_sys_download) setLargeIcon(notificationBitmap) @@ -54,9 +54,35 @@ class ExtensionInstallNotifier(private val context: Context) { context.notificationManager.notify( Notifications.ID_EXTENSION_PROGRESS, progressNotificationBuilder + .setChannelId(Notifications.CHANNEL_EXT_PROGRESS) .setContentTitle(context.getString(R.string.updating_extensions)) .setProgress(max, progress, progress == 0) .build() ) } + + fun showUpdatedNotification(extensions: List, hideContent: Boolean) { + context.notificationManager.notify( + Notifications.ID_UPDATED_EXTS, + progressNotificationBuilder + .setChannelId(Notifications.CHANNEL_EXT_UPDATED) + .setContentTitle( + context.resources.getQuantityString( + R.plurals.extensions_updated_plural, + extensions.size, + extensions.size + ) + ) + .setSmallIcon(R.drawable.ic_extension_updated_24dp) + .setOngoing(false) + .setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context)) + .clearActions() + .setProgress(0, 0, false).apply { + if (!hideContent) { + setContentText(extensions.joinToString(", ") { it.name }) + } + } + .build() + ) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt index 26f7ccd80d..167bf65367 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt @@ -5,9 +5,11 @@ import android.content.Context import android.content.Intent import android.os.IBinder import android.os.PowerManager +import androidx.work.NetworkType import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.util.system.notificationManager @@ -26,6 +28,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.ArrayList import java.util.concurrent.TimeUnit +import kotlin.math.max class ExtensionInstallService( val extensionManager: ExtensionManager = Injekt.get(), @@ -63,7 +66,11 @@ class ExtensionInstallService( */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) return START_NOT_STICKY - if (!preferences.hasPromptedBeforeUpdateAll().get()) { + val showUpdated = intent.getIntExtra(KEY_SHOW_UPDATED, 0) + val showUpdatedNotification = showUpdated > 0 + val reRunUpdateCheck = showUpdated > 1 + + if (!showUpdatedNotification && !preferences.hasPromptedBeforeUpdateAll().get()) { toast(R.string.some_extensions_may_prompt) preferences.hasPromptedBeforeUpdateAll().set(true) } @@ -79,14 +86,19 @@ class ExtensionInstallService( } ?: return START_NOT_STICKY var installed = 0 + val installedExtensions = mutableListOf() job = serviceScope.launch { - val results = list.map { + val results = list.map { extension -> async { requestSemaphore.withPermit { - extensionManager.installExtension(it, serviceScope) + extensionManager.installExtension(extension, serviceScope) .collect { if (it.first.isCompleted()) { + installedExtensions.add(extension) installed++ + val prefCount = + preferences.extensionUpdatesCount().getOrDefault() + preferences.extensionUpdatesCount().set(max(prefCount - 1, 0)) } notifier.showProgressNotification(installed, list.size) } @@ -95,9 +107,18 @@ class ExtensionInstallService( } results.awaitAll() } - job?.invokeOnCompletion { stopSelf(startId) } - return START_REDELIVER_INTENT + job?.invokeOnCompletion { + if (showUpdatedNotification) { + notifier.showUpdatedNotification(installedExtensions, preferences.hideNotificationContent()) + } + if (reRunUpdateCheck || installedExtensions.size != list.size) { + ExtensionUpdateJob.runJobAgain(this, NetworkType.CONNECTED) + } + stopSelf(startId) + } + + return START_NOT_STICKY } /** @@ -149,11 +170,13 @@ class ExtensionInstallService( * Key that defines what should be updated. */ private const val KEY_EXTENSION = "extension" + private const val KEY_SHOW_UPDATED = "show_updated" - fun jobIntent(context: Context, extensions: List): Intent { + fun jobIntent(context: Context, extensions: List, showUpdatedExtension: Int = 0): Intent { return Intent(context, ExtensionInstallService::class.java).apply { val info = extensions.map(::ExtensionInfo) putParcelableArrayListExtra(KEY_EXTENSION, ArrayList(info)) + putExtra(KEY_SHOW_UPDATED, showUpdatedExtension) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 828e40c949..4ed3ed4b27 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -182,6 +182,10 @@ class ExtensionManager( return untrustedExtensionsRelay.asObservable() } + fun isInstalledByApp(extension: Extension.Available): Boolean { + return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName) + } + /** * Finds the available extensions in the [api] and updates [availableExtensions]. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt index cf7284d066..4f8c6458d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt @@ -9,7 +9,9 @@ import androidx.core.content.ContextCompat import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters @@ -18,8 +20,10 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.updater.AutoUpdaterJob import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.notification import kotlinx.coroutines.coroutineScope import uy.kohesive.injekt.Injekt @@ -44,15 +48,40 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam Result.success() } - private fun createUpdateNotification(extensions: List) { + private fun createUpdateNotification(extensionsList: List) { + val extensions = extensionsList.toMutableList() val preferences: PreferencesHelper by injectLazy() preferences.extensionUpdatesCount().set(extensions.size) - // Not doing this yet since users will get prompted while device is idle -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preferences.autoUpdateExtensions()) { -// val intent = ExtensionInstallService.jobIntent(context, extensions) -// context.startForegroundService(intent) -// return -// } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preferences.autoUpdateExtensions() != AutoUpdaterJob.NEVER) { + val cm = context.connectivityManager + if ( + preferences.autoUpdateExtensions() == AutoUpdaterJob.ALWAYS || + !cm.isActiveNetworkMetered + ) { + val extensionManager = Injekt.get() + val extensionsInstalledByApp = + extensions.filter { extensionManager.isInstalledByApp(it) } + val intent = + ExtensionInstallService.jobIntent( + context, + extensionsInstalledByApp, + // Re reun this job if not all the extensions can be auto updated + if (extensionsInstalledByApp.size == extensions.size) { + 1 + } else { + 2 + } + ) + context.startForegroundService(intent) + if (extensionsInstalledByApp.size == extensions.size) { + return + } else { + extensions.removeAll(extensionsInstalledByApp) + } + } else { + runJobAgain(context, NetworkType.UNMETERED) + } + } NotificationManagerCompat.from(context).apply { notify( Notifications.ID_UPDATES_TO_EXTS, @@ -74,7 +103,9 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam context ) ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + extensions.size == extensionsList.size + ) { val intent = ExtensionInstallService.jobIntent(context, extensions) val pendingIntent = PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) @@ -92,6 +123,20 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam companion object { private const val TAG = "ExtensionUpdate" + private const val AUTO_TAG = "AutoExtensionUpdate" + + fun runJobAgain(context: Context, networkType: NetworkType) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(networkType) + .build() + val request = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .addTag(AUTO_TAG) + .build() + + WorkManager.getInstance(context) + .enqueueUniqueWork(AUTO_TAG, ExistingWorkPolicy.REPLACE, request) + } fun setupTask(context: Context, forceAutoUpdateJob: Boolean? = null) { val preferences = Injekt.get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 4a28751433..81644f3336 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -4,7 +4,9 @@ import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.os.Build import dalvik.system.PathClassLoader +import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -85,6 +87,14 @@ internal object ExtensionLoader { return loadExtension(context, pkgName, pkgInfo) } + fun isExtensionInstalledByApp(context: Context, pkgName: String): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + context.packageManager.getInstallSourceInfo(pkgName).installingPackageName + } else { + context.packageManager.getInstallerPackageName(pkgName) + } == BuildConfig.APPLICATION_ID + } + /** * Loads an extension given its package name. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index c89f55b6aa..a73eccf158 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -1,10 +1,12 @@ package eu.kanade.tachiyomi.ui.setting +import android.os.Build import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.data.updater.AutoUpdaterJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.main.MainActivity @@ -34,6 +36,20 @@ class SettingsBrowseController : SettingsController() { true } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + intListPreference(activity) { + key = PreferenceKeys.autoUpdateExtensions + titleRes = R.string.auto_update_extensions + entryRange = 0..2 + entriesRes = arrayOf( + R.string.over_any_network, + R.string.over_wifi_only, + R.string.dont_auto_update + ) + defaultValue = AutoUpdaterJob.ONLY_ON_UNMETERED + } + infoPreference(R.string.some_extensions_may_not_update) + } } preferenceCategory { diff --git a/app/src/main/res/drawable/ic_extension_updated_24dp.xml b/app/src/main/res/drawable/ic_extension_updated_24dp.xml new file mode 100644 index 0000000000..2f58cd7c48 --- /dev/null +++ b/app/src/main/res/drawable/ic_extension_updated_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f14206b6d..6a7c3b2e42 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -292,6 +292,7 @@ Extensions Extension Updates + Extension Updates pending Extension info Filter Languages Obsolete @@ -321,6 +322,11 @@ %d update pending %d updates pending + Extensions updated + + Extension updated + %d extensions updated + Extension update available %d extension updates available @@ -770,6 +776,8 @@ Global search Check for extension updates + Auto-update extensions + Some extensions may not be auto-updated if they were installed outside this app Only search pinned sources Match pinned sources Match enabled sources