[A12] Automatically update extensions that can be silently updated

This commit is contained in:
Jays2Kings 2021-08-07 00:10:22 -04:00
parent 51ec6d9bf6
commit 9f31529870
11 changed files with 177 additions and 17 deletions

View File

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

View File

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

View File

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

View File

@ -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<ExtensionManager.ExtensionInfo>, 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()
)
}
}

View File

@ -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<ExtensionInfo>()
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<Extension.Available>): Intent {
fun jobIntent(context: Context, extensions: List<Extension.Available>, 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)
}
}
}

View File

@ -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].
*/

View File

@ -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<Extension.Available>) {
private fun createUpdateNotification(extensionsList: List<Extension.Available>) {
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<ExtensionManager>()
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<ExtensionUpdateJob>()
.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<PreferencesHelper>()

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="96dp"
android:height="96dp"
android:viewportWidth="96"
android:viewportHeight="96">
<path
android:fillColor="#FF000000"
android:pathData="M50.9,85H25c-3.9,0 -7,-3.1 -7,-7V18c0,-3.9 3.1,-7 7,-7h46c3.9,0 7,3.1 7,7v37c-2.4,-0.9 -5,-1.4 -7.7,-1.4c-1.6,0 -3.1,0.2 -4.6,0.5C65.4,54 65.2,54 65,54h-3v-9c0,-2.8 -2.2,-5 -5,-5h-8v-2c0,-2.8 -2.2,-5 -5,-5s-5,2.2 -5,5v2h-8c-2.8,0 -5,2.2 -5,5v9h4c2.8,0 5,2.2 5,5s-2.2,5 -5,5h-4v8c0,2.8 2.2,5 5,5h8v-4c0,-2.8 2.2,-5 5,-5c2.3,0 4.3,1.6 4.8,3.8c-0.2,1.1 -0.3,2.3 -0.3,3.5C48.6,78.8 49.4,82.1 50.9,85zM81.7,62.3L66.9,77.1l-6.5,-6.5l-4.8,4.8l11.3,12.3l19.5,-19.5L81.7,62.3z"
android:fillType="evenOdd"/>
</vector>

View File

@ -292,6 +292,7 @@
<!-- Extensions -->
<string name="extensions">Extensions</string>
<string name="extension_updates">Extension Updates</string>
<string name="extension_updates_pending">Extension Updates pending</string>
<string name="extension_info">Extension info</string>
<string name="filter_languages">Filter Languages</string>
<string name="obsolete">Obsolete</string>
@ -321,6 +322,11 @@
<item quantity="one">%d update pending</item>
<item quantity="other">%d updates pending</item>
</plurals>
<string name="extensions_updated">Extensions updated</string>
<plurals name="extensions_updated_plural">
<item quantity="one">Extension updated</item>
<item quantity="other">%d extensions updated</item>
</plurals>
<plurals name="extension_updates_available">
<item quantity="one">Extension update available</item>
<item quantity="other">%d extension updates available</item>
@ -770,6 +776,8 @@
<!-- Browse Settings -->
<string name="pref_global_search">Global search</string>
<string name="check_for_extension_updates">Check for extension updates</string>
<string name="auto_update_extensions">Auto-update extensions</string>
<string name="some_extensions_may_not_update">Some extensions may not be auto-updated if they were installed outside this app</string>
<string name="only_search_pinned_when">Only search pinned sources</string>
<string name="match_pinned_sources">Match pinned sources</string>
<string name="match_enabled_sources">Match enabled sources</string>