@ -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 avai lab le 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 ( !is PackageAnExtension ( 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: Extension Info) : 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 signature Ha sh = getSignature Ha sh ( context , pkgInfo )
if ( signature Hash == null ) {
val signature s = getSignature s( pkgInfo )
if ( signature s. 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 = !is OfficiallySigned ( 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 ha sh of the package or null if it ' s not signed .
* Returns the signature s 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 ,
)
}