|  | @@ -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 {
 | 
	
		
			
				|  |  | -            context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
 | 
	
		
			
				|  |  | -        } catch (error: PackageManager.NameNotFoundException) {
 | 
	
		
			
				|  |  | -            // Unlikely, but the package may have been uninstalled at this point
 | 
	
		
			
				|  |  | -            logcat(LogPriority.ERROR, error)
 | 
	
		
			
				|  |  | +        val extensionPackage = getExtensionInfoFromPkgName(context, pkgName)
 | 
	
		
			
				|  |  | +        if (extensionPackage == null) {
 | 
	
		
			
				|  |  | +            logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
 | 
	
		
			
				|  |  |              return LoadResult.Error
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -        if (!isPackageAnExtension(pkgInfo)) {
 | 
	
		
			
				|  |  | -            logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($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
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -        return loadExtension(context, pkgName, pkgInfo)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        val sharedPkg = try {
 | 
	
		
			
				|  |  | +            context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
 | 
	
		
			
				|  |  | +                .takeIf { isPackageAnExtension(it) }
 | 
	
		
			
				|  |  | +                ?.let {
 | 
	
		
			
				|  |  | +                    ExtensionInfo(
 | 
	
		
			
				|  |  | +                        packageInfo = it,
 | 
	
		
			
				|  |  | +                        isShared = true,
 | 
	
		
			
				|  |  | +                    )
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +        } catch (error: PackageManager.NameNotFoundException) {
 | 
	
		
			
				|  |  | +            null
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        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<String>? {
 | 
	
		
			
				|  |  | +        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<String>): Boolean {
 | 
	
		
			
				|  |  | +        return trustedSignatures.any { signatures.contains(it) }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private fun isOfficiallySigned(signatures: List<String>): 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,
 | 
	
		
			
				|  |  | +    )
 | 
	
		
			
				|  |  |  }
 |