Add Shizuku as an option to install extensions + Allow all android versions to update all extensions

And for Shizuku this also allows auto updating extensions when using it

Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-10-31 02:06:01 -04:00
parent 579c79d7f4
commit 7a5c0517d9
15 changed files with 458 additions and 51 deletions

View file

@ -226,6 +226,11 @@ dependencies {
implementation("com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:${Versions.RX_BINDING}")
implementation("com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:${Versions.RX_BINDING}")
// Shizuku
val shizukuVersion = "12.1.0"
implementation("dev.rikka.shizuku:api:$shizukuVersion")
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
// Tests
testImplementation("junit:junit:4.13.2")
testImplementation("org.assertj:assertj-core:3.16.1")

View file

@ -244,6 +244,14 @@
android:name=".data.backup.BackupRestoreService"
android:exported="false"/>
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut"

View file

@ -6,7 +6,9 @@ import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.util.system.notificationManager
/**
@ -97,8 +99,14 @@ object Notifications {
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
listOf(
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.backup_and_restore)),
NotificationChannelGroup(GROUP_EXTENSION_UPDATES, context.getString(R.string.extension_updates)),
NotificationChannelGroup(
GROUP_BACKUP_RESTORE,
context.getString(R.string.backup_and_restore)
),
NotificationChannelGroup(
GROUP_EXTENSION_UPDATES,
context.getString(R.string.extension_updates)
),
NotificationChannelGroup(GROUP_LIBRARY, context.getString(R.string.library)),
).forEach(context.notificationManager::createNotificationChannelGroup)
@ -175,6 +183,28 @@ object Notifications {
)
context.notificationManager.createNotificationChannels(channels)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
addAutoUpdateExtensionsNotifications(true, context)
context.notificationManager.createNotificationChannel(
NotificationChannel(
CHANNEL_UPDATED,
context.getString(R.string.update_completed),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
setShowBadge(false)
}
)
} else {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
addAutoUpdateExtensionsNotifications(
prefs.getBoolean(PreferenceKeys.useShizuku, false),
context
)
}
}
fun addAutoUpdateExtensionsNotifications(canAutoUpdate: Boolean, context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
if (canAutoUpdate) {
val newChannels = listOf(
NotificationChannel(
CHANNEL_EXT_PROGRESS,
@ -191,16 +221,12 @@ object Notifications {
NotificationManager.IMPORTANCE_DEFAULT
).apply {
group = GROUP_EXTENSION_UPDATES
},
NotificationChannel(
CHANNEL_UPDATED,
context.getString(R.string.update_completed),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
setShowBadge(false)
}
)
context.notificationManager.createNotificationChannels(newChannels)
} else {
context.notificationManager.deleteNotificationChannel(CHANNEL_EXT_PROGRESS)
context.notificationManager.deleteNotificationChannel(CHANNEL_EXT_UPDATED)
}
}

View file

@ -231,6 +231,8 @@ object PreferenceKeys {
const val dohProvider = "doh_provider"
const val useShizuku = "use_shizuku"
const val showNsfwSource = "show_nsfw_source"
const val themeMangaDetails = "theme_manga_details"

View file

@ -422,6 +422,8 @@ class PreferencesHelper(val context: Context) {
fun autoUpdateExtensions() = prefs.getInt(Keys.autoUpdateExtensions, AutoAppUpdaterJob.ONLY_ON_UNMETERED)
fun useShizukuForExtensions() = prefs.getBoolean(Keys.useShizuku, false)
fun filterChapterByRead() = flowPrefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
fun filterChapterByDownloaded() = flowPrefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL)

View file

@ -2,8 +2,11 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Parcelable
import androidx.preference.PreferenceManager
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
@ -467,4 +470,12 @@ class ExtensionManager(
versionCode = extension.versionCode
)
}
companion object {
fun canAutoInstallUpdates(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ||
prefs.getBoolean(PreferenceKeys.useShizuku, false)
}
}
}

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
import android.app.PendingIntent
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -27,6 +28,7 @@ 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 rikka.shizuku.Shizuku
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -57,9 +59,17 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
val preferences: PreferencesHelper by injectLazy()
preferences.extensionUpdatesCount().set(extensions.size)
val extensionsInstalledByApp by lazy {
extensions.filter { Injekt.get<ExtensionManager>().isInstalledByApp(it) }
if (preferences.useShizukuForExtensions()) {
if (Shizuku.pingBinder() && Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
extensions
} else {
emptyList()
}
} else {
extensions.filter { Injekt.get<ExtensionManager>().isInstalledByApp(it) }
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
if (ExtensionManager.canAutoInstallUpdates(context) &&
inputData.getBoolean(RUN_AUTO, true) &&
preferences.autoUpdateExtensions() != AutoAppUpdaterJob.NEVER &&
!ExtensionInstallService.isRunning() &&
@ -84,7 +94,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
2
}
)
context.startForegroundService(intent)
ContextCompat.startForegroundService(context, intent)
if (extensionsInstalledByApp.size == extensions.size) {
return
} else {
@ -117,12 +127,26 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
context
)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
if (ExtensionManager.canAutoInstallUpdates(context) &&
extensions.size == extensionsList.size
) {
val intent = ExtensionInstallService.jobIntent(context, extensions)
val pendingIntent =
PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.getForegroundService(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
} else {
PendingIntent.getService(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
addAction(
R.drawable.ic_file_download_24dp,
context.getString(R.string.update_all),

View file

@ -0,0 +1,221 @@
package eu.kanade.tachiyomi.extension
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.util.system.getUriSize
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import rikka.shizuku.Shizuku
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.BufferedReader
import java.io.InputStream
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
class ShizukuInstaller(private val context: Context, val finishedQueue: (ShizukuInstaller) -> Unit) {
private val extensionManager: ExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
data class Entry(val downloadId: Long, val pkgName: String, val uri: Uri)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val shizukuDeadListener = Shizuku.OnBinderDeadListener {
Timber.d("Shizuku was killed prematurely")
finishedQueue(this)
}
fun isInQueue(pkgName: String) = queue.any { it.pkgName == pkgName }
private val shizukuPermissionListener = object : Shizuku.OnRequestPermissionResultListener {
override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) {
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
if (grantResult == PackageManager.PERMISSION_GRANTED) {
ready = true
checkQueue()
} else {
finishedQueue(this@ShizukuInstaller)
}
Shizuku.removeRequestPermissionResultListener(this)
}
}
}
var ready = false
init {
Shizuku.addBinderDeadListener(shizukuDeadListener)
require(Shizuku.pingBinder() && context.isPackageInstalled(shizukuPkgName)) {
finishedQueue(this)
context.getString(R.string.ext_installer_shizuku_stopped)
}
ready = if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
true
} else {
Shizuku.addRequestPermissionResultListener(shizukuPermissionListener)
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
false
}
}
@Suppress("BlockingMethodInNonBlockingContext")
fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId, entry.uri.hashCode())
ioScope.launch {
var sessionId: String? = null
try {
val size = context.getUriSize(entry.uri) ?: throw IllegalStateException()
context.contentResolver.openInputStream(entry.uri)!!.use {
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
"pm install-create --user current -i ${context.packageName} -S $size"
} else {
"pm install-create -i ${context.packageName} -S $size"
}
val createResult = exec(createCommand)
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
?: throw RuntimeException("Failed to create install session")
val writeResult = exec("pm install-write -S $size $sessionId base -", it)
if (writeResult.resultCode != 0) {
throw RuntimeException("Failed to write APK to session $sessionId")
}
val commitResult = exec("pm install-commit $sessionId")
if (commitResult.resultCode != 0) {
throw RuntimeException("Failed to commit install session $sessionId")
}
continueQueue(true)
}
} catch (e: Exception) {
Timber.e(e, "Failed to install extension ${entry.downloadId} ${entry.uri}")
if (sessionId != null) {
exec("pm install-abandon $sessionId")
}
continueQueue(false)
}
}
}
/**
* Checks the queue. The provided service will be stopped if the queue is empty.
* Will not be run when not ready.
*
* @see ready
*/
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
finishedQueue(this)
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeFirst()
processEntry(nextEntry)
}
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [ExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(succeeded: Boolean) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.setInstallationResult(completedEntry.downloadId, succeeded)
checkQueue()
}
}
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, pkgName: String, uri: Uri) {
queue.add(Entry(downloadId, pkgName, uri))
checkQueue()
}
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
queue.forEach { extensionManager.setInstallationResult(it.downloadId, false) }
// extensionManager.up(downloadId, InstallStep.Idle)
}
}
// Don't cancel if entry is already started installing
fun cancelEntry(entry: Entry): Boolean = getActiveEntry() != entry
fun getActiveEntry(): Entry? = waitingInstall.get()
fun onDestroy() {
Shizuku.removeBinderDeadListener(shizukuDeadListener)
Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener)
ioScope.cancel()
LocalBroadcastManager.getInstance(context).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.setInstallationResult(it.pkgName, false) }
queue.clear()
waitingInstall.set(null)
}
private fun exec(command: String, stdin: InputStream? = null): ShellResult {
@Suppress("DEPRECATION")
val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, null)
if (stdin != null) {
process.outputStream.use { stdin.copyTo(it) }
}
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
val resultCode = process.waitFor()
return ShellResult(resultCode, output)
}
private data class ShellResult(val resultCode: Int, val out: String)
companion object {
const val shizukuPkgName = "moe.shizuku.privileged.api"
const val downloadLink = "https://shizuku.rikka.app/download"
private const val SHIZUKU_PERMISSION_REQUEST_CODE = 14045
private val SESSION_ID_REGEX = Regex("(?<=\\[).+?(?=])")
}
}

View file

@ -10,12 +10,18 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -60,6 +66,28 @@ internal class ExtensionInstaller(private val context: Context) {
* returned by the download manager.
*/
val activeDownloads = hashMapOf<String, Long>()
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var installer: ShizukuInstaller? = null
private val shizukuInstaller: ShizukuInstaller?
get() = installer ?: run {
try {
installer = ShizukuInstaller(context) {
it.onDestroy()
ioScope.launch {
delay(500)
downloadsStateFlow.emit("Finished" to (InstallStep.Installed to null))
}
installer = null
}
} catch (e: Exception) {
ioScope.launchUI {
context.toast(e.message)
}
}
installer
}
/**
* StateFlow used to notify the installation step of every download.
@ -160,6 +188,7 @@ internal class ExtensionInstaller(private val context: Context) {
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
}
} catch (_: Exception) {
null
}
if (newDownloadState != null) {
emit(newDownloadState)
@ -203,7 +232,7 @@ internal class ExtensionInstaller(private val context: Context) {
.takeWhile { info ->
val sessionId = downloadInstallerMap[pkgName]
if (sessionId != null) {
info.second != null
info.second != null || installer?.isInQueue(pkgName) == true
} else {
true
}
@ -226,19 +255,25 @@ internal class ExtensionInstaller(private val context: Context) {
val useActivity =
pkgName?.let { !ExtensionLoader.isExtensionInstalledByApp(context, pkgName) } ?: true ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S
val intent =
if (useActivity) {
Intent(context, ExtensionInstallActivity::class.java)
} else {
Intent(context, ExtensionInstallBroadcast::class.java)
}
.setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (useActivity) {
context.startActivity(intent)
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.getBoolean(PreferenceKeys.useShizuku, false) && pkgName != null) {
setInstalling(pkgName, uri.hashCode())
shizukuInstaller?.addToQueue(downloadId, pkgName, uri)
} else {
context.sendBroadcast(intent)
val intent =
if (useActivity) {
Intent(context, ExtensionInstallActivity::class.java)
} else {
Intent(context, ExtensionInstallBroadcast::class.java)
}
.setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (useActivity) {
context.startActivity(intent)
} else {
context.sendBroadcast(intent)
}
}
}
@ -297,10 +332,6 @@ internal class ExtensionInstaller(private val context: Context) {
downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(step, null))
}
fun softDeleteDownload(downloadId: Long) {
downloadManager.remove(downloadId)
}
/**
* Deletes the download for the given package name.
*

View file

@ -208,7 +208,9 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
}
override fun onUpdateAllClicked(position: Int) {
if (!presenter.preferences.hasPromptedBeforeUpdateAll().get()) {
if (!presenter.preferences.useShizukuForExtensions() &&
!presenter.preferences.hasPromptedBeforeUpdateAll().get()
) {
controller.activity!!.materialAlertDialog()
.setTitle(R.string.update_all)
.setMessage(R.string.some_extensions_may_prompt)

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.extension
import android.annotation.SuppressLint
import android.os.Build
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
@ -28,7 +27,7 @@ class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<IFlexible<Recycl
@SuppressLint("SetTextI18n")
fun bind(item: ExtensionGroupItem) {
binding.title.text = item.name
binding.extButton.isVisible = item.canUpdate != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
binding.extButton.isVisible = item.canUpdate != null
binding.extButton.isEnabled = item.canUpdate == true
binding.extSort.isVisible = item.installedSorting != null
binding.extSort.setText(InstalledExtensionsOrder.fromValue(item.installedSorting ?: 0).nameRes)

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
@ -21,7 +22,9 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
@ -30,6 +33,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.disableItems
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.materialAlertDialog
@ -211,6 +215,35 @@ class SettingsAdvancedController : SettingsController() {
}
}
preferenceCategory {
titleRes = R.string.extensions
switchPreference {
key = PreferenceKeys.useShizuku
titleRes = R.string.use_shizuku_to_install
summaryRes = R.string.use_shizuku_summary
defaultValue = false
onChange {
it as Boolean
if (it && !context.isPackageInstalled(ShizukuInstaller.shizukuPkgName)) {
context.materialAlertDialog()
.setTitle(R.string.shizuku)
.setMessage(R.string.ext_installer_shizuku_unavailable_dialog)
.setPositiveButton(R.string.download) { _, _ ->
openInBrowser(ShizukuInstaller.downloadLink)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
false
} else {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
Notifications.addAutoUpdateExtensionsNotifications(it, context)
}
true
}
}
}
}
preferenceCategory {
titleRes = R.string.library
preference {

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity
@ -41,7 +42,7 @@ class SettingsBrowseController : SettingsController() {
true
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ExtensionManager.canAutoInstallUpdates(context)) {
val intPref = intListPreference(activity) {
key = PreferenceKeys.autoUpdateExtensions
titleRes = R.string.auto_update_extensions
@ -53,26 +54,41 @@ class SettingsBrowseController : SettingsController() {
)
defaultValue = AutoAppUpdaterJob.ONLY_ON_UNMETERED
}
val infoPref = infoPreference(R.string.some_extensions_may_not_update)
val switchPref = switchPreference {
key = "notify_ext_updated"
isPersistent = false
titleRes = R.string.notify_extension_updated
isChecked = Notifications.isNotificationChannelEnabled(context, Notifications.CHANNEL_EXT_UPDATED)
updatedExtNotifPref = this
onChange {
false
}
onClick {
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
putExtra(Settings.EXTRA_CHANNEL_ID, Notifications.CHANNEL_EXT_UPDATED)
val infoPref = if (!preferences.useShizukuForExtensions()) {
infoPreference(R.string.some_extensions_may_not_update)
} else {
null
}
val switchPref = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
switchPreference {
key = "notify_ext_updated"
isPersistent = false
titleRes = R.string.notify_extension_updated
isChecked = Notifications.isNotificationChannelEnabled(
context,
Notifications.CHANNEL_EXT_UPDATED
)
updatedExtNotifPref = this
onChange {
false
}
onClick {
val intent =
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID)
putExtra(
Settings.EXTRA_CHANNEL_ID,
Notifications.CHANNEL_EXT_UPDATED
)
}
startActivity(intent)
}
startActivity(intent)
}
} else {
null
}
preferences.automaticExtUpdates().asImmediateFlowIn(viewScope) { value ->
arrayOf(intPref, infoPref, switchPref).forEach { it.isVisible = value }
arrayOf(intPref, infoPref, switchPref).forEach { it?.isVisible = value }
}
}
}

View file

@ -33,6 +33,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import com.nononsenseapps.filepicker.FilePickerActivity
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -222,6 +223,27 @@ fun Context.acquireWakeLock(tag: String): PowerManager.WakeLock {
return wakeLock
}
/**
* Gets document size of provided [Uri]
*
* @return document size of [uri] or null if size can't be obtained
*/
fun Context.getUriSize(uri: Uri): Long? {
return UniFile.fromUri(this, uri).length().takeIf { it >= 0 }
}
/**
* Returns true if [packageName] is installed.
*/
fun Context.isPackageInstalled(packageName: String): Boolean {
return try {
packageManager.getApplicationInfo(packageName, 0)
true
} catch (e: Exception) {
false
}
}
/**
* Property to get the notification manager from the context.
*/

View file

@ -305,6 +305,11 @@
<string name="trust">Trust</string>
<string name="untrusted">Untrusted</string>
<string name="uninstall">Uninstall</string>
<string name="ext_installer_shizuku_stopped">Shizuku is not running</string>
<string name="ext_installer_shizuku_unavailable_dialog">Install and start Shizuku to use Shizuku as extension installer.</string>
<string name="use_shizuku_to_install">Use Shizuku to install extensions</string>
<string name="shizuku" translatable="false">Shizuku</string>
<string name="use_shizuku_summary">Allows extensions to be installed without user prompts and enables automatic updates for devices under Android 12</string>
<string name="untrusted_extension">Untrusted extension</string>
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Tachiyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
<string name="obsolete_extension_message">This extension is no longer available.</string>