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-support-v4-kotlin:${Versions.RX_BINDING}")
implementation("com.jakewharton.rxbinding:rxbinding-recyclerview-v7-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 // Tests
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
testImplementation("org.assertj:assertj-core:3.16.1") testImplementation("org.assertj:assertj-core:3.16.1")

View file

@ -244,6 +244,14 @@
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false"/> 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" <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
android:value="false" /> android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut" <meta-data android:name="android.webkit.WebView.MetricsOptOut"

View file

@ -6,7 +6,9 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
/** /**
@ -97,8 +99,14 @@ object Notifications {
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel) deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
listOf( listOf(
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.backup_and_restore)), NotificationChannelGroup(
NotificationChannelGroup(GROUP_EXTENSION_UPDATES, context.getString(R.string.extension_updates)), 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)), NotificationChannelGroup(GROUP_LIBRARY, context.getString(R.string.library)),
).forEach(context.notificationManager::createNotificationChannelGroup) ).forEach(context.notificationManager::createNotificationChannelGroup)
@ -175,6 +183,28 @@ object Notifications {
) )
context.notificationManager.createNotificationChannels(channels) context.notificationManager.createNotificationChannels(channels)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 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( val newChannels = listOf(
NotificationChannel( NotificationChannel(
CHANNEL_EXT_PROGRESS, CHANNEL_EXT_PROGRESS,
@ -191,16 +221,12 @@ object Notifications {
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
).apply { ).apply {
group = GROUP_EXTENSION_UPDATES group = GROUP_EXTENSION_UPDATES
},
NotificationChannel(
CHANNEL_UPDATED,
context.getString(R.string.update_completed),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
setShowBadge(false)
} }
) )
context.notificationManager.createNotificationChannels(newChannels) 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 dohProvider = "doh_provider"
const val useShizuku = "use_shizuku"
const val showNsfwSource = "show_nsfw_source" const val showNsfwSource = "show_nsfw_source"
const val themeMangaDetails = "theme_manga_details" 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 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 filterChapterByRead() = flowPrefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
fun filterChapterByDownloaded() = flowPrefs.getInt(Keys.defaultChapterFilterByDownloaded, 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.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import androidx.preference.PreferenceManager
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@ -467,4 +470,12 @@ class ExtensionManager(
versionCode = extension.versionCode 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.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat 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.connectivityManager
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import rikka.shizuku.Shizuku
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -57,9 +59,17 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
preferences.extensionUpdatesCount().set(extensions.size) preferences.extensionUpdatesCount().set(extensions.size)
val extensionsInstalledByApp by lazy { 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) && inputData.getBoolean(RUN_AUTO, true) &&
preferences.autoUpdateExtensions() != AutoAppUpdaterJob.NEVER && preferences.autoUpdateExtensions() != AutoAppUpdaterJob.NEVER &&
!ExtensionInstallService.isRunning() && !ExtensionInstallService.isRunning() &&
@ -84,7 +94,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
2 2
} }
) )
context.startForegroundService(intent) ContextCompat.startForegroundService(context, intent)
if (extensionsInstalledByApp.size == extensions.size) { if (extensionsInstalledByApp.size == extensions.size) {
return return
} else { } else {
@ -117,12 +127,26 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
context context
) )
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && if (ExtensionManager.canAutoInstallUpdates(context) &&
extensions.size == extensionsList.size extensions.size == extensionsList.size
) { ) {
val intent = ExtensionInstallService.jobIntent(context, extensions) val intent = ExtensionInstallService.jobIntent(context, extensions)
val pendingIntent = 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( addAction(
R.drawable.ic_file_download_24dp, R.drawable.ic_file_download_24dp,
context.getString(R.string.update_all), 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.Build
import android.os.Environment import android.os.Environment
import androidx.core.net.toUri 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.ExtensionManager
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.storage.getUriCompat 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -60,6 +66,28 @@ internal class ExtensionInstaller(private val context: Context) {
* returned by the download manager. * returned by the download manager.
*/ */
val activeDownloads = hashMapOf<String, Long>() 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. * 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)) cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
} }
} catch (_: Exception) { } catch (_: Exception) {
null
} }
if (newDownloadState != null) { if (newDownloadState != null) {
emit(newDownloadState) emit(newDownloadState)
@ -203,7 +232,7 @@ internal class ExtensionInstaller(private val context: Context) {
.takeWhile { info -> .takeWhile { info ->
val sessionId = downloadInstallerMap[pkgName] val sessionId = downloadInstallerMap[pkgName]
if (sessionId != null) { if (sessionId != null) {
info.second != null info.second != null || installer?.isInQueue(pkgName) == true
} else { } else {
true true
} }
@ -226,19 +255,25 @@ internal class ExtensionInstaller(private val context: Context) {
val useActivity = val useActivity =
pkgName?.let { !ExtensionLoader.isExtensionInstalledByApp(context, pkgName) } ?: true || pkgName?.let { !ExtensionLoader.isExtensionInstalledByApp(context, pkgName) } ?: true ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S Build.VERSION.SDK_INT < Build.VERSION_CODES.S
val intent = val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (useActivity) { if (prefs.getBoolean(PreferenceKeys.useShizuku, false) && pkgName != null) {
Intent(context, ExtensionInstallActivity::class.java) setInstalling(pkgName, uri.hashCode())
} else { shizukuInstaller?.addToQueue(downloadId, pkgName, uri)
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 { } 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)) downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(step, null))
} }
fun softDeleteDownload(downloadId: Long) {
downloadManager.remove(downloadId)
}
/** /**
* Deletes the download for the given package name. * 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) { override fun onUpdateAllClicked(position: Int) {
if (!presenter.preferences.hasPromptedBeforeUpdateAll().get()) { if (!presenter.preferences.useShizukuForExtensions() &&
!presenter.preferences.hasPromptedBeforeUpdateAll().get()
) {
controller.activity!!.materialAlertDialog() controller.activity!!.materialAlertDialog()
.setTitle(R.string.update_all) .setTitle(R.string.update_all)
.setMessage(R.string.some_extensions_may_prompt) .setMessage(R.string.some_extensions_may_prompt)

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.extension package eu.kanade.tachiyomi.ui.extension
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -28,7 +27,7 @@ class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<IFlexible<Recycl
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(item: ExtensionGroupItem) { fun bind(item: ExtensionGroupItem) {
binding.title.text = item.name 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.extButton.isEnabled = item.canUpdate == true
binding.extSort.isVisible = item.installedSorting != null binding.extSort.isVisible = item.installedSorting != null
binding.extSort.setText(InstalledExtensionsOrder.fromValue(item.installedSorting ?: 0).nameRes) 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.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings 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.download.DownloadProvider
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target 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.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE 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.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.disableItems 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.launchIO
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.materialAlertDialog 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 { preferenceCategory {
titleRes = R.string.library titleRes = R.string.library
preference { 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.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
@ -41,7 +42,7 @@ class SettingsBrowseController : SettingsController() {
true true
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ExtensionManager.canAutoInstallUpdates(context)) {
val intPref = intListPreference(activity) { val intPref = intListPreference(activity) {
key = PreferenceKeys.autoUpdateExtensions key = PreferenceKeys.autoUpdateExtensions
titleRes = R.string.auto_update_extensions titleRes = R.string.auto_update_extensions
@ -53,26 +54,41 @@ class SettingsBrowseController : SettingsController() {
) )
defaultValue = AutoAppUpdaterJob.ONLY_ON_UNMETERED defaultValue = AutoAppUpdaterJob.ONLY_ON_UNMETERED
} }
val infoPref = infoPreference(R.string.some_extensions_may_not_update) val infoPref = if (!preferences.useShizukuForExtensions()) {
val switchPref = switchPreference { infoPreference(R.string.some_extensions_may_not_update)
key = "notify_ext_updated" } else {
isPersistent = false null
titleRes = R.string.notify_extension_updated }
isChecked = Notifications.isNotificationChannelEnabled(context, Notifications.CHANNEL_EXT_UPDATED) val switchPref = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
updatedExtNotifPref = this switchPreference {
onChange { key = "notify_ext_updated"
false isPersistent = false
} titleRes = R.string.notify_extension_updated
onClick { isChecked = Notifications.isNotificationChannelEnabled(
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { context,
putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID) Notifications.CHANNEL_EXT_UPDATED
putExtra(Settings.EXTRA_CHANNEL_ID, 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 -> 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.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import com.nononsenseapps.filepicker.FilePickerActivity import com.nononsenseapps.filepicker.FilePickerActivity
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -222,6 +223,27 @@ fun Context.acquireWakeLock(tag: String): PowerManager.WakeLock {
return 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. * Property to get the notification manager from the context.
*/ */

View file

@ -305,6 +305,11 @@
<string name="trust">Trust</string> <string name="trust">Trust</string>
<string name="untrusted">Untrusted</string> <string name="untrusted">Untrusted</string>
<string name="uninstall">Uninstall</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">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="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> <string name="obsolete_extension_message">This extension is no longer available.</string>