mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
Add private extension install method
Had to set private ext files to read only because Android 14 targetting https: //developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading Co-Authored-By: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
This commit is contained in:
parent
ba74224e06
commit
89ad5e03c1
13 changed files with 356 additions and 150 deletions
|
@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
||||
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
|
||||
|
@ -474,7 +475,7 @@ class PreferencesHelper(val context: Context) {
|
|||
|
||||
fun autoUpdateExtensions() = prefs.getInt(Keys.autoUpdateExtensions, AppDownloadInstallJob.ONLY_ON_UNMETERED)
|
||||
|
||||
fun useShizukuForExtensions() = prefs.getBoolean(Keys.useShizuku, false)
|
||||
fun extensionInstaller() = flowPrefs.getInt("extension_installer", ExtensionInstaller.PACKAGE_INSTALLER)
|
||||
|
||||
fun filterChapterByRead() = flowPrefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
|
||||
|
||||
|
|
|
@ -4,8 +4,6 @@ import android.content.Context
|
|||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.preference.PreferenceManager
|
||||
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
|
||||
|
@ -78,8 +76,10 @@ class ExtensionManager(
|
|||
.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
|
||||
return if (pkgName != null) {
|
||||
try {
|
||||
return iconMap[pkgName]
|
||||
?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
|
||||
return iconMap.getOrPut(pkgName) {
|
||||
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
||||
.loadIcon(context.packageManager)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
@ -250,7 +250,6 @@ class ExtensionManager(
|
|||
/** Sets the result of the installation of an extension.
|
||||
*
|
||||
* @param sessionId The id of the download.
|
||||
* @param result Whether the extension was installed or not.
|
||||
*/
|
||||
fun cancelInstallation(sessionId: Int) {
|
||||
installer.cancelInstallation(sessionId)
|
||||
|
@ -299,6 +298,7 @@ class ExtensionManager(
|
|||
* @param pkgName The package name of the application to uninstall.
|
||||
*/
|
||||
fun uninstallExtension(pkgName: String) {
|
||||
ExtensionLoader.uninstallPrivateExtension(context, pkgName)
|
||||
installer.uninstallApk(pkgName)
|
||||
}
|
||||
|
||||
|
@ -446,13 +446,14 @@ class ExtensionManager(
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun canAutoInstallUpdates(context: Context, checkIfShizukuIsRunning: Boolean = false): Boolean {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
fun canAutoInstallUpdates(checkIfShizukuIsRunning: Boolean = false): Boolean {
|
||||
val prefs = Injekt.get<PreferencesHelper>().extensionInstaller().get()
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ||
|
||||
(
|
||||
prefs.getBoolean(PreferenceKeys.useShizuku, false) &&
|
||||
prefs == ExtensionInstaller.SHIZUKU &&
|
||||
(!checkIfShizukuIsRunning || ShizukuInstaller.isShizukuRunning())
|
||||
)
|
||||
) ||
|
||||
prefs == ExtensionInstaller.PRIVATE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension
|
|||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
@ -25,6 +26,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
||||
import eu.kanade.tachiyomi.util.system.localeContext
|
||||
import eu.kanade.tachiyomi.util.system.notification
|
||||
|
@ -61,17 +64,22 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||
val preferences: PreferencesHelper by injectLazy()
|
||||
preferences.extensionUpdatesCount().set(extensions.size)
|
||||
val extensionsInstalledByApp by lazy {
|
||||
if (preferences.useShizukuForExtensions()) {
|
||||
if (preferences.extensionInstaller().get() == ExtensionInstaller.SHIZUKU) {
|
||||
if (Shizuku.pingBinder() && Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
|
||||
extensions
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
extensions.filter { Injekt.get<ExtensionManager>().isInstalledByApp(it) }
|
||||
val extManager = Injekt.get<ExtensionManager>()
|
||||
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
extensions.filter {
|
||||
(isOnA12 && extManager.isInstalledByApp(it)) ||
|
||||
ExtensionLoader.isExtensionPrivate(context, it.pkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ExtensionManager.canAutoInstallUpdates(context, true) &&
|
||||
if (ExtensionManager.canAutoInstallUpdates(true) &&
|
||||
inputData.getBoolean(RUN_AUTO, true) &&
|
||||
preferences.autoUpdateExtensions() != AppDownloadInstallJob.NEVER &&
|
||||
!ExtensionInstallerJob.isRunning(context) &&
|
||||
|
@ -126,7 +134,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||
context,
|
||||
),
|
||||
)
|
||||
if (ExtensionManager.canAutoInstallUpdates(context, true) &&
|
||||
if (ExtensionManager.canAutoInstallUpdates(true) &&
|
||||
extensions.size == extensionsList.size
|
||||
) {
|
||||
val pendingIntent =
|
||||
|
|
|
@ -4,7 +4,9 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.util.system.launchNow
|
||||
|
@ -36,6 +38,9 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addAction(ACTION_EXTENSION_ADDED)
|
||||
addAction(ACTION_EXTENSION_REPLACED)
|
||||
addAction(ACTION_EXTENSION_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
|
||||
|
@ -47,7 +52,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> {
|
||||
if (!isReplacing(intent)) {
|
||||
launchNow {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
|
@ -58,7 +63,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
|
||||
launchNow {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||
|
@ -69,7 +74,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
Intent.ACTION_PACKAGE_REMOVED, ACTION_EXTENSION_REMOVED -> {
|
||||
if (!isReplacing(intent)) {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName != null) {
|
||||
|
@ -117,4 +122,30 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
|
|||
fun onExtensionUntrusted(extension: Extension.Untrusted)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_EXTENSION_ADDED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_ADDED"
|
||||
private const val ACTION_EXTENSION_REPLACED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REPLACED"
|
||||
private const val ACTION_EXTENSION_REMOVED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REMOVED"
|
||||
|
||||
fun notifyAdded(context: Context, pkgName: String) {
|
||||
notify(context, pkgName, ACTION_EXTENSION_ADDED)
|
||||
}
|
||||
|
||||
fun notifyReplaced(context: Context, pkgName: String) {
|
||||
notify(context, pkgName, ACTION_EXTENSION_REPLACED)
|
||||
}
|
||||
|
||||
fun notifyRemoved(context: Context, pkgName: String) {
|
||||
notify(context, pkgName, ACTION_EXTENSION_REMOVED)
|
||||
}
|
||||
|
||||
private fun notify(context: Context, pkgName: String, action: String) {
|
||||
Intent(action).apply {
|
||||
data = Uri.parse("package:$pkgName")
|
||||
`package` = context.packageName
|
||||
context.sendBroadcast(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,14 +12,14 @@ import android.os.Build
|
|||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.ExtensionInstallerJob
|
||||
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.isPackageInstalled
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -44,6 +44,8 @@ import kotlinx.coroutines.flow.takeWhile
|
|||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
@ -262,24 +264,58 @@ 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 prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (prefs.getBoolean(PreferenceKeys.useShizuku, false) && pkgName != null) {
|
||||
setInstalling(pkgName, uri.hashCode())
|
||||
shizukuInstaller?.addToQueue(downloadId, pkgName, uri)
|
||||
} else {
|
||||
val intent =
|
||||
if (useActivity) {
|
||||
Intent(context, ExtensionInstallActivity::class.java)
|
||||
} else {
|
||||
Intent(context, ExtensionInstallBroadcast::class.java)
|
||||
val prefs: PreferencesHelper = Injekt.get()
|
||||
when (prefs.extensionInstaller().get()) {
|
||||
SHIZUKU -> {
|
||||
pkgName ?: return
|
||||
setInstalling(pkgName, uri.hashCode())
|
||||
shizukuInstaller?.addToQueue(downloadId, pkgName, uri)
|
||||
}
|
||||
PRIVATE -> {
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
val tempFile = File(context.cacheDir, "temp_$downloadId")
|
||||
|
||||
pkgName ?: return
|
||||
if (tempFile.exists() && !tempFile.delete()) {
|
||||
// Unlikely but just in case
|
||||
setInstallationResult(pkgName, false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
|
||||
setInstallationResult(pkgName, true)
|
||||
} else {
|
||||
setInstallationResult(pkgName, false)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to read downloaded extension file.")
|
||||
setInstallationResult(pkgName, false)
|
||||
}
|
||||
|
||||
tempFile.delete()
|
||||
}
|
||||
else -> {
|
||||
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)
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -290,11 +326,15 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||
* @param pkgName The package name of the extension to uninstall
|
||||
*/
|
||||
fun uninstallApk(pkgName: String) {
|
||||
val packageUri = "package:$pkgName".toUri()
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
context.startActivity(intent)
|
||||
if (context.isPackageInstalled(pkgName)) {
|
||||
@Suppress("DEPRECATION")
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
ExtensionLoader.uninstallPrivateExtension(context, pkgName)
|
||||
ExtensionInstallReceiver.notifyRemoved(context, pkgName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -430,5 +470,9 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
||||
const val FILE_SCHEME = "file://"
|
||||
|
||||
const val PACKAGE_INSTALLER = 0
|
||||
const val SHIZUKU = 1
|
||||
const val PRIVATE = 2
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions installed in the system.
|
||||
|
@ -56,6 +59,55 @@ internal object ExtensionLoader {
|
|||
*/
|
||||
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
|
||||
|
||||
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
|
||||
|
||||
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
|
||||
|
||||
fun installPrivateExtensionFile(context: Context, file: File): Boolean {
|
||||
val extension = context.packageManager.getPackageArchiveInfo(file.absolutePath, PACKAGE_FLAGS)
|
||||
?.takeIf { isPackageAnExtension(it) } ?: return false
|
||||
val currentExtension = getExtensionPackageInfoFromPkgName(context, extension.packageName)
|
||||
|
||||
if (currentExtension != null) {
|
||||
if (PackageInfoCompat.getLongVersionCode(extension) <
|
||||
PackageInfoCompat.getLongVersionCode(currentExtension)
|
||||
) {
|
||||
Timber.e("Installed extension version is higher. Downgrading is not allowed.")
|
||||
return false
|
||||
}
|
||||
|
||||
val extensionSignatures = getSignatures(extension)
|
||||
if (extensionSignatures.isNullOrEmpty()) {
|
||||
Timber.e("Extension to be installed is not signed.")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!extensionSignatures.containsAll(getSignatures(currentExtension)!!)) {
|
||||
Timber.e("Installed extension signature is not matched.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
|
||||
return try {
|
||||
file.copyTo(target, overwrite = true)
|
||||
if (currentExtension != null) {
|
||||
ExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
|
||||
} else {
|
||||
ExtensionInstallReceiver.notifyAdded(context, extension.packageName)
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to copy extension file.")
|
||||
target.delete()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallPrivateExtension(context: Context, pkgName: String) {
|
||||
File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION").delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all the installed extensions initialized concurrently.
|
||||
*
|
||||
|
@ -75,28 +127,29 @@ internal object ExtensionLoader {
|
|||
.filter { isPackageAnExtension(it) }
|
||||
.map { ExtensionInfo(packageInfo = it, isShared = true) }
|
||||
|
||||
// val privateExtPkgs = getPrivateExtensionDir(context)
|
||||
// .listFiles()
|
||||
// ?.asSequence()
|
||||
// ?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
|
||||
// ?.mapNotNull {
|
||||
// val path = it.absolutePath
|
||||
// pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
|
||||
// ?.apply { applicationInfo.fixBasePaths(path) }
|
||||
// }
|
||||
// ?.filter { isPackageAnExtension(it) }
|
||||
// ?.map { ExtensionInfo(packageInfo = it, isShared = false) }
|
||||
// ?: emptySequence()
|
||||
val privateExtPkgs = getPrivateExtensionDir(context)
|
||||
.listFiles()
|
||||
?.asSequence()
|
||||
?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
|
||||
?.mapNotNull {
|
||||
it.setReadOnly()
|
||||
val path = it.absolutePath
|
||||
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
|
||||
?.apply { applicationInfo.fixBasePaths(path) }
|
||||
}
|
||||
?.filter { isPackageAnExtension(it) }
|
||||
?.map { ExtensionInfo(packageInfo = it, isShared = false) }
|
||||
?: emptySequence()
|
||||
|
||||
val extPkgs = (sharedExtPkgs)
|
||||
val extPkgs = (sharedExtPkgs + privateExtPkgs)
|
||||
// Remove duplicates. Shared takes priority than private by default
|
||||
.distinctBy { it.packageInfo.packageName }
|
||||
// Compare version number
|
||||
// .mapNotNull { sharedPkg ->
|
||||
// val privatePkg = privateExtPkgs
|
||||
// .singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName }
|
||||
// selectExtensionPackage(sharedPkg, privatePkg)
|
||||
// }
|
||||
.mapNotNull { sharedPkg ->
|
||||
val privatePkg = privateExtPkgs
|
||||
.singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName }
|
||||
selectExtensionPackage(sharedPkg, privatePkg)
|
||||
}
|
||||
.toList()
|
||||
|
||||
if (extPkgs.isEmpty()) return emptyList()
|
||||
|
@ -126,22 +179,58 @@ internal object ExtensionLoader {
|
|||
fun getExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? {
|
||||
return getExtensionInfoFromPkgName(context, pkgName)?.packageInfo
|
||||
}
|
||||
fun isExtensionPrivate(context: Context, pkgName: String): Boolean =
|
||||
getExtensionInfoFromPkgName(context, pkgName)?.isShared == false
|
||||
|
||||
fun extensionInstallDate(context: Context, extension: Extension.Installed): Long {
|
||||
return try {
|
||||
if (!extension.isShared) {
|
||||
val file = ExtensionLoader.privateExtensionFile(context, extension.pkgName)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val attr = Files.readAttributes(file.toPath(), BasicFileAttributes::class.java)
|
||||
attr.creationTime().toMillis()
|
||||
} else {
|
||||
file.lastModified()
|
||||
}
|
||||
} else {
|
||||
context.packageManager.getPackageInfo(extension.pkgName, 0).firstInstallTime
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun extensionUpdateDate(context: Context, extension: Extension.Installed): Long {
|
||||
return try {
|
||||
if (!extension.isShared) {
|
||||
ExtensionLoader.privateExtensionFile(context, extension.pkgName).lastModified()
|
||||
} else {
|
||||
context.packageManager.getPackageInfo(extension.pkgName, 0).lastUpdateTime
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun privateExtensionFile(context: Context, pkgName: String): File =
|
||||
File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION")
|
||||
|
||||
private fun getExtensionInfoFromPkgName(context: Context, pkgName: String): ExtensionInfo? {
|
||||
// val privateExtensionFile = File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION")
|
||||
// val privatePkg = if (privateExtensionFile.isFile) {
|
||||
// context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
|
||||
// ?.takeIf { isPackageAnExtension(it) }
|
||||
// ?.let {
|
||||
// it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath)
|
||||
// ExtensionInfo(
|
||||
// packageInfo = it,
|
||||
// isShared = false,
|
||||
// )
|
||||
// }
|
||||
// } else {
|
||||
// null
|
||||
// }
|
||||
val privateExtensionFile = File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION")
|
||||
val privatePkg = if (privateExtensionFile.isFile) {
|
||||
privateExtensionFile.setReadOnly()
|
||||
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
|
||||
?.takeIf { isPackageAnExtension(it) }
|
||||
?.let {
|
||||
it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath)
|
||||
ExtensionInfo(
|
||||
packageInfo = it,
|
||||
isShared = false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val sharedPkg = try {
|
||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||
|
@ -156,7 +245,7 @@ internal object ExtensionLoader {
|
|||
null
|
||||
}
|
||||
|
||||
return sharedPkg // selectExtensionPackage(sharedPkg, privatePkg)
|
||||
return selectExtensionPackage(sharedPkg, privatePkg)
|
||||
}
|
||||
|
||||
fun isExtensionInstalledByApp(context: Context, pkgName: String): Boolean {
|
||||
|
|
|
@ -7,13 +7,13 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
|||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.ui.migration.BaseMigrationPresenter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
@ -128,8 +128,8 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
|
|||
{
|
||||
when (sortOrder) {
|
||||
InstalledExtensionsOrder.Name -> it.name
|
||||
InstalledExtensionsOrder.RecentlyUpdated -> Long.MAX_VALUE - extensionUpdateDate(it.pkgName)
|
||||
InstalledExtensionsOrder.RecentlyInstalled -> Long.MAX_VALUE - extensionInstallDate(it.pkgName)
|
||||
InstalledExtensionsOrder.RecentlyUpdated -> Long.MAX_VALUE - ExtensionLoader.extensionUpdateDate(context, it)
|
||||
InstalledExtensionsOrder.RecentlyInstalled -> Long.MAX_VALUE - ExtensionLoader.extensionInstallDate(context, it)
|
||||
InstalledExtensionsOrder.Language -> it.lang
|
||||
}
|
||||
},
|
||||
|
@ -188,24 +188,6 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
|
|||
return items
|
||||
}
|
||||
|
||||
private fun extensionInstallDate(pkgName: String): Long {
|
||||
val context = view?.context ?: return 0
|
||||
return try {
|
||||
context.packageManager.getPackageInfo(pkgName, 0).firstInstallTime
|
||||
} catch (e: java.lang.Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun extensionUpdateDate(pkgName: String): Long {
|
||||
val context = view?.context ?: return 0
|
||||
return try {
|
||||
context.packageManager.getPackageInfo(pkgName, 0).lastUpdateTime
|
||||
} catch (e: java.lang.Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun getExtensionUpdateCount(): Int = preferences.extensionUpdatesCount().get()
|
||||
|
||||
@Synchronized
|
||||
|
|
|
@ -21,6 +21,8 @@ import eu.kanade.tachiyomi.databinding.RecyclerWithScrollerBinding
|
|||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.ui.extension.details.ExtensionDetailsController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.migration.BaseMigrationInterface
|
||||
|
@ -30,6 +32,7 @@ import eu.kanade.tachiyomi.ui.migration.SourceAdapter
|
|||
import eu.kanade.tachiyomi.ui.migration.SourceItem
|
||||
import eu.kanade.tachiyomi.ui.migration.manga.design.PreMigrationController
|
||||
import eu.kanade.tachiyomi.ui.source.BrowseController
|
||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
||||
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
|
||||
import eu.kanade.tachiyomi.util.view.activityBinding
|
||||
|
@ -220,7 +223,7 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
|||
|
||||
override fun onUpdateAllClicked(position: Int) {
|
||||
(controller.activity as? MainActivity)?.showNotificationPermissionPrompt()
|
||||
if (!presenter.preferences.useShizukuForExtensions() &&
|
||||
if (presenter.preferences.extensionInstaller().get() != ExtensionInstaller.SHIZUKU &&
|
||||
!presenter.preferences.hasPromptedBeforeUpdateAll().get()
|
||||
) {
|
||||
controller.activity!!.materialAlertDialog()
|
||||
|
@ -410,7 +413,24 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
|||
}
|
||||
|
||||
override fun uninstallExtension(pkgName: String) {
|
||||
presenter.uninstallExtension(pkgName)
|
||||
if (context.isPackageInstalled(pkgName)) {
|
||||
presenter.uninstallExtension(pkgName)
|
||||
} else {
|
||||
val extName = run {
|
||||
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(
|
||||
context,
|
||||
pkgName,
|
||||
)?.applicationInfo ?: return@run pkgName
|
||||
context.packageManager.getApplicationLabel(appInfo).toString()
|
||||
}
|
||||
controller.activity!!.materialAlertDialog()
|
||||
.setTitle(extName)
|
||||
.setPositiveButton(R.string.uninstall) { _, _ ->
|
||||
presenter.uninstallExtension(pkgName)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
|
|
|
@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.databinding.ExtensionCardItemBinding
|
|||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
@ -47,19 +48,21 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||
if (extension is Extension.Installed && !extension.hasUpdate) {
|
||||
when (InstalledExtensionsOrder.fromValue(adapter.installedSortOrder)) {
|
||||
InstalledExtensionsOrder.RecentlyUpdated -> {
|
||||
extensionUpdateDate(extension.pkgName)?.let {
|
||||
binding.date.isVisible = true
|
||||
binding.date.text = itemView.context.timeSpanFromNow(R.string.updated_, it)
|
||||
infoText.add("")
|
||||
}
|
||||
ExtensionLoader.extensionUpdateDate(itemView.context, extension)
|
||||
.takeUnless { it == 0L }?.let {
|
||||
binding.date.isVisible = true
|
||||
binding.date.text = itemView.context.timeSpanFromNow(R.string.updated_, it)
|
||||
infoText.add("")
|
||||
}
|
||||
}
|
||||
InstalledExtensionsOrder.RecentlyInstalled -> {
|
||||
extensionInstallDate(extension.pkgName)?.let {
|
||||
binding.date.isVisible = true
|
||||
binding.date.text =
|
||||
itemView.context.timeSpanFromNow(R.string.installed_, it)
|
||||
infoText.add("")
|
||||
}
|
||||
ExtensionLoader.extensionInstallDate(itemView.context, extension)
|
||||
.takeUnless { it == 0L }?.let {
|
||||
binding.date.isVisible = true
|
||||
binding.date.text =
|
||||
itemView.context.timeSpanFromNow(R.string.installed_, it)
|
||||
infoText.add("")
|
||||
}
|
||||
}
|
||||
else -> binding.date.isVisible = false
|
||||
}
|
||||
|
@ -156,22 +159,4 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||
setText(R.string.install)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extensionInstallDate(pkgName: String): Long? {
|
||||
val context = itemView.context
|
||||
return try {
|
||||
context.packageManager.getPackageInfo(pkgName, 0).firstInstallTime
|
||||
} catch (e: java.lang.Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun extensionUpdateDate(pkgName: String): Long? {
|
||||
val context = itemView.context
|
||||
return try {
|
||||
context.packageManager.getPackageInfo(pkgName, 0).lastUpdateTime
|
||||
} catch (e: java.lang.Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import androidx.core.view.isVisible
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.ui.extension.getApplicationIcon
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
||||
import eu.kanade.tachiyomi.util.view.inflate
|
||||
|
||||
class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPresenter) :
|
||||
|
@ -49,7 +51,24 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
|||
binding.extensionPkg.text = extension.pkgName
|
||||
|
||||
binding.extensionUninstallButton.setOnClickListener {
|
||||
presenter.uninstallExtension()
|
||||
if (extension.isShared) {
|
||||
presenter.uninstallExtension()
|
||||
} else {
|
||||
val extName = run {
|
||||
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(
|
||||
context,
|
||||
extension.pkgName,
|
||||
)?.applicationInfo ?: return@run extension.name
|
||||
context.packageManager.getApplicationLabel(appInfo).toString()
|
||||
}
|
||||
context.materialAlertDialog()
|
||||
.setTitle(extName)
|
||||
.setPositiveButton(R.string.uninstall) { _, _ ->
|
||||
presenter.uninstallExtension()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
binding.extensionAppInfoButton.setOnClickListener {
|
||||
|
@ -59,6 +78,8 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
|
|||
it.context.startActivity(intent)
|
||||
}
|
||||
|
||||
binding.extensionAppInfoButton.isVisible = extension.isShared
|
||||
|
||||
if (extension.isUnofficial) {
|
||||
binding.extensionWarningBanner.isVisible = true
|
||||
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
|
||||
|
|
|
@ -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
|
||||
|
@ -27,7 +28,9 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
|
|||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob.Target
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
||||
import eu.kanade.tachiyomi.extension.ShizukuInstaller
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
|
||||
|
@ -298,26 +301,45 @@ 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
|
||||
|
||||
intListPreference(activity) {
|
||||
bindTo(preferences.extensionInstaller())
|
||||
titleRes = R.string.ext_installer_pref
|
||||
entriesRes = arrayOf(
|
||||
R.string.default_value,
|
||||
R.string.ext_installer_shizuku,
|
||||
R.string.ext_installer_private,
|
||||
)
|
||||
entryValues = listOf(
|
||||
ExtensionInstaller.PACKAGE_INSTALLER,
|
||||
ExtensionInstaller.SHIZUKU,
|
||||
ExtensionInstaller.PRIVATE,
|
||||
)
|
||||
|
||||
onChange {
|
||||
it as Boolean
|
||||
if (it && !context.isPackageInstalled(ShizukuInstaller.shizukuPkgName) && !Sui.isSui()) {
|
||||
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 {
|
||||
true
|
||||
it as Int
|
||||
if (it == ExtensionInstaller.SHIZUKU) {
|
||||
return@onChange if (!context.isPackageInstalled(ShizukuInstaller.shizukuPkgName) && !Sui.isSui()) {
|
||||
context.materialAlertDialog()
|
||||
.setTitle(R.string.ext_installer_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 {
|
||||
true
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
infoPreference(R.string.ext_installer_summary).apply {
|
||||
preferences.extensionInstaller().asImmediateFlowIn(viewScope) {
|
||||
isVisible =
|
||||
it != ExtensionInstaller.PACKAGE_INSTALLER && Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
|
|||
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
||||
|
@ -49,7 +50,7 @@ class SettingsBrowseController : SettingsController() {
|
|||
true
|
||||
}
|
||||
}
|
||||
if (ExtensionManager.canAutoInstallUpdates(context)) {
|
||||
if (ExtensionManager.canAutoInstallUpdates()) {
|
||||
val intPref = intListPreference(activity) {
|
||||
key = PreferenceKeys.autoUpdateExtensions
|
||||
titleRes = R.string.auto_update_extensions
|
||||
|
@ -61,7 +62,7 @@ class SettingsBrowseController : SettingsController() {
|
|||
)
|
||||
defaultValue = AppDownloadInstallJob.ONLY_ON_UNMETERED
|
||||
}
|
||||
val infoPref = if (!preferences.useShizukuForExtensions()) {
|
||||
val infoPref = if (preferences.extensionInstaller().get() != ExtensionInstaller.SHIZUKU) {
|
||||
infoPreference(R.string.some_extensions_may_not_update)
|
||||
} else {
|
||||
null
|
||||
|
|
|
@ -329,9 +329,10 @@
|
|||
<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="ext_installer_pref">Installer</string>
|
||||
<string name="ext_installer_shizuku" translatable="false">Shizuku</string>
|
||||
<string name="ext_installer_private" translatable="false">Private</string>
|
||||
<string name="ext_installer_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