mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
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:
parent
579c79d7f4
commit
7a5c0517d9
15 changed files with 458 additions and 51 deletions
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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("(?<=\\[).+?(?=])")
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue