diff --git a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt index eb69664f71..34ef79b483 100644 --- a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt @@ -24,5 +24,6 @@ class BasePreferences( LEGACY(R.string.ext_installer_legacy), PACKAGEINSTALLER(R.string.ext_installer_packageinstaller), SHIZUKU(R.string.ext_installer_shizuku), + PRIVATE(R.string.ext_installer_private), } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index 3264b5b477..b319f9fdc6 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -10,12 +10,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.HelpOutline @@ -176,7 +174,8 @@ private fun ExtensionDetails( data = Uri.fromParts("package", extension.pkgName, null) context.startActivity(this) } - }, + Unit + }.takeIf { extension.isShared }, onClickAgeRating = { showNsfwWarning = true }, @@ -209,7 +208,7 @@ private fun DetailsHeader( extension: Extension, onClickAgeRating: () -> Unit, onClickUninstall: () -> Unit, - onClickAppInfo: () -> Unit, + onClickAppInfo: (() -> Unit)?, ) { val context = LocalContext.current @@ -293,6 +292,7 @@ private fun DetailsHeader( top = MaterialTheme.padding.small, bottom = MaterialTheme.padding.medium, ), + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { OutlinedButton( modifier = Modifier.weight(1f), @@ -301,16 +301,16 @@ private fun DetailsHeader( Text(stringResource(R.string.ext_uninstall)) } - Spacer(Modifier.width(16.dp)) - - Button( - modifier = Modifier.weight(1f), - onClick = onClickAppInfo, - ) { - Text( - text = stringResource(R.string.ext_app_info), - color = MaterialTheme.colorScheme.onPrimary, - ) + if (onClickAppInfo != null) { + Button( + modifier = Modifier.weight(1f), + onClick = onClickAppInfo, + ) { + Text( + text = stringResource(R.string.ext_app_info), + color = MaterialTheme.colorScheme.onPrimary, + ) + } } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt index 1d71e9a906..fbe82ad6e6 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.browse.components -import android.content.pm.PackageManager import android.util.DisplayMetrics import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box @@ -31,6 +30,7 @@ import eu.kanade.domain.source.model.icon import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.extension.util.ExtensionLoader import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.source.model.Source import tachiyomi.source.local.isLocal @@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St return produceState>(initialValue = Result.Loading, this) { withIOContext { value = try { - val appInfo = context.packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) + val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo val appResources = context.packageManager.getResourcesForApplication(appInfo) Result.Success( appResources.getDrawableForDensity(appInfo.icon, density, null)!! diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index f35e919d19..49ca43c89d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -66,7 +66,10 @@ class ExtensionManager( fun getAppIconForSource(sourceId: Long): Drawable? { val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName if (pkgName != null) { - return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) } + return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { + ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo + .loadIcon(context.packageManager) + } } return null } @@ -333,6 +336,7 @@ class ExtensionManager( } override fun onPackageUninstalled(pkgName: String) { + ExtensionLoader.uninstallPrivateExtension(context, pkgName) unregisterExtension(pkgName) updatePendingUpdatesCount() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index 92a80e237f..e7eab29b61 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -32,6 +32,7 @@ sealed class Extension { val hasUpdate: Boolean = false, val isObsolete: Boolean = false, val isUnofficial: Boolean = false, + val isShared: Boolean, ) : Extension() data class Available( diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index 80d240ee0c..a2404fd40f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -4,6 +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 kotlinx.coroutines.CoroutineStart @@ -27,7 +30,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : * Registers this broadcast receiver */ fun register(context: Context) { - context.registerReceiver(this, filter) + ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED) } /** @@ -38,6 +41,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") } @@ -49,7 +55,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)) return launchNow { @@ -60,7 +66,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) @@ -70,7 +76,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : } } } - Intent.ACTION_PACKAGE_REMOVED -> { + Intent.ACTION_PACKAGE_REMOVED, ACTION_EXTENSION_REMOVED -> { if (isReplacing(intent)) return val pkgName = getPackageNameFromIntent(intent) @@ -121,4 +127,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) + } + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 29c910a208..5ea3a3ee64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -11,10 +11,12 @@ import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.net.toUri import eu.kanade.domain.base.BasePreferences +import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.installer.Installer import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.storage.getUriCompat +import eu.kanade.tachiyomi.util.system.isPackageInstalled import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -156,6 +158,35 @@ internal class ExtensionInstaller(private val context: Context) { context.startActivity(intent) } + BasePreferences.ExtensionInstaller.PRIVATE -> { + val extensionManager = Injekt.get() + val tempFile = File(context.cacheDir, "temp_$downloadId") + + if (tempFile.exists() && !tempFile.delete()) { + // Unlikely but just in case + extensionManager.updateInstallStep(downloadId, InstallStep.Error) + return + } + + try { + context.contentResolver.openInputStream(uri)?.use { input -> + tempFile.outputStream().use { output -> + input.copyTo(output) + } + } + + if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) { + extensionManager.updateInstallStep(downloadId, InstallStep.Installed) + } else { + extensionManager.updateInstallStep(downloadId, InstallStep.Error) + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." } + extensionManager.updateInstallStep(downloadId, InstallStep.Error) + } + + tempFile.delete() + } else -> { val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer) ContextCompat.startForegroundService(context, intent) @@ -178,10 +209,15 @@ internal class ExtensionInstaller(private val context: Context) { * @param pkgName The package name of the extension to uninstall */ fun uninstallApk(pkgName: String) { - val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri()) - .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) + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index de97e8cbeb..f6ab9f577e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.extension.util -import android.annotation.SuppressLint import android.content.Context +import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build @@ -14,17 +14,28 @@ import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.util.lang.Hash -import eu.kanade.tachiyomi.util.system.getApplicationIcon import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import logcat.LogPriority import tachiyomi.core.util.system.logcat import uy.kohesive.injekt.injectLazy +import java.io.File /** - * Class that handles the loading of the extensions installed in the system. + * Class that handles the loading of the extensions. Supports two kinds of extensions: + * + * 1. Shared extension: This extension is installed to the system with package + * installer, so other variants of Tachiyomi and its forks can also use this extension. + * + * 2. Private extension: This extension is put inside private data directory of the + * running app, so this extension can only be used by the running app and not shared + * with other apps. + * + * When both kinds of extensions are installed with a same package name, shared + * extension will be used unless the version codes are different. In that case the + * one with higher version code will be used. */ -@SuppressLint("PackageManagerGetSignatures") internal object ExtensionLoader { private val preferences: SourcePreferences by injectLazy() @@ -41,12 +52,11 @@ internal object ExtensionLoader { const val LIB_VERSION_MIN = 1.4 const val LIB_VERSION_MAX = 1.5 - private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES - } else { - @Suppress("DEPRECATION") - PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES - } + @Suppress("DEPRECATION") + private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or + PackageManager.GET_META_DATA or + PackageManager.GET_SIGNATURES or + (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0) // inorichi's key private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" @@ -56,8 +66,57 @@ 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) + ) { + logcat(LogPriority.ERROR) { "Installed extension version is higher. Downgrading is not allowed." } + return false + } + + val extensionSignatures = getSignatures(extension) + if (extensionSignatures.isNullOrEmpty()) { + logcat(LogPriority.ERROR) { "Extension to be installed is not signed." } + return false + } + + if (!extensionSignatures.containsAll(getSignatures(currentExtension)!!)) { + logcat(LogPriority.ERROR) { "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) { + logcat(LogPriority.ERROR, 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. + * Return a list of all the available extensions initialized concurrently. * * @param context The application context. */ @@ -70,16 +129,43 @@ internal object ExtensionLoader { pkgManager.getInstalledPackages(PACKAGE_FLAGS) } - val extPkgs = installedPkgs.filter { isPackageAnExtension(it) } + val sharedExtPkgs = installedPkgs + .asSequence() + .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 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) + } + .toList() if (extPkgs.isEmpty()) return emptyList() // Load each extension concurrently and wait for completion return runBlocking { val deferred = extPkgs.map { - async { loadExtension(context, it.packageName, it) } + async { loadExtension(context, it) } } - deferred.map { it.await() } + deferred.awaitAll() } } @@ -88,37 +174,61 @@ internal object ExtensionLoader { * contains the required feature flag before trying to load it. */ fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult { - val pkgInfo = try { + val extensionPackage = getExtensionInfoFromPkgName(context, pkgName) + if (extensionPackage == null) { + logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" } + return LoadResult.Error + } + return loadExtension(context, extensionPackage) + } + + fun getExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? { + return getExtensionInfoFromPkgName(context, pkgName)?.packageInfo + } + + 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 sharedPkg = try { context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) + .takeIf { isPackageAnExtension(it) } + ?.let { + ExtensionInfo( + packageInfo = it, + isShared = true, + ) + } } catch (error: PackageManager.NameNotFoundException) { - // Unlikely, but the package may have been uninstalled at this point - logcat(LogPriority.ERROR, error) - return LoadResult.Error + null } - if (!isPackageAnExtension(pkgInfo)) { - logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" } - return LoadResult.Error - } - return loadExtension(context, pkgName, pkgInfo) + + return selectExtensionPackage(sharedPkg, privatePkg) } /** - * Loads an extension given its package name. + * Loads an extension * * @param context The application context. - * @param pkgName The package name of the extension to load. - * @param pkgInfo The package info of the extension. + * @param extensionInfo The extension to load. */ - private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult { + private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { val pkgManager = context.packageManager - - val appInfo = try { - pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) - } catch (error: PackageManager.NameNotFoundException) { - // Unlikely, but the package may have been uninstalled at this point - logcat(LogPriority.ERROR, error) - return LoadResult.Error - } + val pkgInfo = extensionInfo.packageInfo + val appInfo = pkgInfo.applicationInfo + val pkgName = pkgInfo.packageName val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") val versionName = pkgInfo.versionName @@ -139,12 +249,19 @@ internal object ExtensionLoader { return LoadResult.Error } - val signatureHash = getSignatureHash(context, pkgInfo) - if (signatureHash == null) { + val signatures = getSignatures(pkgInfo) + if (signatures.isNullOrEmpty()) { logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } return LoadResult.Error - } else if (signatureHash !in trustedSignatures) { - val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash) + } else if (!hasTrustedSignature(signatures)) { + val extension = Extension.Untrusted( + extName, + pkgName, + versionName, + versionCode, + libVersion, + signatures.last(), + ) logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" } return LoadResult.Untrusted(extension) } @@ -204,12 +321,35 @@ internal object ExtensionLoader { hasChangelog = hasChangelog, sources = sources, pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY), - isUnofficial = signatureHash != officialSignature, - icon = context.getApplicationIcon(pkgName), + isUnofficial = !isOfficiallySigned(signatures), + icon = appInfo.loadIcon(pkgManager), + isShared = extensionInfo.isShared, ) return LoadResult.Success(extension) } + /** + * Choose which extension package to use based on version code + * + * @param shared extension installed to system + * @param private extension installed to data directory + */ + private fun selectExtensionPackage(shared: ExtensionInfo?, private: ExtensionInfo?): ExtensionInfo? { + when { + private == null && shared != null -> return shared + shared == null && private != null -> return private + shared == null && private == null -> return null + } + + return if (PackageInfoCompat.getLongVersionCode(shared!!.packageInfo) >= + PackageInfoCompat.getLongVersionCode(private!!.packageInfo) + ) { + shared + } else { + private + } + } + /** * Returns true if the given package is an extension. * @@ -220,12 +360,50 @@ internal object ExtensionLoader { } /** - * Returns the signature hash of the package or null if it's not signed. + * Returns the signatures of the package or null if it's not signed. * * @param pkgInfo The package info of the application. + * @return List SHA256 digest of the signatures */ - private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? { - val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName) - return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) } + private fun getSignatures(pkgInfo: PackageInfo): List? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val signingInfo = pkgInfo.signingInfo + if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners + } else { + signingInfo.signingCertificateHistory + } + } else { + @Suppress("DEPRECATION") + pkgInfo.signatures + } + ?.map { Hash.sha256(it.toByteArray()) } + ?.toList() } + + private fun hasTrustedSignature(signatures: List): Boolean { + return trustedSignatures.any { signatures.contains(it) } + } + + private fun isOfficiallySigned(signatures: List): Boolean { + return signatures.all { it == officialSignature } + } + + /** + * On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't + * have sourceDir which breaks assets loading (used for getting icon here). + */ + private fun ApplicationInfo.fixBasePaths(apkPath: String) { + if (sourceDir == null) { + sourceDir = apkPath + } + if (publicSourceDir == null) { + publicSourceDir = apkPath + } + } + + private data class ExtensionInfo( + val packageInfo: PackageInfo, + val isShared: Boolean, + ) } diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 4ae1995384..7f8c138f37 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -314,6 +314,7 @@ Legacy PackageInstaller Shizuku + Private Shizuku is not running Install and start Shizuku to use Shizuku as extension installer.