Merge pull request #271 from RepoDevil/cleanup

The motherload
This commit is contained in:
rebel onion 2024-03-23 16:56:18 -05:00 committed by GitHub
commit d43d643bbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
163 changed files with 5058 additions and 5910 deletions

View file

@ -20,7 +20,7 @@ class ExtensionInstallerPreference(
val entries
get() = ExtensionInstaller.values().run {
get() = ExtensionInstaller.entries.toTypedArray().run {
if (context.hasMiuiPackageInstaller) {
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
} else {

View file

@ -58,8 +58,7 @@ interface AnimeSource {
*/
@Suppress("DEPRECATION")
suspend fun getVideoList(episode: SEpisode): List<Video> {
val list = fetchVideoList(episode).awaitSingle()
return list
return fetchVideoList(episode).awaitSingle()
}
@Deprecated(

View file

@ -10,10 +10,11 @@ import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.preference.plusAssign
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -51,7 +52,7 @@ class AnimeExtensionManager(
/**
* The installer which installs, updates and uninstalls the anime extensions.
*/
private val installer by lazy { AnimeExtensionInstaller(context) }
private val installer by lazy { ExtensionInstaller(context) }
private val iconMap = mutableMapOf<String, Drawable>()
@ -92,14 +93,14 @@ class AnimeExtensionManager(
init {
initAnimeExtensions()
AnimeExtensionInstallReceiver(AnimeInstallationListener()).register(context)
ExtensionInstallReceiver().setAnimeListener(InstallationListener()).register(context)
}
/**
* Loads and registers the installed animeextensions.
*/
private fun initAnimeExtensions() {
val animeextensions = AnimeExtensionLoader.loadExtensions(context)
val animeextensions = ExtensionLoader.loadAnimeExtensions(context)
_installedAnimeExtensionsFlow.value = animeextensions
.filterIsInstance<AnimeLoadResult.Success>()
@ -254,12 +255,13 @@ class AnimeExtensionManager(
*
* @param signature The signature to whitelist.
*/
@OptIn(DelicateCoroutinesApi::class)
fun trustSignature(signature: String) {
val untrustedSignatures =
_untrustedAnimeExtensionsFlow.value.map { it.signatureHash }.toSet()
if (signature !in untrustedSignatures) return
AnimeExtensionLoader.trustedSignatures += signature
ExtensionLoader.trustedSignaturesAnime += signature
preferences.trustedSignatures() += signature
val nowTrustedAnimeExtensions =
@ -271,7 +273,7 @@ class AnimeExtensionManager(
nowTrustedAnimeExtensions
.map { animeextension ->
async {
AnimeExtensionLoader.loadExtensionFromPkgName(
ExtensionLoader.loadAnimeExtensionFromPkgName(
ctx,
animeextension.pkgName
)
@ -333,7 +335,7 @@ class AnimeExtensionManager(
/**
* Listener which receives events of the anime extensions being installed, updated or removed.
*/
private inner class AnimeInstallationListener : AnimeExtensionInstallReceiver.Listener {
private inner class InstallationListener : ExtensionInstallReceiver.AnimeListener {
override fun onExtensionInstalled(extension: AnimeExtension.Installed) {
registerNewExtension(extension.withUpdateCheck())

View file

@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
@ -87,7 +87,7 @@ internal class AnimeExtensionGithubApi {
findExtensions().also { lastExtCheck.set(Date().time) }
}
val installedExtensions = AnimeExtensionLoader.loadExtensions(context)
val installedExtensions = ExtensionLoader.loadAnimeExtensions(context)
.filterIsInstance<AnimeLoadResult.Success>()
.map { it.extension }
@ -115,7 +115,7 @@ internal class AnimeExtensionGithubApi {
return this
.filter {
val libVersion = it.extractLibVersion()
libVersion >= AnimeExtensionLoader.LIB_VERSION_MIN && libVersion <= AnimeExtensionLoader.LIB_VERSION_MAX
libVersion >= ExtensionLoader.ANIME_LIB_VERSION_MIN && libVersion <= ExtensionLoader.ANIME_LIB_VERSION_MAX
}
.map {
AnimeExtension.Available(

View file

@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.extension.anime.model
sealed class AnimeLoadResult {
class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult()
class Untrusted(val extension: AnimeExtension.Untrusted) : AnimeLoadResult()
object Error : AnimeLoadResult()
data object Error : AnimeLoadResult()
}

View file

@ -1,81 +0,0 @@
package eu.kanade.tachiyomi.extension.anime.util
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import ani.dantotsu.themes.ThemeManager
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
/**
* Activity used to install extensions, because we can only receive the result of the installation
* with [startActivityForResult], which we need to update the UI.
*/
class AnimeExtensionInstallActivity : Activity() {
// MIUI package installer bug workaround
private var ignoreUntil = 0L
private var ignoreResult = false
private var hasIgnoredResult = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type)
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (hasMiuiPackageInstaller) {
ignoreResult = true
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
}
try {
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
} catch (error: Exception) {
// Either install package can't be found (probably bots) or there's a security exception
// with the download manager. Nothing we can workaround.
toast(error.message)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (ignoreResult && System.nanoTime() < ignoreUntil) {
hasIgnoredResult = true
return
}
if (requestCode == INSTALL_REQUEST_CODE) {
checkInstallationResult(resultCode)
}
finish()
}
override fun onStart() {
super.onStart()
if (hasIgnoredResult) {
checkInstallationResult(RESULT_CANCELED)
finish()
}
}
private fun checkInstallationResult(resultCode: Int) {
val downloadId = intent.extras!!.getLong(AnimeExtensionInstaller.EXTRA_DOWNLOAD_ID)
val extensionManager = Injekt.get<AnimeExtensionManager>()
val newStep = when (resultCode) {
RESULT_OK -> InstallStep.Installed
RESULT_CANCELED -> InstallStep.Idle
else -> InstallStep.Error
}
extensionManager.updateInstallStep(downloadId, newStep)
}
}
private const val INSTALL_REQUEST_CODE = 500

View file

@ -1,132 +0,0 @@
package eu.kanade.tachiyomi.extension.anime.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import tachiyomi.core.util.lang.launchNow
/**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
* notifies the given [listener] when the package is an extension.
*
* @param listener The listener that should be notified of extension installation events.
*/
internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
BroadcastReceiver() {
/**
* Registers this broadcast receiver
*/
fun register(context: Context) {
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
/**
* Returns the intent filter this receiver should subscribe to.
*/
private val filter
get() = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
/**
* Called when one of the events of the [filter] is received. When the package is an extension,
* it's loaded in background and it notifies the [listener] when finished.
*/
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
if (isReplacing(intent)) return
launchNow {
when (val result = getExtensionFromIntent(context, intent)) {
is AnimeLoadResult.Success -> listener.onExtensionInstalled(result.extension)
is AnimeLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
launchNow {
when (val result = getExtensionFromIntent(context, intent)) {
is AnimeLoadResult.Success -> listener.onExtensionUpdated(result.extension)
// Not needed as a package can't be upgraded if the signature is different
// is LoadResult.Untrusted -> {}
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
if (isReplacing(intent)) return
val pkgName = getPackageNameFromIntent(intent)
if (pkgName != null) {
listener.onPackageUninstalled(pkgName)
}
}
}
}
/**
* Returns true if this package is performing an update.
*
* @param intent The intent that triggered the event.
*/
private fun isReplacing(intent: Intent): Boolean {
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
}
/**
* Returns the extension triggered by the given intent.
*
* @param context The application context.
* @param intent The intent containing the package name of the extension.
*/
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult {
val pkgName = getPackageNameFromIntent(intent)
if (pkgName == null) {
Logger.log("Package name not found")
return AnimeLoadResult.Error
}
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
AnimeExtensionLoader.loadExtensionFromPkgName(
context,
pkgName,
)
}.await()
}
/**
* Returns the package name of the installed, updated or removed application.
*/
private fun getPackageNameFromIntent(intent: Intent?): String? {
return intent?.data?.encodedSchemeSpecificPart ?: return null
}
/**
* Listener that receives extension installation events.
*/
interface Listener {
fun onExtensionInstalled(extension: AnimeExtension.Installed)
fun onExtensionUpdated(extension: AnimeExtension.Installed)
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
fun onPackageUninstalled(pkgName: String)
}
}

View file

@ -1,94 +0,0 @@
package eu.kanade.tachiyomi.extension.anime.util
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.Build
import android.os.IBinder
import ani.dantotsu.R
import ani.dantotsu.util.Logger
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
import eu.kanade.tachiyomi.extension.anime.installer.PackageInstallerInstallerAnime
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
import eu.kanade.tachiyomi.util.system.notificationBuilder
class AnimeExtensionInstallService : Service() {
private var installer: InstallerAnime? = null
override fun onCreate() {
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
setSmallIcon(R.drawable.ic_download_24)
setAutoCancel(false)
setOngoing(true)
setShowWhen(false)
setContentTitle("Installing Anime Extension...")
setProgress(100, 100, true)
}.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
Notifications.ID_EXTENSION_INSTALLER,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.data
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
EXTRA_INSTALLER,
)
if (uri == null || id == null || installerUsed == null) {
stopSelf()
return START_NOT_STICKY
}
if (installer == null) {
installer = when (installerUsed) {
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerAnime(
this
)
else -> {
Logger.log("Not implemented for installer $installerUsed")
stopSelf()
return START_NOT_STICKY
}
}
}
installer!!.addToQueue(id, uri)
return START_NOT_STICKY
}
override fun onDestroy() {
installer?.onDestroy()
installer = null
}
override fun onBind(i: Intent?): IBinder? = null
companion object {
private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
fun getIntent(
context: Context,
downloadId: Long,
uri: Uri,
installer: BasePreferences.ExtensionInstaller,
): Intent {
return Intent(context, AnimeExtensionInstallService::class.java)
.setDataAndType(uri, AnimeExtensionInstaller.APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.putExtra(EXTRA_INSTALLER, installer)
}
}
}

View file

@ -1,273 +0,0 @@
package eu.kanade.tachiyomi.extension.anime.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import ani.dantotsu.util.Logger
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.util.storage.getUriCompat
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.TimeUnit
/**
* The installer which installs, updates and uninstalls the extensions.
*
* @param context The application context.
*/
internal class AnimeExtensionInstaller(private val context: Context) {
/**
* The system's download manager
*/
private val downloadManager = context.getSystemService<DownloadManager>()!!
/**
* The broadcast receiver which listens to download completion events.
*/
private val downloadReceiver = DownloadCompletionReceiver()
/**
* The currently requested downloads, with the package name (unique id) as key, and the id
* returned by the download manager.
*/
private val activeDownloads = hashMapOf<String, Long>()
/**
* Relay used to notify the installation step of every download.
*/
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
/**
* Adds the given extension to the downloads queue and returns an observable containing its
* step in the installation process.
*
* @param url The url of the apk.
* @param extension The extension to install.
*/
fun downloadAndInstall(url: String, extension: AnimeExtension) = Observable.defer {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
if (oldDownload != null) {
deleteDownload(pkgName)
}
// Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register()
val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
.setDestinationInExternalFilesDir(
context,
Environment.DIRECTORY_DOWNLOADS,
downloadUri.lastPathSegment
)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id }
.map { it.second }
// Poll download status
.mergeWith(pollStatus(id))
// Stop when the application is installed or errors
.takeUntil { it.isCompleted() }
// Always notify on main thread
.observeOn(AndroidSchedulers.mainThread())
// Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) }
}
/**
* Returns an observable that polls the given download id for its status every second, as the
* manager doesn't have any notification system. It'll stop once the download finishes.
*
* @param id The id of the download to poll.
*/
private fun pollStatus(id: Long): Observable<InstallStep> {
val query = DownloadManager.Query().setFilterById(id)
return Observable.interval(0, 1, TimeUnit.SECONDS)
// Get the current download status
.map {
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
} else {
DownloadManager.STATUS_FAILED
}
}
}
// Ignore duplicate results
.distinctUntilChanged()
// Stop polling when the download fails or finishes
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
// Map to our model
.flatMap { status ->
when (status) {
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
else -> Observable.empty()
}
}
}
/**
* Starts an intent to install the extension at the given uri.
*
* @param uri The uri of the extension to install.
*/
fun installApk(downloadId: Long, uri: Uri) {
when (val installer = extensionInstaller.get()) {
BasePreferences.ExtensionInstaller.LEGACY -> {
val intent = Intent(context, AnimeExtensionInstallActivity::class.java)
.setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
}
else -> {
val intent =
AnimeExtensionInstallService.getIntent(context, downloadId, uri, installer)
ContextCompat.startForegroundService(context, intent)
}
}
}
/**
* Cancels extension install and remove from download manager and installer.
*/
fun cancelInstall(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName) ?: return
downloadManager.remove(downloadId)
InstallerAnime.cancelInstallQueue(context, downloadId)
}
/**
* Starts an intent to uninstall the extension by the given package name.
*
* @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)
}
/**
* Sets the step of the installation of an extension.
*
* @param downloadId The id of the download.
* @param step New install step.
*/
fun updateInstallStep(downloadId: Long, step: InstallStep) {
downloadsRelay.call(downloadId to step)
}
/**
* Deletes the download for the given package name.
*
* @param pkgName The package name of the download to delete.
*/
private fun deleteDownload(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName)
if (downloadId != null) {
downloadManager.remove(downloadId)
}
if (activeDownloads.isEmpty()) {
downloadReceiver.unregister()
}
}
/**
* Receiver that listens to download status events.
*/
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
/**
* Whether this receiver is currently registered.
*/
private var isRegistered = false
/**
* Registers this receiver if it's not already.
*/
fun register() {
if (isRegistered) return
isRegistered = true
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
/**
* Unregisters this receiver if it's not already.
*/
fun unregister() {
if (!isRegistered) return
isRegistered = false
context.unregisterReceiver(this)
}
/**
* Called when a download event is received. It looks for the download in the current active
* downloads and notifies its installation step.
*/
override fun onReceive(context: Context, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
// Avoid events for downloads we didn't request
if (id !in activeDownloads.values) return
val uri = downloadManager.getUriForDownloadedFile(id)
// Set next installation step
if (uri == null) {
Logger.log("Couldn't locate downloaded APK")
downloadsRelay.call(id to InstallStep.Error)
return
}
val query = DownloadManager.Query().setFilterById(id)
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val localUri = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
).removePrefix(FILE_SCHEME)
installApk(id, File(localUri).getUriCompat(context))
}
}
}
}
companion object {
const val APK_MIME = "application/vnd.android.package-archive"
const val EXTRA_DOWNLOAD_ID = "AnimeExtensionInstaller.extra.DOWNLOAD_ID"
const val FILE_SCHEME = "file://"
}
}

View file

@ -1,244 +0,0 @@
package eu.kanade.tachiyomi.extension.anime.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import ani.dantotsu.util.Logger
import dalvik.system.PathClassLoader
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.system.getApplicationIcon
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
*/
@SuppressLint("PackageManagerGetSignatures")
internal object AnimeExtensionLoader {
private val preferences: SourcePreferences by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.animeextension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
private const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
private const val METADATA_HAS_README = "tachiyomi.animeextension.hasReadme"
private const val METADATA_HAS_CHANGELOG = "tachiyomi.animeextension.hasChangelog"
const val LIB_VERSION_MIN = 12
const val LIB_VERSION_MAX = 15
private const val PACKAGE_FLAGS =
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// jmir1's key
private const val officialSignature =
"50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
/**
* List of the trusted signatures.
*/
var trustedSignatures =
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
*
* @param context The application context.
*/
fun loadExtensions(context: Context): List<AnimeLoadResult> {
val pkgManager = context.packageManager
@Suppress("DEPRECATION")
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
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) }
}
deferred.map { it.await() }
}
}
/**
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
fun loadExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
val pkgInfo = try {
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
Logger.log(error)
return AnimeLoadResult.Error
}
if (!isPackageAnExtension(pkgInfo)) {
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
return AnimeLoadResult.Error
}
return loadExtension(context, pkgName, pkgInfo)
}
/**
* Loads an extension given its package name.
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
private fun loadExtension(
context: Context,
pkgName: String,
pkgInfo: PackageInfo
): AnimeLoadResult {
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
Logger.log(error)
return AnimeLoadResult.Error
}
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
Logger.log("Missing versionName for extension $extName")
return AnimeLoadResult.Error
}
// Validate lib version
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
Logger.log("Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
)
return AnimeLoadResult.Error
}
val signatureHash = getSignatureHash(pkgInfo)
if (signatureHash == null) {
Logger.log("Package $pkgName isn't signed")
return AnimeLoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = AnimeExtension.Untrusted(
extName,
pkgName,
versionName,
versionCode,
libVersion,
signatureHash
)
Logger.log("Extension $pkgName isn't trusted")
return AnimeLoadResult.Untrusted(extension)
}
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (!loadNsfwSource && isNsfw) {
Logger.log("NSFW extension $pkgName not allowed")
return AnimeLoadResult.Error
}
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
.split(";")
.map {
val sourceClass = it.trim()
if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass
} else {
sourceClass
}
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is AnimeSource -> listOf(obj)
is AnimeSourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Logger.log("Extension load error: $extName ($it)")
return AnimeLoadResult.Error
}
}
val langs = sources.filterIsInstance<AnimeCatalogueSource>()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extension = AnimeExtension.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
hasReadme = hasReadme,
hasChangelog = hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature,
icon = context.getApplicationIcon(pkgName),
)
return AnimeLoadResult.Success(extension)
}
/**
* Returns true if the given package is an extension.
*
* @param pkgInfo The package info of the application.
*/
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
}
/**
* Returns the signature hash of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
*/
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
}
}

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.anime.installer
package eu.kanade.tachiyomi.extension.installer
import android.app.Service
import android.content.BroadcastReceiver
@ -8,8 +8,11 @@ import android.content.IntentFilter
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.novel.NovelExtensionManager
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import uy.kohesive.injekt.injectLazy
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
@ -17,9 +20,11 @@ import java.util.concurrent.atomic.AtomicReference
/**
* Base implementation class for extension installer. To be used inside a foreground [Service].
*/
abstract class InstallerAnime(private val service: Service) {
abstract class Installer(private val service: Service) {
private val extensionManager: AnimeExtensionManager by injectLazy()
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
private val novelExtensionManager: NovelExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
@ -44,8 +49,8 @@ abstract class InstallerAnime(private val service: Service) {
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, uri: Uri) {
queue.add(Entry(downloadId, uri))
fun addToQueue(type: MediaType, downloadId: Long, uri: Uri) {
queue.add(Entry(type, downloadId, uri))
checkQueue()
}
@ -58,7 +63,11 @@ abstract class InstallerAnime(private val service: Service) {
*/
@CallSuper
open fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId)
when (entry.type) {
MediaType.ANIME -> animeExtensionManager.setInstalling(entry.downloadId)
MediaType.MANGA -> mangaExtensionManager.setInstalling(entry.downloadId)
MediaType.NOVEL -> novelExtensionManager.setInstalling(entry.downloadId)
}
}
/**
@ -81,7 +90,19 @@ abstract class InstallerAnime(private val service: Service) {
fun continueQueue(resultStep: InstallStep) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
when (completedEntry.type) {
MediaType.ANIME -> {
animeExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
}
MediaType.MANGA -> {
mangaExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
}
MediaType.NOVEL -> {
novelExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
}
}
checkQueue()
}
}
@ -113,7 +134,19 @@ abstract class InstallerAnime(private val service: Service) {
@CallSuper
open fun onDestroy() {
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
queue.forEach {
when (it.type) {
MediaType.ANIME -> {
animeExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
}
MediaType.MANGA -> {
mangaExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
}
MediaType.NOVEL -> {
novelExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
}
}
}
queue.clear()
waitingInstall.set(null)
}
@ -135,7 +168,17 @@ abstract class InstallerAnime(private val service: Service) {
this.waitingInstall.set(null)
checkQueue()
}
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
when (toCancel.type) {
MediaType.ANIME -> {
animeExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
}
MediaType.MANGA -> {
mangaExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
}
MediaType.NOVEL -> {
novelExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
}
}
}
}
@ -145,7 +188,7 @@ abstract class InstallerAnime(private val service: Service) {
* @param downloadId Download ID as known by [ExtensionManager]
* @param uri Uri of APK to install
*/
data class Entry(val downloadId: Long, val uri: Uri)
data class Entry(val type: MediaType, val downloadId: Long, val uri: Uri)
init {
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
@ -153,8 +196,8 @@ abstract class InstallerAnime(private val service: Service) {
}
companion object {
private const val ACTION_CANCEL_QUEUE = "InstallerAnime.action.CANCEL_QUEUE"
private const val EXTRA_DOWNLOAD_ID = "InstallerAnime.extra.DOWNLOAD_ID"
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
/**
* Attempts to cancel the installation entry for the provided download ID.

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.anime.installer
package eu.kanade.tachiyomi.extension.installer
import android.app.PendingIntent
import android.app.Service
@ -9,6 +9,7 @@ import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.content.IntentSanitizer
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep
@ -16,7 +17,7 @@ import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.getUriSize
class PackageInstallerInstallerAnime(private val service: Service) : InstallerAnime(service) {
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
private val packageInstaller = service.packageManager.packageInstaller
@ -27,7 +28,18 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
PackageInstaller.STATUS_FAILURE
)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)?.run {
IntentSanitizer.Builder()
.allowAction(this.action!!)
.allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second }
.allowAnyComponent()
.allowPackage {
// There is no way to check the actual installer name so allow all.
true
}
.build()
.sanitizeByFiltering(this)
}
if (userAction == null) {
Logger.log("Fatal error for $intent")
continueQueue(InstallStep.Error)
@ -78,7 +90,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
val intentSender = PendingIntent.getBroadcast(
service,
activeSession!!.second,
Intent(INSTALL_ACTION),
Intent(INSTALL_ACTION).setPackage(service.packageName),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -88,8 +100,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
session.commit(intentSender)
}
} catch (e: Exception) {
Logger.log(e)
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}")
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}\n$e")
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
activeSession?.let { (_, sessionId) ->
packageInstaller.abandonSession(sessionId)
@ -118,7 +129,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
service,
packageActionReceiver,
IntentFilter(INSTALL_ACTION),
ContextCompat.RECEIVER_EXPORTED
ContextCompat.RECEIVER_EXPORTED,
)
}
}

View file

@ -10,9 +10,9 @@ import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.preference.plusAssign
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
@ -51,7 +51,7 @@ class MangaExtensionManager(
/**
* The installer which installs, updates and uninstalls the extensions.
*/
private val installer by lazy { MangaExtensionInstaller(context) }
private val installer by lazy { ExtensionInstaller(context) }
private val iconMap = mutableMapOf<String, Drawable>()
@ -89,14 +89,14 @@ class MangaExtensionManager(
init {
initExtensions()
MangaExtensionInstallReceiver(InstallationListener()).register(context)
ExtensionInstallReceiver().setMangaListener(InstallationListener()).register(context)
}
/**
* Loads and registers the installed extensions.
*/
private fun initExtensions() {
val extensions = MangaExtensionLoader.loadMangaExtensions(context)
val extensions = ExtensionLoader.loadMangaExtensions(context)
_installedExtensionsFlow.value = extensions
.filterIsInstance<MangaLoadResult.Success>()
@ -254,7 +254,7 @@ class MangaExtensionManager(
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
if (signature !in untrustedSignatures) return
MangaExtensionLoader.trustedSignatures += signature
ExtensionLoader.trustedSignaturesManga += signature
preferences.trustedSignatures() += signature
val nowTrustedExtensions =
@ -266,7 +266,7 @@ class MangaExtensionManager(
nowTrustedExtensions
.map { extension ->
async {
MangaExtensionLoader.loadMangaExtensionFromPkgName(
ExtensionLoader.loadMangaExtensionFromPkgName(
ctx,
extension.pkgName
)
@ -326,7 +326,7 @@ class MangaExtensionManager(
/**
* Listener which receives events of the extensions being installed, updated or removed.
*/
private inner class InstallationListener : MangaExtensionInstallReceiver.Listener {
private inner class InstallationListener : ExtensionInstallReceiver.MangaListener {
override fun onExtensionInstalled(extension: MangaExtension.Installed) {
registerNewExtension(extension.withUpdateCheck())

View file

@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
@ -87,7 +87,7 @@ internal class MangaExtensionGithubApi {
findExtensions().also { lastExtCheck.set(Date().time) }
}
val installedExtensions = MangaExtensionLoader.loadMangaExtensions(context)
val installedExtensions = ExtensionLoader.loadMangaExtensions(context)
.filterIsInstance<MangaLoadResult.Success>()
.map { it.extension }
@ -114,7 +114,7 @@ internal class MangaExtensionGithubApi {
return this
.filter {
val libVersion = it.extractLibVersion()
libVersion >= MangaExtensionLoader.LIB_VERSION_MIN && libVersion <= MangaExtensionLoader.LIB_VERSION_MAX
libVersion >= ExtensionLoader.MANGA_LIB_VERSION_MIN && libVersion <= ExtensionLoader.MANGA_LIB_VERSION_MAX
}
.map {
MangaExtension.Available(

View file

@ -1,170 +0,0 @@
package eu.kanade.tachiyomi.extension.manga.installer
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import uy.kohesive.injekt.injectLazy
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
/**
* Base implementation class for extension installer. To be used inside a foreground [Service].
*/
abstract class InstallerManga(private val service: Service) {
private val extensionManager: MangaExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
/**
* Installer readiness. If false, queue check will not run.
*
* @see checkQueue
*/
abstract var ready: Boolean
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, uri: Uri) {
queue.add(Entry(downloadId, uri))
checkQueue()
}
/**
* Proceeds to install the APK of this entry inside this method. Call [continueQueue]
* when the install process for this entry is finished to continue the queue.
*
* @param entry The [Entry] of item to process
* @see continueQueue
*/
@CallSuper
open fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId)
}
/**
* Called before queue continues. Override this to handle when the removed entry is
* currently being processed.
*
* @return true if this entry can be removed from queue.
*/
open fun cancelEntry(entry: Entry): Boolean {
return true
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [MangaExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(resultStep: InstallStep) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
checkQueue()
}
}
/**
* Checks the queue. The provided service will be stopped if the queue is empty.
* Will not be run when not ready.
*
* @see ready
*/
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
service.stopSelf()
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeFirst()
processEntry(nextEntry)
}
}
/**
* Call this method when the provided service is destroyed.
*/
@CallSuper
open fun onDestroy() {
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
queue.clear()
waitingInstall.set(null)
}
protected fun getActiveEntry(): Entry? = waitingInstall.get()
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
*/
private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
}
}
/**
* Install item to queue.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
* @param uri Uri of APK to install
*/
data class Entry(val downloadId: Long, val uri: Uri)
init {
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
}
companion object {
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
/**
* Attempts to cancel the installation entry for the provided download ID.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
*/
fun cancelInstallQueue(context: Context, downloadId: Long) {
val intent = Intent(ACTION_CANCEL_QUEUE)
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
}
}

View file

@ -1,126 +0,0 @@
package eu.kanade.tachiyomi.extension.manga.installer
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import androidx.core.content.ContextCompat
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.getUriSize
class PackageInstallerInstallerManga(private val service: Service) : InstallerManga(service) {
private val packageInstaller = service.packageManager.packageInstaller
private val packageActionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.getIntExtra(
PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE
)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
if (userAction == null) {
Logger.log("Fatal error for $intent")
continueQueue(InstallStep.Error)
return
}
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
service.startActivity(userAction)
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
continueQueue(InstallStep.Idle)
}
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
else -> continueQueue(InstallStep.Error)
}
}
}
private var activeSession: Pair<Entry, Int>? = null
// Always ready
override var ready = true
override fun processEntry(entry: Entry) {
super.processEntry(entry)
activeSession = null
try {
val installParams =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
activeSession = entry to packageInstaller.createSession(installParams)
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
installParams.setSize(fileSize)
val inputStream =
service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
val session = packageInstaller.openSession(activeSession!!.second)
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
session.use {
arrayOf(inputStream, outputStream).use {
inputStream.copyTo(outputStream)
session.fsync(outputStream)
}
val intentSender = PendingIntent.getBroadcast(
service,
activeSession!!.second,
Intent(INSTALL_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else 0
).intentSender
session.commit(intentSender)
}
} catch (e: Exception) {
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}")
Logger.log(e)
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
activeSession?.let { (_, sessionId) ->
packageInstaller.abandonSession(sessionId)
}
continueQueue(InstallStep.Error)
}
}
override fun cancelEntry(entry: Entry): Boolean {
activeSession?.let { (activeEntry, sessionId) ->
if (activeEntry == entry) {
packageInstaller.abandonSession(sessionId)
return false
}
}
return true
}
override fun onDestroy() {
service.unregisterReceiver(packageActionReceiver)
super.onDestroy()
}
init {
ContextCompat.registerReceiver(
service,
packageActionReceiver,
IntentFilter(INSTALL_ACTION),
ContextCompat.RECEIVER_EXPORTED
)
}
}
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"

View file

@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.extension.manga.model
sealed class MangaLoadResult {
class Success(val extension: MangaExtension.Installed) : MangaLoadResult()
class Untrusted(val extension: MangaExtension.Untrusted) : MangaLoadResult()
object Error : MangaLoadResult()
data object Error : MangaLoadResult()
}

View file

@ -1,132 +0,0 @@
package eu.kanade.tachiyomi.extension.manga.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import tachiyomi.core.util.lang.launchNow
/**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
* notifies the given [listener] when the package is an extension.
*
* @param listener The listener that should be notified of extension installation events.
*/
internal class MangaExtensionInstallReceiver(private val listener: Listener) :
BroadcastReceiver() {
/**
* Registers this broadcast receiver
*/
fun register(context: Context) {
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
/**
* Returns the intent filter this receiver should subscribe to.
*/
private val filter
get() = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
/**
* Called when one of the events of the [filter] is received. When the package is an extension,
* it's loaded in background and it notifies the [listener] when finished.
*/
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
if (isReplacing(intent)) return
launchNow {
when (val result = getExtensionFromIntent(context, intent)) {
is MangaLoadResult.Success -> listener.onExtensionInstalled(result.extension)
is MangaLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
launchNow {
when (val result = getExtensionFromIntent(context, intent)) {
is MangaLoadResult.Success -> listener.onExtensionUpdated(result.extension)
// Not needed as a package can't be upgraded if the signature is different
// is LoadResult.Untrusted -> {}
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
if (isReplacing(intent)) return
val pkgName = getPackageNameFromIntent(intent)
if (pkgName != null) {
listener.onPackageUninstalled(pkgName)
}
}
}
}
/**
* Returns true if this package is performing an update.
*
* @param intent The intent that triggered the event.
*/
private fun isReplacing(intent: Intent): Boolean {
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
}
/**
* Returns the extension triggered by the given intent.
*
* @param context The application context.
* @param intent The intent containing the package name of the extension.
*/
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
val pkgName = getPackageNameFromIntent(intent)
if (pkgName == null) {
Logger.log("Package name not found")
return MangaLoadResult.Error
}
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
MangaExtensionLoader.loadMangaExtensionFromPkgName(
context,
pkgName,
)
}.await()
}
/**
* Returns the package name of the installed, updated or removed application.
*/
private fun getPackageNameFromIntent(intent: Intent?): String? {
return intent?.data?.encodedSchemeSpecificPart ?: return null
}
/**
* Listener that receives extension installation events.
*/
interface Listener {
fun onExtensionInstalled(extension: MangaExtension.Installed)
fun onExtensionUpdated(extension: MangaExtension.Installed)
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
fun onPackageUninstalled(pkgName: String)
}
}

View file

@ -1,240 +0,0 @@
package eu.kanade.tachiyomi.extension.manga.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import ani.dantotsu.util.Logger
import dalvik.system.PathClassLoader
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
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.runBlocking
import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
*/
@SuppressLint("PackageManagerGetSignatures")
internal object MangaExtensionLoader {
private val preferences: SourcePreferences by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.5
private const val PACKAGE_FLAGS =
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
private const val officialSignature =
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* List of the trusted signatures.
*/
var trustedSignatures =
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
*
* @param context The application context.
*/
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
val pkgManager = context.packageManager
@Suppress("DEPRECATION")
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
if (extPkgs.isEmpty()) return emptyList()
// Load each extension concurrently and wait for completion
return runBlocking {
val deferred = extPkgs.map {
async { loadMangaExtension(context, it.packageName, it) }
}
deferred.map { it.await() }
}
}
/**
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
val pkgInfo = try {
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
Logger.log(error)
return MangaLoadResult.Error
}
if (!isPackageAnExtension(pkgInfo)) {
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
return MangaLoadResult.Error
}
return loadMangaExtension(context, pkgName, pkgInfo)
}
/**
* Loads an extension given its package name.
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
private fun loadMangaExtension(
context: Context,
pkgName: String,
pkgInfo: PackageInfo
): MangaLoadResult {
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
Logger.log(error)
return MangaLoadResult.Error
}
val extName =
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
Logger.log("Missing versionName for extension $extName")
return MangaLoadResult.Error
}
// Validate lib version
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
Logger.log("Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
)
return MangaLoadResult.Error
}
val signatureHash = getSignatureHash(pkgInfo)
/* temporarily disabling signature check TODO: remove?
if (signatureHash == null) {
Logger.log("Package $pkgName isn't signed")
return MangaLoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
Logger.log("Extension $pkgName isn't trusted")
return MangaLoadResult.Untrusted(extension)
}
*/
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (!loadNsfwSource && isNsfw) {
Logger.log("NSFW extension $pkgName not allowed")
return MangaLoadResult.Error
}
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
.split(";")
.map {
val sourceClass = it.trim()
if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass
} else {
sourceClass
}
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is MangaSource -> listOf(obj)
is SourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Logger.log("Extension load error: $extName ($it)")
return MangaLoadResult.Error
}
}
val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extension = MangaExtension.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
hasReadme = hasReadme,
hasChangelog = hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature,
icon = context.getApplicationIcon(pkgName),
)
return MangaLoadResult.Success(extension)
}
/**
* Returns true if the given package is an extension.
*
* @param pkgInfo The package info of the application.
*/
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
}
/**
* Returns the signature hash of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
*/
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
}
}

View file

@ -1,11 +1,17 @@
package eu.kanade.tachiyomi.extension.manga.util
package eu.kanade.tachiyomi.extension.util
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.themes.ThemeManager
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
@ -16,18 +22,24 @@ import kotlin.time.Duration.Companion.seconds
* Activity used to install extensions, because we can only receive the result of the installation
* with [startActivityForResult], which we need to update the UI.
*/
class MangaExtensionInstallActivity : Activity() {
class ExtensionInstallActivity : AppCompatActivity() {
// MIUI package installer bug workaround
private var ignoreUntil = 0L
private var ignoreResult = false
private var hasIgnoredResult = false
private var type: MediaType? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
if (intent.hasExtra(ExtensionInstaller.EXTRA_EXTENSION_TYPE))
type = intent.getSerializableExtraCompat<MediaType>(ExtensionInstaller.EXTRA_EXTENSION_TYPE)
@Suppress("DEPRECATION")
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type)
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
@ -38,8 +50,19 @@ class MangaExtensionInstallActivity : Activity() {
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
}
val onInstallResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
if (ignoreResult && System.nanoTime() < ignoreUntil) {
hasIgnoredResult = true
return@registerForActivityResult
}
checkInstallationResult(result.resultCode)
finish()
}
try {
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
onInstallResult.launch(installIntent)
} catch (error: Exception) {
// Either install package can't be found (probably bots) or there's a security exception
// with the download manager. Nothing we can workaround.
@ -47,17 +70,6 @@ class MangaExtensionInstallActivity : Activity() {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (ignoreResult && System.nanoTime() < ignoreUntil) {
hasIgnoredResult = true
return
}
if (requestCode == INSTALL_REQUEST_CODE) {
checkInstallationResult(resultCode)
}
finish()
}
override fun onStart() {
super.onStart()
if (hasIgnoredResult) {
@ -67,15 +79,23 @@ class MangaExtensionInstallActivity : Activity() {
}
private fun checkInstallationResult(resultCode: Int) {
val downloadId = intent.extras!!.getLong(MangaExtensionInstaller.EXTRA_DOWNLOAD_ID)
val extensionManager = Injekt.get<MangaExtensionManager>()
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
val newStep = when (resultCode) {
RESULT_OK -> InstallStep.Installed
RESULT_CANCELED -> InstallStep.Idle
else -> InstallStep.Error
}
extensionManager.updateInstallStep(downloadId, newStep)
when (type) {
MediaType.ANIME -> {
Injekt.get<AnimeExtensionManager>().updateInstallStep(downloadId, newStep)
}
MediaType.MANGA -> {
Injekt.get<MangaExtensionManager>().updateInstallStep(downloadId, newStep)
}
MediaType.NOVEL -> {
Injekt.get<NovelExtensionManager>().updateInstallStep(downloadId, newStep)
}
null -> { }
}
}
}
private const val INSTALL_REQUEST_CODE = 500

View file

@ -0,0 +1,205 @@
package eu.kanade.tachiyomi.extension.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.content.ContextCompat
import ani.dantotsu.media.MediaType
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import tachiyomi.core.util.lang.launchNow
/**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
* notifies the given [listener] when the package is an extension.
*
* @param listener The listener that should be notified of extension installation events.
*/
internal class ExtensionInstallReceiver : BroadcastReceiver() {
private var animeListener: AnimeListener? = null
private var mangaListener: MangaListener? = null
private var type: MediaType? = null
/**
* Registers this broadcast receiver
*/
fun register(context: Context) {
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
fun setAnimeListener(listener: AnimeListener) : ExtensionInstallReceiver {
this.type = MediaType.ANIME
animeListener = listener
this.animeListener
return this
}
fun setMangaListener(listener: MangaListener) : ExtensionInstallReceiver {
this.type = MediaType.MANGA
mangaListener = listener
return this
}
/**
* Returns the intent filter this receiver should subscribe to.
*/
private val filter
get() = IntentFilter().apply {
priority = 100
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
/**
* Called when one of the events of the [filter] is received. When the package is an extension,
* it's loaded in background and it notifies the [listener] when finished.
*/
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
if (isReplacing(intent)) return
launchNow {
when (type) {
MediaType.ANIME -> {
when (val result = getAnimeExtensionFromIntent(context, intent)) {
is AnimeLoadResult.Success -> animeListener?.onExtensionInstalled(result.extension)
is AnimeLoadResult.Untrusted -> animeListener?.onExtensionUntrusted(result.extension)
else -> {}
}
}
MediaType.MANGA -> {
when (val result = getMangaExtensionFromIntent(context, intent)) {
is MangaLoadResult.Success -> mangaListener?.onExtensionInstalled(result.extension)
is MangaLoadResult.Untrusted -> mangaListener?.onExtensionUntrusted(result.extension)
else -> {}
}
}
else -> { }
}
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
launchNow {
when (type) {
MediaType.ANIME -> {
when (val result = getAnimeExtensionFromIntent(context, intent)) {
is AnimeLoadResult.Success -> animeListener?.onExtensionUpdated(result.extension)
else -> {}
}
}
MediaType.MANGA -> {
when (val result = getMangaExtensionFromIntent(context, intent)) {
is MangaLoadResult.Success -> mangaListener?.onExtensionUpdated(result.extension)
else -> {}
}
}
else -> { }
}
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
if (isReplacing(intent)) return
val pkgName = getPackageNameFromIntent(intent)
if (pkgName != null) {
when (type) {
MediaType.ANIME -> {
animeListener?.onPackageUninstalled(pkgName)
}
MediaType.MANGA -> {
mangaListener?.onPackageUninstalled(pkgName)
}
else -> { }
}
}
}
}
}
/**
* Returns true if this package is performing an update.
*
* @param intent The intent that triggered the event.
*/
private fun isReplacing(intent: Intent): Boolean {
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
}
/**
* Returns the extension triggered by the given intent.
*
* @param context The application context.
* @param intent The intent containing the package name of the extension.
*/
private suspend fun getAnimeExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult {
val pkgName = getPackageNameFromIntent(intent)
if (pkgName == null) {
Logger.log("Package name not found")
return AnimeLoadResult.Error
}
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
ExtensionLoader.loadAnimeExtensionFromPkgName(
context,
pkgName,
)
}.await()
}
private suspend fun getMangaExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
val pkgName = getPackageNameFromIntent(intent)
if (pkgName == null) {
Logger.log("Package name not found")
return MangaLoadResult.Error
}
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
ExtensionLoader.loadMangaExtensionFromPkgName(
context,
pkgName,
)
}.await()
}
/**
* Returns the package name of the installed, updated or removed application.
*/
private fun getPackageNameFromIntent(intent: Intent?): String? {
return intent?.data?.encodedSchemeSpecificPart ?: return null
}
/**
* Listener that receives extension installation events.
*/
interface AnimeListener {
fun onExtensionInstalled(extension: AnimeExtension.Installed)
fun onExtensionUpdated(extension: AnimeExtension.Installed)
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
fun onPackageUninstalled(pkgName: String)
}
interface MangaListener {
fun onExtensionInstalled(extension: MangaExtension.Installed)
fun onExtensionUpdated(extension: MangaExtension.Installed)
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
fun onPackageUninstalled(pkgName: String)
}
}

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.manga.util
package eu.kanade.tachiyomi.extension.util
import android.app.Service
import android.content.Context
@ -8,18 +8,20 @@ import android.net.Uri
import android.os.Build
import android.os.IBinder
import ani.dantotsu.R
import ani.dantotsu.media.MediaType
import ani.dantotsu.util.Logger
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
import eu.kanade.tachiyomi.extension.manga.installer.PackageInstallerInstallerManga
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_EXTENSION_TYPE
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
import eu.kanade.tachiyomi.util.system.notificationBuilder
class MangaExtensionInstallService : Service() {
class ExtensionInstallService : Service() {
private var installer: InstallerManga? = null
private var installer: Installer? = null
override fun onCreate() {
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
@ -43,18 +45,19 @@ class MangaExtensionInstallService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.data
val type = intent?.getSerializableExtraCompat<MediaType>(EXTRA_EXTENSION_TYPE)
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
EXTRA_INSTALLER,
EXTRA_INSTALLER
)
if (uri == null || id == null || installerUsed == null) {
if (uri == null || type == null || id == null || installerUsed == null) {
stopSelf()
return START_NOT_STICKY
}
if (installer == null) {
installer = when (installerUsed) {
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerManga(
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(
this
)
@ -65,7 +68,7 @@ class MangaExtensionInstallService : Service() {
}
}
}
installer!!.addToQueue(id, uri)
installer!!.addToQueue(type, id, uri)
return START_NOT_STICKY
}
@ -81,13 +84,15 @@ class MangaExtensionInstallService : Service() {
fun getIntent(
context: Context,
type: MediaType,
downloadId: Long,
uri: Uri,
installer: BasePreferences.ExtensionInstaller,
): Intent {
return Intent(context, MangaExtensionInstallService::class.java)
.setDataAndType(uri, MangaExtensionInstaller.APK_MIME)
return Intent(context, ExtensionInstallService::class.java)
.setDataAndType(uri, ExtensionInstaller.APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.putExtra(EXTRA_EXTENSION_TYPE, type)
.putExtra(EXTRA_INSTALLER, installer)
}
}

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.manga.util
package eu.kanade.tachiyomi.extension.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
@ -6,15 +6,19 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.novel.NovelExtension
import ani.dantotsu.util.Logger
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.util.storage.getUriCompat
import rx.Observable
@ -29,7 +33,7 @@ import java.util.concurrent.TimeUnit
*
* @param context The application context.
*/
internal class MangaExtensionInstaller(private val context: Context) {
internal class ExtensionInstaller(private val context: Context) {
/**
* The system's download manager
@ -61,7 +65,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
* @param url The url of the apk.
* @param extension The extension to install.
*/
fun downloadAndInstall(url: String, extension: MangaExtension) = Observable.defer {
fun downloadAndInstall(url: String, extension: AnimeExtension): Observable<InstallStep> = Observable.defer {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
@ -81,6 +85,83 @@ internal class MangaExtensionInstaller(private val context: Context) {
Environment.DIRECTORY_DOWNLOADS,
downloadUri.lastPathSegment
)
.setDescription(MediaType.ANIME.asText())
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id }
.map { it.second }
// Poll download status
.mergeWith(pollStatus(id))
// Stop when the application is installed or errors
.takeUntil { it.isCompleted() }
// Always notify on main thread
.observeOn(AndroidSchedulers.mainThread())
// Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) }
}
fun downloadAndInstall(url: String, extension: MangaExtension): Observable<InstallStep> = Observable.defer {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
if (oldDownload != null) {
deleteDownload(pkgName)
}
// Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register()
val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
.setDestinationInExternalFilesDir(
context,
Environment.DIRECTORY_DOWNLOADS,
downloadUri.lastPathSegment
)
.setDescription(MediaType.MANGA.asText())
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id }
.map { it.second }
// Poll download status
.mergeWith(pollStatus(id))
// Stop when the application is installed or errors
.takeUntil { it.isCompleted() }
// Always notify on main thread
.observeOn(AndroidSchedulers.mainThread())
// Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) }
}
fun downloadAndInstall(url: String, extension: NovelExtension) = Observable.defer {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
if (oldDownload != null) {
deleteDownload(pkgName)
}
// Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register()
val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
.setDestinationInExternalFilesDir(
context,
Environment.DIRECTORY_DOWNLOADS,
downloadUri.lastPathSegment
)
.setDescription(MediaType.MANGA.asText())
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request)
@ -134,11 +215,12 @@ internal class MangaExtensionInstaller(private val context: Context) {
*
* @param uri The uri of the extension to install.
*/
fun installApk(downloadId: Long, uri: Uri) {
fun installApk(type: MediaType, downloadId: Long, uri: Uri) {
when (val installer = extensionInstaller.get()) {
BasePreferences.ExtensionInstaller.LEGACY -> {
val intent = Intent(context, MangaExtensionInstallActivity::class.java)
val intent = Intent(context, ExtensionInstallActivity::class.java)
.setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_EXTENSION_TYPE, type)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
@ -147,7 +229,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
else -> {
val intent =
MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)
ExtensionInstallService.getIntent(context, type, downloadId, uri, installer)
ContextCompat.startForegroundService(context, intent)
}
}
@ -159,7 +241,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
fun cancelInstall(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName) ?: return
downloadManager.remove(downloadId)
InstallerManga.cancelInstallQueue(context, downloadId)
Installer.cancelInstallQueue(context, downloadId)
}
/**
@ -168,10 +250,13 @@ internal class MangaExtensionInstaller(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)
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
Intent(Intent.ACTION_DELETE).setData("package:$pkgName".toUri())
else
@Suppress("DEPRECATION")
Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
context.startActivity(intent)
context.startActivity(intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
/**
@ -255,8 +340,11 @@ internal class MangaExtensionInstaller(private val context: Context) {
val localUri = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
).removePrefix(FILE_SCHEME)
val type = MediaType.fromText(cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION),
))
installApk(id, File(localUri).getUriCompat(context))
installApk(type, id, File(localUri).getUriCompat(context))
}
}
}
@ -265,6 +353,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
companion object {
const val APK_MIME = "application/vnd.android.package-archive"
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
const val EXTRA_EXTENSION_TYPE = "ExtensionInstaller.extra.EXTENSION_TYPE"
const val FILE_SCHEME = "file://"
}
}

View file

@ -0,0 +1,427 @@
package eu.kanade.tachiyomi.extension.util
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import ani.dantotsu.media.MediaType
import ani.dantotsu.util.Logger
import dalvik.system.PathClassLoader
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
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.runBlocking
import uy.kohesive.injekt.injectLazy
/**
* 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.
*/
internal object ExtensionLoader {
private val preferences: SourcePreferences by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
private const val ANIME_PACKAGE = "tachiyomi.animeextension"
private const val MANGA_PACKAGE = "tachiyomi.extension"
private const val XX_METADATA_SOURCE_CLASS = ".class"
private const val XX_METADATA_SOURCE_FACTORY = ".factory"
private const val XX_METADATA_NSFW = "n.nsfw"
private const val XX_METADATA_HAS_README = ".hasReadme"
private const val XX_METADATA_HAS_CHANGELOG = ".hasChangelog"
const val ANIME_LIB_VERSION_MIN = 12
const val ANIME_LIB_VERSION_MAX = 15
const val MANGA_LIB_VERSION_MIN = 1.2
const val MANGA_LIB_VERSION_MAX = 1.5
private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or
PackageManager.GET_META_DATA or
@Suppress ("DEPRECATION") PackageManager.GET_SIGNATURES or
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
PackageManager.GET_SIGNING_CERTIFICATES else 0)
// jmir1's key
private const val officialSignatureAnime =
"50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
var trustedSignaturesAnime =
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignatureAnime
// inorichi's key
private const val officialSignatureManga =
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* List of the trusted signatures.
*/
var trustedSignaturesManga =
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignatureManga
/**
* Return a list of all the installed extensions initialized concurrently.
*
* @param context The application context.
*/
fun loadAnimeExtensions(context: Context): List<AnimeLoadResult> {
val pkgManager = context.packageManager
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter { isPackageAnExtension(MediaType.ANIME, it) }
if (extPkgs.isEmpty()) return emptyList()
// Load each extension concurrently and wait for completion
return runBlocking {
val deferred = extPkgs.map {
async { loadAnimeExtension(context, it.packageName, it) }
}
deferred.map { it.await() }
}
}
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
val pkgManager = context.packageManager
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter { isPackageAnExtension(MediaType.MANGA, it) }
if (extPkgs.isEmpty()) return emptyList()
// Load each extension concurrently and wait for completion
return runBlocking {
val deferred = extPkgs.map {
async { loadMangaExtension(context, it.packageName, it) }
}
deferred.map { it.await() }
}
}
/**
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
fun loadAnimeExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
val pkgInfo = try {
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
Logger.log(error)
return AnimeLoadResult.Error
}
if (!isPackageAnExtension(MediaType.ANIME,pkgInfo)) {
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
return AnimeLoadResult.Error
}
return loadAnimeExtension(context, pkgName, pkgInfo)
}
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
val pkgInfo = try {
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
Logger.log(error)
return MangaLoadResult.Error
}
if (!isPackageAnExtension(MediaType.MANGA, pkgInfo)) {
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
return MangaLoadResult.Error
}
return loadMangaExtension(context, pkgName, pkgInfo)
}
/**
* Loads an extension given its package name.
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
private fun loadAnimeExtension(
context: Context,
pkgName: String,
pkgInfo: PackageInfo
): AnimeLoadResult {
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
Logger.log(error)
return AnimeLoadResult.Error
}
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
Logger.log("Missing versionName for extension $extName")
return AnimeLoadResult.Error
}
// Validate lib version
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
if (libVersion == null || libVersion < ANIME_LIB_VERSION_MIN || libVersion > ANIME_LIB_VERSION_MAX) {
Logger.log("Lib version is $libVersion, while only versions " +
"$ANIME_LIB_VERSION_MIN to $ANIME_LIB_VERSION_MAX are allowed"
)
return AnimeLoadResult.Error
}
val signatureHash = getSignatureHash(pkgInfo)
if (signatureHash == null) {
Logger.log("Package $pkgName isn't signed")
return AnimeLoadResult.Error
} else if (signatureHash !in trustedSignaturesAnime) {
val extension = AnimeExtension.Untrusted(
extName,
pkgName,
versionName,
versionCode,
libVersion,
signatureHash
)
Logger.log("Extension $pkgName isn't trusted")
return AnimeLoadResult.Untrusted(extension)
}
val isNsfw = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_NSFW") == 1
if (!loadNsfwSource && isNsfw) {
Logger.log("NSFW extension $pkgName not allowed")
return AnimeLoadResult.Error
}
val hasReadme = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_README", 0) == 1
val hasChangelog = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString("$ANIME_PACKAGE$XX_METADATA_SOURCE_CLASS")!!
.split(";")
.map {
val sourceClass = it.trim()
if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass
} else {
sourceClass
}
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
is AnimeSource -> listOf(obj)
is AnimeSourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Logger.log("Extension load error: $extName ($it)")
return AnimeLoadResult.Error
}
}
val langs = sources.filterIsInstance<AnimeCatalogueSource>()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extension = AnimeExtension.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
hasReadme = hasReadme,
hasChangelog = hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString("$ANIME_PACKAGE$XX_METADATA_SOURCE_FACTORY"),
isUnofficial = signatureHash != officialSignatureAnime,
icon = context.getApplicationIcon(pkgName),
)
return AnimeLoadResult.Success(extension)
}
private fun loadMangaExtension(
context: Context,
pkgName: String,
pkgInfo: PackageInfo
): MangaLoadResult {
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
Logger.log(error)
return MangaLoadResult.Error
}
val extName =
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
Logger.log("Missing versionName for extension $extName")
return MangaLoadResult.Error
}
// Validate lib version
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
if (libVersion == null || libVersion < MANGA_LIB_VERSION_MIN || libVersion > MANGA_LIB_VERSION_MAX) {
Logger.log("Lib version is $libVersion, while only versions " +
"$MANGA_LIB_VERSION_MIN to $MANGA_LIB_VERSION_MAX are allowed"
)
return MangaLoadResult.Error
}
val signatureHash = getSignatureHash(pkgInfo)
/* temporarily disabling signature check TODO: remove?
if (signatureHash == null) {
Logger.log("Package $pkgName isn't signed")
return MangaLoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
Logger.log("Extension $pkgName isn't trusted")
return MangaLoadResult.Untrusted(extension)
}
*/
val isNsfw = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_NSFW") == 1
if (!loadNsfwSource && isNsfw) {
Logger.log("NSFW extension $pkgName not allowed")
return MangaLoadResult.Error
}
val hasReadme = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_README", 0) == 1
val hasChangelog = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString("$MANGA_PACKAGE$XX_METADATA_SOURCE_CLASS")!!
.split(";")
.map {
val sourceClass = it.trim()
if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass
} else {
sourceClass
}
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader)
.getDeclaredConstructor().newInstance()) {
is MangaSource -> listOf(obj)
is SourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Logger.log("Extension load error: $extName ($it)")
return MangaLoadResult.Error
}
}
val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extension = MangaExtension.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
hasReadme = hasReadme,
hasChangelog = hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString("$MANGA_PACKAGE$XX_METADATA_SOURCE_FACTORY"),
isUnofficial = signatureHash != officialSignatureManga,
icon = context.getApplicationIcon(pkgName),
)
return MangaLoadResult.Success(extension)
}
/**
* Returns true if the given package is an extension.
*
* @param pkgInfo The package info of the application.
*/
private fun isPackageAnExtension(type: MediaType, pkgInfo: PackageInfo): Boolean {
return pkgInfo.reqFeatures.orEmpty().any { it.name == when (type) {
MediaType.ANIME -> ANIME_PACKAGE
MediaType.MANGA -> MANGA_PACKAGE
else -> ""
} }
}
/**
* Returns the signature hash of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
*/
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
pkgInfo.signingInfo.signingCertificateHistory
else
@Suppress ("DEPRECATION") pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
}
}

View file

@ -19,8 +19,7 @@ fun GET(
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
val nUrl = url.toHttpUrl()
val g = GET(nUrl, headers, cache)
return g
return GET(nUrl, headers, cache)
}
/**

View file

@ -82,7 +82,7 @@ class AndroidAnimeSourceManager(
}
private suspend fun createStubSource(id: Long): StubAnimeSource {
private fun createStubSource(id: Long): StubAnimeSource {
return StubAnimeSource(AnimeSourceData(id, "", ""))
}
}

View file

@ -80,7 +80,7 @@ class AndroidMangaSourceManager(
}
private suspend fun createStubSource(id: Long): StubMangaSource {
private fun createStubSource(id: Long): StubMangaSource {
return StubMangaSource(MangaSourceData(id, "", ""))
}
}

View file

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.util.system
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.core.content.IntentCompat
import java.io.Serializable
fun Uri.toShareIntent(context: Context, type: String = "image/*", message: String? = null): Intent {
fun Uri.toShareIntent(type: String = "image/*", message: String? = null): Intent {
val uri = this
val shareIntent = Intent(Intent.ACTION_SEND).apply {
@ -36,6 +36,15 @@ inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
return IntentCompat.getParcelableExtra(this, name, T::class.java)
}
inline fun <reified T : Serializable> Bundle.getSerializableCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSerializable(name, T::class.java)
} else {
@Suppress("DEPRECATION")
getSerializable(name) as? T
}
}
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSerializableExtra(name, T::class.java)

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.util.system
import android.content.Context
import androidx.core.os.LocaleListCompat
import java.util.Locale
@ -17,7 +16,7 @@ object LocaleHelper {
/**
* Returns display name of a string language code.
*/
fun getSourceDisplayName(lang: String?, context: Context): String {
fun getSourceDisplayName(lang: String?): String {
return when (lang) {
LAST_USED_KEY -> "Last used"
PINNED_KEY -> "Pinned"