浏览代码

Option to auto check for extension updates (#2680)

* Option to auto check for extension updates

* Addressing comments

* Added foreground check for extensions

* Added Extension Preference widget
Jays2Kings 5 年之前
父节点
当前提交
9585f9a1a6

+ 14 - 0
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt

@@ -437,5 +437,19 @@ class NotificationReceiver : BroadcastReceiver() {
             }
             return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
         }
+
+        /**
+         * Returns [PendingIntent] that opens the extensions controller.
+         *
+         * @param context context of application
+         */
+        internal fun openExtensionsPendingActivity(context: Context): PendingIntent {
+            val newIntent =
+                Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_EXTENSIONS)
+                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+            return PendingIntent.getActivity(
+                context, 0, newIntent, PendingIntent.FLAG_UPDATE_CURRENT
+            )
+        }
     }
 }

+ 10 - 1
app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt

@@ -39,6 +39,12 @@ object Notifications {
     const val ID_NEW_CHAPTERS = -301
     const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
 
+    /**
+     * Notification channel and ids used by the library updater.
+     */
+    const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
+    const val ID_UPDATES_TO_EXTS = -401
+
     /**
      * Creates the notification channels introduced in Android Oreo.
      *
@@ -59,7 +65,10 @@ object Notifications {
                     setShowBadge(false)
                 },
                 NotificationChannel(CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
-                        NotificationManager.IMPORTANCE_DEFAULT)
+                        NotificationManager.IMPORTANCE_DEFAULT),
+                NotificationChannel(CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates),
+                        NotificationManager.IMPORTANCE_DEFAULT
+        )
         )
         context.notificationManager.createNotificationChannels(channels)
     }

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt

@@ -107,6 +107,8 @@ object PreferenceKeys {
 
     const val automaticUpdates = "automatic_updates"
 
+    const val automaticExtUpdates = "automatic_ext_updates"
+
     const val startScreen = "start_screen"
 
     const val useBiometricLock = "use_biometric_lock"

+ 6 - 0
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt

@@ -190,6 +190,12 @@ class PreferencesHelper(val context: Context) {
 
     fun automaticUpdates() = prefs.getBoolean(Keys.automaticUpdates, true)
 
+    fun automaticExtUpdates() = rxPrefs.getBoolean(Keys.automaticExtUpdates, false)
+
+    fun extensionUpdatesCount() = rxPrefs.getInteger("ext_updates_count", 0)
+
+    fun lastExtCheck() = rxPrefs.getLong("last_ext_check", 0)
+
     fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
 
     fun downloadNew() = rxPrefs.getBoolean(Keys.downloadNew, false)

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt

@@ -162,6 +162,7 @@ class ExtensionManager(
      */
     private fun updatedInstalledExtensionsStatuses(availableExtensions: List<Extension.Available>) {
         if (availableExtensions.isEmpty()) {
+            preferences.extensionUpdatesCount().set(0)
             return
         }
 
@@ -186,6 +187,7 @@ class ExtensionManager(
         if (changed) {
             installedExtensions = mutInstalledExtensions
         }
+        preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
     }
 
     /**
@@ -316,10 +318,12 @@ class ExtensionManager(
 
         override fun onExtensionInstalled(extension: Extension.Installed) {
             registerNewExtension(extension.withUpdateCheck())
+            preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
         }
 
         override fun onExtensionUpdated(extension: Extension.Installed) {
             registerUpdatedExtension(extension.withUpdateCheck())
+            preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
         }
 
         override fun onExtensionUntrusted(extension: Extension.Untrusted) {
@@ -328,6 +332,7 @@ class ExtensionManager(
 
         override fun onPackageUninstalled(pkgName: String) {
             unregisterExtension(pkgName)
+            preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
         }
     }
 

+ 86 - 0
app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt

@@ -0,0 +1,86 @@
+package eu.kanade.tachiyomi.extension
+
+import android.content.Context
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import eu.kanade.tachiyomi.R
+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.extension.api.ExtensionGithubApi
+import eu.kanade.tachiyomi.util.system.notification
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.coroutineScope
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
+    CoroutineWorker(context, workerParams) {
+
+    override suspend fun doWork(): Result = coroutineScope {
+        val pendingUpdates = try {
+            ExtensionGithubApi().checkForUpdates(context)
+        } catch (e: Exception) {
+            return@coroutineScope Result.failure()
+        }
+        if (pendingUpdates.isNotEmpty()) {
+            val names = pendingUpdates.map { it.name }
+            NotificationManagerCompat.from(context).apply {
+                notify(Notifications.ID_UPDATES_TO_EXTS,
+                    context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) {
+                        setContentTitle(
+                            context.resources.getQuantityString(
+                                R.plurals.update_check_notification_ext_updates,
+                                names.size,
+                                names.size
+                            )
+                        )
+                        val extNames = names.joinToString(", ")
+                        setContentText(extNames)
+                        setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
+                        setSmallIcon(R.drawable.ic_extension_24dp)
+                        setContentIntent(
+                            NotificationReceiver.openExtensionsPendingActivity(
+                                context
+                            )
+                        )
+                        setAutoCancel(true)
+                    })
+            }
+        }
+        Result.success()
+    }
+
+    companion object {
+        const val TAG = "ExtensionUpdate"
+
+        fun setupTask(context: Context, forceAutoUpdateJob: Boolean? = null) {
+            val preferences = Injekt.get<PreferencesHelper>()
+            val autoUpdateJob = forceAutoUpdateJob ?: preferences.automaticExtUpdates().getOrDefault()
+            if (autoUpdateJob) {
+                val constraints = Constraints.Builder()
+                    .setRequiredNetworkType(NetworkType.CONNECTED)
+                    .build()
+
+                val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
+                    12, TimeUnit.HOURS,
+                    1, TimeUnit.HOURS)
+                    .addTag(TAG)
+                    .setConstraints(constraints)
+                    .build()
+
+                WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
+            } else {
+                WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
+            }
+        }
+    }
+}

+ 32 - 1
app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt

@@ -1,5 +1,6 @@
 package eu.kanade.tachiyomi.extension.api
 
+import android.content.Context
 import com.github.salomonbrys.kotson.fromJson
 import com.github.salomonbrys.kotson.get
 import com.github.salomonbrys.kotson.int
@@ -7,6 +8,7 @@ import com.github.salomonbrys.kotson.string
 import com.google.gson.Gson
 import com.google.gson.JsonArray
 import eu.kanade.tachiyomi.extension.model.Extension
+import eu.kanade.tachiyomi.extension.model.LoadResult
 import eu.kanade.tachiyomi.extension.util.ExtensionLoader
 import eu.kanade.tachiyomi.network.GET
 import eu.kanade.tachiyomi.network.NetworkHelper
@@ -23,13 +25,41 @@ internal class ExtensionGithubApi {
     private val gson: Gson by injectLazy()
 
     suspend fun findExtensions(): List<Extension.Available> {
-        val call = GET("$REPO_URL/index.json")
+        val call = GET(EXT_URL)
 
         return withContext(Dispatchers.IO) {
             parseResponse(network.client.newCall(call).await())
         }
     }
 
+    suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
+        return withContext(Dispatchers.IO) {
+            val call = GET(EXT_URL)
+            val response = network.client.newCall(call).await()
+
+            if (response.isSuccessful) {
+                val extensions = parseResponse(response)
+                val extensionsWithUpdate = mutableListOf<Extension.Installed>()
+
+                val installedExtensions = ExtensionLoader.loadExtensions(context)
+                    .filterIsInstance<LoadResult.Success>()
+                    .map { it.extension }
+                for (installedExt in installedExtensions) {
+                    val pkgName = installedExt.pkgName
+                    val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
+
+                    val hasUpdate = availableExt.versionCode > installedExt.versionCode
+                    if (hasUpdate) extensionsWithUpdate.add(installedExt)
+                }
+
+                extensionsWithUpdate
+            } else {
+                response.close()
+                throw Exception("Failed to get extensions")
+            }
+        }
+    }
+
     private fun parseResponse(response: Response): List<Extension.Available> {
         val text = response.body?.use { it.string() } ?: return emptyList()
 
@@ -60,5 +90,6 @@ internal class ExtensionGithubApi {
 
     companion object {
         private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
+        private const val EXT_URL = "$REPO_URL/index.json"
     }
 }

+ 17 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionController.kt

@@ -17,11 +17,15 @@ import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
 import eu.kanade.tachiyomi.extension.model.Extension
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
-import kotlinx.android.synthetic.main.extension_controller.ext_recycler
-import kotlinx.android.synthetic.main.extension_controller.ext_swipe_refresh
+import kotlinx.android.synthetic.main.extension_controller.*
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 
 /**
  * Controller to manage the catalogues available in the app.
@@ -86,6 +90,13 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
                         .popChangeHandler(SettingsExtensionsFadeChangeHandler())
                         .pushChangeHandler(FadeChangeHandler()))
             }
+            R.id.action_auto_check -> {
+                item.isChecked = !item.isChecked
+                val preferences: PreferencesHelper = Injekt.get()
+                preferences.automaticExtUpdates().set(item.isChecked)
+                ExtensionUpdateJob.setupTask(activity!!, item.isChecked)
+            }
+            else -> return super.onOptionsItemSelected(item)
         }
         return super.onOptionsItemSelected(item)
     }
@@ -138,6 +149,10 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
 
         // Fixes problem with the overflow icon showing up in lieu of search
         searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
+
+        val autoItem = menu.findItem(R.id.action_auto_check)
+        val preferences: PreferencesHelper = Injekt.get()
+        autoItem.isChecked = preferences.automaticExtUpdates().getOrDefault()
     }
 
     override fun onItemClick(view: View, position: Int): Boolean {

+ 52 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -13,6 +13,9 @@ import com.bluelinelabs.conductor.RouterTransaction
 import eu.kanade.tachiyomi.Migrations
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.notification.NotificationReceiver
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
 import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 import eu.kanade.tachiyomi.ui.base.controller.DialogController
 import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
@@ -23,16 +26,20 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
 import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
 import eu.kanade.tachiyomi.ui.download.DownloadController
+import eu.kanade.tachiyomi.ui.extension.ExtensionController
 import eu.kanade.tachiyomi.ui.library.LibraryController
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.more.MoreController
 import eu.kanade.tachiyomi.ui.recent.history.HistoryController
 import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
-import kotlinx.android.synthetic.main.main_activity.appbar
-import kotlinx.android.synthetic.main.main_activity.bottom_nav
-import kotlinx.android.synthetic.main.main_activity.drawer
-import kotlinx.android.synthetic.main.main_activity.tabs
-import kotlinx.android.synthetic.main.main_activity.toolbar
+import java.util.Date
+import java.util.concurrent.TimeUnit
+import kotlinx.android.synthetic.main.main_activity.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import uy.kohesive.injekt.injectLazy
 
 class MainActivity : BaseActivity() {
 
@@ -133,6 +140,10 @@ class MainActivity : BaseActivity() {
                 ChangelogDialogController().showDialog(router)
             }
         }
+        preferences.extensionUpdatesCount().asObservable().subscribe {
+            setExtensionsBadge()
+        }
+        setExtensionsBadge()
     }
 
     override fun onNewIntent(intent: Intent) {
@@ -141,6 +152,37 @@ class MainActivity : BaseActivity() {
         }
     }
 
+    private fun setExtensionsBadge() {
+        val updates = preferences.extensionUpdatesCount().getOrDefault()
+        if (updates > 0) {
+            val badge = bottom_nav.getOrCreateBadge(R.id.nav_more)
+            badge.number = updates
+        } else {
+            bottom_nav.removeBadge(R.id.nav_more)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        getExtensionUpdates()
+    }
+
+    private fun getExtensionUpdates() {
+        if (Date().time >= preferences.lastExtCheck().getOrDefault() +
+            TimeUnit.HOURS.toMillis(2)) {
+            GlobalScope.launch(Dispatchers.IO) {
+                val preferences: PreferencesHelper by injectLazy()
+                try {
+                    val pendingUpdates = ExtensionGithubApi().checkForUpdates(this@MainActivity)
+                    preferences.extensionUpdatesCount().set(pendingUpdates.size)
+                    preferences.lastExtCheck().set(Date().time)
+                } catch (e: java.lang.Exception) {
+                    Timber.e(e)
+                }
+            }
+        }
+    }
+
     private fun handleIntentAction(intent: Intent): Boolean {
         val notificationId = intent.getIntExtra("notificationId", -1)
         if (notificationId > -1) {
@@ -152,6 +194,10 @@ class MainActivity : BaseActivity() {
             SHORTCUT_RECENTLY_UPDATED -> setSelectedDrawerItem(R.id.nav_updates)
             SHORTCUT_RECENTLY_READ -> setSelectedDrawerItem(R.id.nav_history)
             SHORTCUT_CATALOGUES -> setSelectedDrawerItem(R.id.nav_catalogues)
+            SHORTCUT_EXTENSIONS -> {
+                setSelectedDrawerItem(R.id.nav_more)
+                router.pushController(ExtensionController().withFadeTransaction())
+            }
             SHORTCUT_MANGA -> {
                 val extras = intent.extras ?: return false
                 setSelectedDrawerItem(R.id.nav_library)
@@ -267,6 +313,7 @@ class MainActivity : BaseActivity() {
         const val SHORTCUT_CATALOGUES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
         const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
         const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
+        const val SHORTCUT_EXTENSIONS = "eu.kanade.tachiyomi.EXTENSIONS"
 
         const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
         const val INTENT_SEARCH_QUERY = "query"

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt

@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.ui.extension.ExtensionController
 import eu.kanade.tachiyomi.ui.migration.MigrationController
 import eu.kanade.tachiyomi.ui.setting.SettingsController
 import eu.kanade.tachiyomi.ui.setting.SettingsMainController
+import eu.kanade.tachiyomi.util.preference.extensionPreference
 import eu.kanade.tachiyomi.util.preference.iconRes
 import eu.kanade.tachiyomi.util.preference.iconTint
 import eu.kanade.tachiyomi.util.preference.onClick
@@ -25,7 +26,7 @@ class MoreController : SettingsController(), RootController {
 
         val tintColor = context.getResourceColor(R.attr.colorAccent)
 
-        preference {
+        extensionPreference {
             titleRes = R.string.label_extensions
             iconRes = R.drawable.ic_extension_24dp
             iconTint = tintColor

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceDSL.kt

@@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager
 import androidx.preference.PreferenceScreen
 import androidx.preference.SwitchPreferenceCompat
 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
+import eu.kanade.tachiyomi.widget.preference.ExtensionPreference
 import eu.kanade.tachiyomi.widget.preference.IntListPreference
 import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
 
@@ -56,6 +57,10 @@ inline fun PreferenceGroup.multiSelectListPreference(block: (@DSL MultiSelectLis
     return initThenAdd(MultiSelectListPreference(context), block).also(::initDialog)
 }
 
+inline fun PreferenceGroup.extensionPreference(block: (@DSL Preference).() -> Unit): ExtensionPreference {
+    return initThenAdd(ExtensionPreference(context), block)
+}
+
 inline fun PreferenceScreen.preferenceCategory(block: (@DSL PreferenceCategory).() -> Unit): PreferenceCategory {
     return addThenInit(PreferenceCategory(context), block)
 }

+ 37 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/preference/ExtensionPreference.kt

@@ -0,0 +1,37 @@
+package eu.kanade.tachiyomi.widget.preference
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.util.view.gone
+import eu.kanade.tachiyomi.util.view.visible
+import kotlinx.android.synthetic.main.preference_update_text.view.*
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class ExtensionPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+    Preference(context, attrs) {
+
+    init {
+        widgetLayoutResource = R.layout.preference_update_text
+    }
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+
+        val extUpdateText = holder.itemView.textView
+
+        val updates = Injekt.get<PreferencesHelper>().extensionUpdatesCount().getOrDefault()
+        if (updates > 0) {
+            extUpdateText.text = updates.toString()
+            extUpdateText.visible()
+        } else {
+            extUpdateText.text = null
+            extUpdateText.gone()
+        }
+    }
+}

+ 9 - 0
app/src/main/res/drawable/round_textview_background.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="13dp"/>
+    <size
+        android:height="25dp"
+        android:width="25dp" />
+    <solid android:color="@color/material_red_900"/>
+</shape>

+ 17 - 0
app/src/main/res/layout/preference_update_text.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/textView"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
+    android:background="@drawable/round_textview_background"
+    android:textColor="#FFFFFF"
+    android:layout_marginTop="12dp"
+    android:layout_marginBottom="12dp"
+    android:textStyle="bold"
+    tools:text="3"
+    android:layout_marginStart="12dp"
+    android:paddingStart="3dp"
+    android:paddingEnd="3dp"/>

+ 6 - 0
app/src/main/res/menu/extension_main.xml

@@ -16,4 +16,10 @@
         app:iconTint="?attr/colorOnPrimary"
         app:showAsAction="ifRoom" />
 
+    <item
+        android:id="@+id/action_auto_check"
+        android:title="@string/action_auto_check_extensions"
+        android:checkable="true"
+        app:showAsAction="never"/>
+
 </menu>

+ 8 - 0
app/src/main/res/values/strings.xml

@@ -211,6 +211,7 @@
     <string name="ext_version_info">Version: %1$s</string>
     <string name="ext_language_info">Language: %1$s</string>
     <string name="ext_empty_preferences">No preferences to edit for this extension</string>
+    <string name="action_auto_check_extensions">Auto-check for updates</string>
 
       <!-- Reader section -->
     <string name="pref_fullscreen">Fullscreen</string>
@@ -566,6 +567,12 @@
     <string name="update_check_notification_download_error">Download error</string>
     <string name="update_check_notification_update_available">Update available</string>
 
+    <!--Extension Updates Notifications-->
+    <plurals name="update_check_notification_ext_updates">
+        <item quantity="one">Extension update available</item>
+        <item quantity="other">%d extension updates available</item>
+    </plurals>
+
     <!--Content Description-->
     <string name="description_backdrop">Backdrop image of manga</string>
     <string name="description_cover">Cover of manga</string>
@@ -594,5 +601,6 @@
     <string name="channel_library">Library</string>
     <string name="channel_downloader">Downloader</string>
     <string name="channel_new_chapters">Chapter updates</string>
+    <string name="channel_ext_updates">Extension Updates</string>
 
 </resources>