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:
Jays2Kings 2023-10-04 20:43:25 -07:00
parent ba74224e06
commit 89ad5e03c1
13 changed files with 356 additions and 150 deletions

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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 =

View file

@ -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)
}
}
}
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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() {

View file

@ -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
}
}
}

View file

@ -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)

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
@ -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
}
}
}

View file

@ -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

View file

@ -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>