Initial commit

This commit is contained in:
Finnley Somdahl 2023-10-17 18:42:43 -05:00
commit 21bfbfb139
520 changed files with 47819 additions and 0 deletions

View file

@ -0,0 +1,394 @@
package ani.dantotsu.aniyomi.anime
import android.content.Context
import android.graphics.drawable.Drawable
import ani.dantotsu.aniyomi.domain.source.anime.model.AnimeSourceData
import ani.dantotsu.aniyomi.util.extension.InstallStep
import ani.dantotsu.aniyomi.util.launchNow
import ani.dantotsu.aniyomi.anime.api.AnimeExtensionGithubApi
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstallReceiver
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstaller
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionLoader
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
//import eu.kanade.tachiyomi.util.preference.plusAssign
import ani.dantotsu.aniyomi.util.toast
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import logcat.LogPriority
import rx.Observable
import ani.dantotsu.aniyomi.util.logcat
import ani.dantotsu.aniyomi.util.withUIContext
/**
* The manager of anime extensions installed as another apk which extend the available sources. It handles
* the retrieval of remotely available anime extensions as well as installing, updating and removing them.
* To avoid malicious distribution, every anime extension must be signed and it will only be loaded if its
* signature is trusted, otherwise the user will be prompted with a warning to trust it before being
* loaded.
*
* @param context The application context.
* @param preferences The application preferences.
*/
class AnimeExtensionManager(
private val context: Context,
) {
var isInitialized = false
private set
/**
* API where all the available anime extensions can be found.
*/
private val api = AnimeExtensionGithubApi()
/**
* The installer which installs, updates and uninstalls the anime extensions.
*/
private val installer by lazy { AnimeExtensionInstaller(context) }
private val iconMap = mutableMapOf<String, Drawable>()
private val _installedAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Installed>())
val installedExtensionsFlow = _installedAnimeExtensionsFlow.asStateFlow()
private var subLanguagesEnabledOnFirstRun = false
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedAnimeExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
}
return null
}
private val _availableAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Available>())
val availableExtensionsFlow = _availableAnimeExtensionsFlow.asStateFlow()
private var availableAnimeExtensionsSourcesData: Map<Long, AnimeSourceData> = emptyMap()
private fun setupAvailableAnimeExtensionsSourcesDataMap(animeextensions: List<AnimeExtension.Available>) {
if (animeextensions.isEmpty()) return
availableAnimeExtensionsSourcesData = animeextensions
.flatMap { ext -> ext.sources.map { it.toAnimeSourceData() } }
.associateBy { it.id }
}
fun getSourceData(id: Long) = availableAnimeExtensionsSourcesData[id]
private val _untrustedAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Untrusted>())
val untrustedExtensionsFlow = _untrustedAnimeExtensionsFlow.asStateFlow()
init {
initAnimeExtensions()
AnimeExtensionInstallReceiver(AnimeInstallationListener()).register(context)
}
/**
* Loads and registers the installed animeextensions.
*/
private fun initAnimeExtensions() {
val animeextensions = AnimeExtensionLoader.loadExtensions(context)
logcat { "Loaded ${animeextensions.size} anime extensions" }
for (result in animeextensions) {
when (result) {
is AnimeLoadResult.Success -> {
logcat { "Loaded: ${result.extension.pkgName}" }
for(source in result.extension.sources) {
logcat { "Loaded: ${source.name}" }
}
val sc = result.extension.sources.first()
if (sc is AnimeCatalogueSource) {
//val res = sc.fetchSearchAnime(1, "spy x family", AnimeFilterList()).toBlocking().first()
/*val newScope = CoroutineScope(Dispatchers.IO)
newScope.launch {
println("fetching popular anime")
try {
val res = sc.fetchPopularAnime(1).toBlocking().first()
println("res111: $res")
}
catch (e: Exception) {
println("Exception111: $e")
}
}*/
}
}
else -> {
logcat(LogPriority.ERROR) { "Error loading anime extension: $result" }
}
}
}
_installedAnimeExtensionsFlow.value = animeextensions
.filterIsInstance<AnimeLoadResult.Success>()
.map { it.extension }
_untrustedAnimeExtensionsFlow.value = animeextensions
.filterIsInstance<AnimeLoadResult.Untrusted>()
.map { it.extension }
isInitialized = true
}
/**
* Finds the available anime extensions in the [api] and updates [availableExtensions].
*/
suspend fun findAvailableExtensions() {
val extensions: List<AnimeExtension.Available> = try {
api.findExtensions()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast("Could not update anime extensions") }
emptyList()
}
enableAdditionalSubLanguages(extensions)
_availableAnimeExtensionsFlow.value = extensions
println("AnimeExtensions: $extensions")
updatedInstalledAnimeExtensionsStatuses(extensions)
setupAvailableAnimeExtensionsSourcesDataMap(extensions)
}
/**
* Enables the additional sub-languages in the app first run. This addresses
* the issue where users still need to enable some specific languages even when
* the device language is inside that major group. As an example, if a user
* has a zh device language, the app will also enable zh-Hans and zh-Hant.
*
* If the user have already changed the enabledLanguages preference value once,
* the new languages will not be added to respect the user enabled choices.
*/
private fun enableAdditionalSubLanguages(animeextensions: List<AnimeExtension.Available>) {
if (subLanguagesEnabledOnFirstRun || animeextensions.isEmpty()) {
return
}
// Use the source lang as some aren't present on the animeextension level.
/*val availableLanguages = animeextensions
.flatMap(AnimeExtension.Available::sources)
.distinctBy(AvailableAnimeSources::lang)
.map(AvailableAnimeSources::lang)
val deviceLanguage = Locale.getDefault().language
val defaultLanguages = preferences.enabledLanguages().defaultValue()
val languagesToEnable = availableLanguages.filter {
it != deviceLanguage && it.startsWith(deviceLanguage)
}
preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)*/
subLanguagesEnabledOnFirstRun = true
}
/**
* Sets the update field of the installed animeextensions with the given [availableAnimeExtensions].
*
* @param availableAnimeExtensions The list of animeextensions given by the [api].
*/
private fun updatedInstalledAnimeExtensionsStatuses(availableAnimeExtensions: List<AnimeExtension.Available>) {
if (availableAnimeExtensions.isEmpty()) {
//preferences.animeExtensionUpdatesCount().set(0)
return
}
val mutInstalledAnimeExtensions = _installedAnimeExtensionsFlow.value.toMutableList()
var changed = false
for ((index, installedExt) in mutInstalledAnimeExtensions.withIndex()) {
val pkgName = installedExt.pkgName
val availableExt = availableAnimeExtensions.find { it.pkgName == pkgName }
if (!installedExt.isUnofficial && availableExt == null && !installedExt.isObsolete) {
mutInstalledAnimeExtensions[index] = installedExt.copy(isObsolete = true)
changed = true
} else if (availableExt != null) {
val hasUpdate = installedExt.updateExists(availableExt)
if (installedExt.hasUpdate != hasUpdate) {
mutInstalledAnimeExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
changed = true
}
}
}
if (changed) {
_installedAnimeExtensionsFlow.value = mutInstalledAnimeExtensions
}
updatePendingUpdatesCount()
}
/**
* Returns an observable of the installation process for the given anime extension. It will complete
* once the anime extension is installed or throws an error. The process will be canceled if
* unsubscribed before its completion.
*
* @param extension The anime extension to be installed.
*/
fun installExtension(extension: AnimeExtension.Available): Observable<InstallStep> {
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
}
/**
* Returns an observable of the installation process for the given anime extension. It will complete
* once the anime extension is updated or throws an error. The process will be canceled if
* unsubscribed before its completion.
*
* @param extension The anime extension to be updated.
*/
fun updateExtension(extension: AnimeExtension.Installed): Observable<InstallStep> {
val availableExt = _availableAnimeExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
return installExtension(availableExt)
}
fun cancelInstallUpdateExtension(extension: AnimeExtension) {
installer.cancelInstall(extension.pkgName)
}
/**
* Sets to "installing" status of an anime extension installation.
*
* @param downloadId The id of the download.
*/
fun setInstalling(downloadId: Long) {
installer.updateInstallStep(downloadId, InstallStep.Installing)
}
fun updateInstallStep(downloadId: Long, step: InstallStep) {
installer.updateInstallStep(downloadId, step)
}
/**
* Uninstalls the anime extension that matches the given package name.
*
* @param pkgName The package name of the application to uninstall.
*/
fun uninstallExtension(pkgName: String) {
installer.uninstallApk(pkgName)
}
/**
* Adds the given signature to the list of trusted signatures. It also loads in background the
* anime extensions that match this signature.
*
* @param signature The signature to whitelist.
*/
fun trustSignature(signature: String) {
val untrustedSignatures = _untrustedAnimeExtensionsFlow.value.map { it.signatureHash }.toSet()
if (signature !in untrustedSignatures) return
AnimeExtensionLoader.trustedSignatures += signature
//preferences.trustedSignatures() += signature
val nowTrustedAnimeExtensions = _untrustedAnimeExtensionsFlow.value.filter { it.signatureHash == signature }
_untrustedAnimeExtensionsFlow.value -= nowTrustedAnimeExtensions
val ctx = context
launchNow {
nowTrustedAnimeExtensions
.map { animeextension ->
async { AnimeExtensionLoader.loadExtensionFromPkgName(ctx, animeextension.pkgName) }
}
.map { it.await() }
.forEach { result ->
if (result is AnimeLoadResult.Success) {
registerNewExtension(result.extension)
}
}
}
}
/**
* Registers the given anime extension in this and the source managers.
*
* @param extension The anime extension to be registered.
*/
private fun registerNewExtension(extension: AnimeExtension.Installed) {
_installedAnimeExtensionsFlow.value += extension
}
/**
* Registers the given updated anime extension in this and the source managers previously removing
* the outdated ones.
*
* @param extension The anime extension to be registered.
*/
private fun registerUpdatedExtension(extension: AnimeExtension.Installed) {
val mutInstalledAnimeExtensions = _installedAnimeExtensionsFlow.value.toMutableList()
val oldAnimeExtension = mutInstalledAnimeExtensions.find { it.pkgName == extension.pkgName }
if (oldAnimeExtension != null) {
mutInstalledAnimeExtensions -= oldAnimeExtension
}
mutInstalledAnimeExtensions += extension
_installedAnimeExtensionsFlow.value = mutInstalledAnimeExtensions
}
/**
* Unregisters the animeextension in this and the source managers given its package name. Note this
* method is called for every uninstalled application in the system.
*
* @param pkgName The package name of the uninstalled application.
*/
private fun unregisterAnimeExtension(pkgName: String) {
val installedAnimeExtension = _installedAnimeExtensionsFlow.value.find { it.pkgName == pkgName }
if (installedAnimeExtension != null) {
_installedAnimeExtensionsFlow.value -= installedAnimeExtension
}
val untrustedAnimeExtension = _untrustedAnimeExtensionsFlow.value.find { it.pkgName == pkgName }
if (untrustedAnimeExtension != null) {
_untrustedAnimeExtensionsFlow.value -= untrustedAnimeExtension
}
}
/**
* Listener which receives events of the anime extensions being installed, updated or removed.
*/
private inner class AnimeInstallationListener : AnimeExtensionInstallReceiver.Listener {
override fun onExtensionInstalled(extension: AnimeExtension.Installed) {
registerNewExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUpdated(extension: AnimeExtension.Installed) {
registerUpdatedExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUntrusted(extension: AnimeExtension.Untrusted) {
_untrustedAnimeExtensionsFlow.value += extension
}
override fun onPackageUninstalled(pkgName: String) {
unregisterAnimeExtension(pkgName)
updatePendingUpdatesCount()
}
}
/**
* AnimeExtension method to set the update field of an installed anime extension.
*/
private fun AnimeExtension.Installed.withUpdateCheck(): AnimeExtension.Installed {
return if (updateExists()) {
copy(hasUpdate = true)
} else {
this
}
}
private fun AnimeExtension.Installed.updateExists(availableAnimeExtension: AnimeExtension.Available? = null): Boolean {
val availableExt = availableAnimeExtension ?: _availableAnimeExtensionsFlow.value.find { it.pkgName == pkgName }
if (isUnofficial || availableExt == null) return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
}
private fun updatePendingUpdatesCount() {
//preferences.animeExtensionUpdatesCount().set(_installedAnimeExtensionsFlow.value.count { it.hasUpdate })
}
}

View file

@ -0,0 +1,187 @@
package ani.dantotsu.aniyomi.anime.api
import android.content.Context
import ani.dantotsu.aniyomi.util.extension.ExtensionUpdateNotifier
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
import ani.dantotsu.aniyomi.anime.model.AvailableAnimeSources
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionLoader
import ani.dantotsu.aniyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import logcat.LogPriority
//import ani.dantotsu.aniyomi.core.preference.Preference
//import ani.dantotsu.aniyomi.core.preference.PreferenceStore
import ani.dantotsu.aniyomi.util.withIOContext
import ani.dantotsu.aniyomi.util.logcat
import uy.kohesive.injekt.injectLazy
internal class AnimeExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val json: Json by injectLazy()
//private val lastExtCheck: Preference<Long> by lazy {
// preferenceStore.getLong("last_ext_check", 0)
//}
private val lastExtCheck: Long = 0
private var requiresFallbackSource = false
suspend fun findExtensions(): List<AnimeExtension.Available> {
return withIOContext {
val githubResponse = if (requiresFallbackSource) {
null
} else {
try {
networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
requiresFallbackSource = true
null
}
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
}
val extensions = with(json) {
response
.parseAs<List<AnimeExtensionJsonObject>>()
.toExtensions()
}
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (extensions.size < 10) {
throw Exception()
}
extensions
}
}
suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<AnimeExtension.Installed>? {
// Limit checks to once a day at most
//if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
// return null
//}
val extensions = if (fromAvailableExtensionList) {
animeExtensionManager.availableExtensionsFlow.value
} else {
findExtensions().also { }//lastExtCheck.set(Date().time) }
}
val installedExtensions = AnimeExtensionLoader.loadExtensions(context)
.filterIsInstance<AnimeLoadResult.Success>()
.map { it.extension }
val extensionsWithUpdate = mutableListOf<AnimeExtension.Installed>()
for (installedExt in installedExtensions) {
val pkgName = installedExt.pkgName
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer || hasUpdatedLib)
if (hasUpdate) {
extensionsWithUpdate.add(installedExt)
}
}
if (extensionsWithUpdate.isNotEmpty()) {
ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name })
}
return extensionsWithUpdate
}
private fun List<AnimeExtensionJsonObject>.toExtensions(): List<AnimeExtension.Available> {
return this
.filter {
val libVersion = it.extractLibVersion()
libVersion >= AnimeExtensionLoader.LIB_VERSION_MIN && libVersion <= AnimeExtensionLoader.LIB_VERSION_MAX
}
.map {
AnimeExtension.Available(
name = it.name.substringAfter("Aniyomi: "),
pkgName = it.pkg,
versionName = it.version,
versionCode = it.code,
libVersion = it.extractLibVersion(),
lang = it.lang,
isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toAnimeExtensionSources().orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
)
}
}
private fun List<AnimeExtensionSourceJsonObject>.toAnimeExtensionSources(): List<AvailableAnimeSources> {
return this.map {
AvailableAnimeSources(
id = it.id,
lang = it.lang,
name = it.name,
baseUrl = it.baseUrl,
)
}
}
fun getApkUrl(extension: AnimeExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
}
}
}
private fun AnimeExtensionJsonObject.extractLibVersion(): Double {
return version.substringBeforeLast('.').toDouble()
}
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/aniyomiorg/aniyomi-extensions/repo/"
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/aniyomiorg/aniyomi-extensions@repo/"
@Serializable
private data class AnimeExtensionJsonObject(
val name: String,
val pkg: String,
val apk: String,
val lang: String,
val code: Long,
val version: String,
val nsfw: Int,
val hasReadme: Int = 0,
val hasChangelog: Int = 0,
val sources: List<AnimeExtensionSourceJsonObject>?,
)
@Serializable
private data class AnimeExtensionSourceJsonObject(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
)

View file

@ -0,0 +1,30 @@
package ani.dantotsu.aniyomi.anime.custom
/*
import android.app.Application
import ani.dantotsu.aniyomi.data.Notifications
import ani.dantotsu.aniyomi.util.logcat
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import uy.kohesive.injekt.Injekt
class App : Application() {
override fun onCreate() {
super<Application>.onCreate()
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
setupNotificationChannels()
if (!LogcatLogger.isInstalled) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}
}
private fun setupNotificationChannels() {
try {
Notifications.createChannels(this)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
}
}
}*/

View file

@ -0,0 +1,48 @@
package ani.dantotsu.aniyomi.anime.custom
import android.app.Application
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
import ani.dantotsu.aniyomi.core.preference.PreferenceStore
import ani.dantotsu.aniyomi.domain.base.BasePreferences
import ani.dantotsu.aniyomi.domain.source.service.SourcePreferences
import ani.dantotsu.aniyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory {
Json {
ignoreUnknownKeys = true
explicitNulls = false
}
}
}
}
class PreferenceModule(val application: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory<PreferenceStore> {
AndroidPreferenceStore(application)
}
addSingletonFactory {
SourcePreferences(get())
}
addSingletonFactory {
BasePreferences(application, get())
}
}
}

View file

@ -0,0 +1,170 @@
package ani.dantotsu.aniyomi.anime.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 ani.dantotsu.aniyomi.util.extension.InstallStep
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
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 InstallerAnime(private val service: Service) {
private val extensionManager: AnimeExtensionManager 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 [ExtensionManager]
* @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 [ExtensionManager].
*
* @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 [ExtensionManager]
*/
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 [ExtensionManager]
* @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 = "InstallerAnime.action.CANCEL_QUEUE"
private const val EXTRA_DOWNLOAD_ID = "InstallerAnime.extra.DOWNLOAD_ID"
/**
* Attempts to cancel the installation entry for the provided download ID.
*
* @param downloadId Download ID as known by [ExtensionManager]
*/
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

@ -0,0 +1,107 @@
package ani.dantotsu.aniyomi.anime.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 ani.dantotsu.aniyomi.util.extension.InstallStep
import ani.dantotsu.aniyomi.util.lang.use
import ani.dantotsu.aniyomi.util.system.getParcelableExtraCompat
import ani.dantotsu.aniyomi.util.system.getUriSize
import logcat.LogPriority
import ani.dantotsu.aniyomi.util.logcat
class PackageInstallerInstallerAnime(private val service: Service) : InstallerAnime(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) {
logcat(LogPriority.ERROR) { "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.S) PendingIntent.FLAG_MUTABLE else 0,
).intentSender
session.commit(intentSender)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "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 {
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
}
}
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"

View file

@ -0,0 +1,79 @@
package ani.dantotsu.aniyomi.anime.model
import android.graphics.drawable.Drawable
import ani.dantotsu.aniyomi.animesource.AnimeSource
import ani.dantotsu.aniyomi.domain.source.anime.model.AnimeSourceData
sealed class AnimeExtension {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Long
abstract val libVersion: Double
abstract val lang: String?
abstract val isNsfw: Boolean
abstract val hasReadme: Boolean
abstract val hasChangelog: Boolean
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val pkgFactory: String?,
val sources: List<AnimeSource>,
val icon: Drawable?,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
) : AnimeExtension()
data class Available(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val sources: List<AvailableAnimeSources>,
val apkName: String,
val iconUrl: String,
) : AnimeExtension()
data class Untrusted(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false,
override val hasReadme: Boolean = false,
override val hasChangelog: Boolean = false,
) : AnimeExtension()
}
data class AvailableAnimeSources(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
) {
fun toAnimeSourceData(): AnimeSourceData {
return AnimeSourceData(
id = this.id,
lang = this.lang,
name = this.name,
)
}
}

View file

@ -0,0 +1,7 @@
package ani.dantotsu.aniyomi.anime.model
sealed class AnimeLoadResult {
class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult()
class Untrusted(val extension: AnimeExtension.Untrusted) : AnimeLoadResult()
object Error : AnimeLoadResult()
}

View file

@ -0,0 +1,78 @@
package ani.dantotsu.aniyomi.anime.util
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import ani.dantotsu.aniyomi.util.extension.InstallStep
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager
import ani.dantotsu.aniyomi.util.system.hasMiuiPackageInstaller
import ani.dantotsu.aniyomi.util.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)
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

@ -0,0 +1,130 @@
package ani.dantotsu.aniyomi.anime.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import logcat.LogPriority
import ani.dantotsu.aniyomi.util.launchNow
import ani.dantotsu.aniyomi.util.logcat
/**
* 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) {
context.registerReceiver(this, filter)
}
/**
* 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) {
logcat(LogPriority.WARN) { "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

@ -0,0 +1,82 @@
package ani.dantotsu.aniyomi.anime.util
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import ani.dantotsu.R
import ani.dantotsu.aniyomi.domain.base.BasePreferences
import ani.dantotsu.aniyomi.data.Notifications
import ani.dantotsu.aniyomi.anime.installer.InstallerAnime
import ani.dantotsu.aniyomi.anime.installer.PackageInstallerInstallerAnime
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import ani.dantotsu.aniyomi.util.system.getSerializableExtraCompat
import ani.dantotsu.aniyomi.util.system.notificationBuilder
import logcat.LogPriority
import ani.dantotsu.aniyomi.util.logcat
class AnimeExtensionInstallService : Service() {
private var installer: InstallerAnime? = null
override fun onCreate() {
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
setSmallIcon(R.drawable.spinner_icon)
setAutoCancel(false)
setOngoing(true)
setShowWhen(false)
setContentTitle("Installing Anime Extension...")
setProgress(100, 100, true)
}.build()
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 -> {
logcat(LogPriority.ERROR) { "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

@ -0,0 +1,266 @@
package ani.dantotsu.aniyomi.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 com.jakewharton.rxrelay.PublishRelay
import ani.dantotsu.aniyomi.util.extension.InstallStep
import ani.dantotsu.aniyomi.anime.installer.InstallerAnime
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.domain.base.BasePreferences
import ani.dantotsu.aniyomi.util.storage.getUriCompat
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import ani.dantotsu.aniyomi.util.logcat
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 ->
cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
}
// 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)
context.registerReceiver(this, filter)
}
/**
* 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) {
logcat(LogPriority.ERROR) { "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

@ -0,0 +1,232 @@
package ani.dantotsu.aniyomi.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 dalvik.system.PathClassLoader
import ani.dantotsu.aniyomi.domain.source.service.SourcePreferences
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
import ani.dantotsu.aniyomi.animesource.AnimeSource
import ani.dantotsu.aniyomi.animesource.AnimeSourceFactory
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult
import ani.dantotsu.aniyomi.util.lang.Hash
import ani.dantotsu.aniyomi.util.system.getApplicationIcon
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import ani.dantotsu.aniyomi.util.logcat
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
logcat(LogPriority.ERROR, error)
return AnimeLoadResult.Error
}
if (!isPackageAnExtension(pkgInfo)) {
logcat(LogPriority.WARN) { "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
logcat(LogPriority.ERROR, error)
return AnimeLoadResult.Error
}
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "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) {
logcat(LogPriority.WARN) {
"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) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return AnimeLoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = AnimeExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
logcat(LogPriority.WARN, message = { "Extension $pkgName isn't trusted" })
return AnimeLoadResult.Untrusted(extension)
}
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (!loadNsfwSource && isNsfw) {
logcat(LogPriority.WARN) { "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) {
logcat(LogPriority.ERROR, e) { "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
}
}
}