merge double upstream

This commit is contained in:
Rani Sargees
2020-01-06 11:43:11 -05:00
committed by Jobobby04
900 changed files with 31103 additions and 22299 deletions

View File

@@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.extension
import android.content.Context
import com.elvishew.xlog.XLog
import android.graphics.drawable.Drawable
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
@@ -12,13 +11,13 @@ import eu.kanade.tachiyomi.extension.model.LoadResult
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.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.launchNow
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast
import exh.source.BlacklistedSources
import kotlinx.coroutines.async
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -33,8 +32,8 @@ import uy.kohesive.injekt.api.get
* @param preferences The application preferences.
*/
class ExtensionManager(
private val context: Context,
private val preferences: PreferencesHelper = Injekt.get()
private val context: Context,
private val preferences: PreferencesHelper = Injekt.get()
) {
/**
@@ -52,6 +51,8 @@ class ExtensionManager(
*/
private val installedExtensionsRelay = BehaviorRelay.create<List<Extension.Installed>>()
private val iconMap = mutableMapOf<String, Drawable>()
/**
* List of the currently installed extensions.
*/
@@ -61,6 +62,14 @@ class ExtensionManager(
installedExtensionsRelay.call(value)
}
fun getAppIconForSource(source: Source): Drawable? {
val pkgName = installedExtensions.find { ext -> ext.sources.any { it.id == source.id } }?.pkgName
if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
}
return null
}
/**
* Relay used to notify the available extensions.
*/
@@ -73,7 +82,7 @@ class ExtensionManager(
private set(value) {
field = value
availableExtensionsRelay.call(value)
setUpdateFieldOfInstalledExtensions(value)
updatedInstalledExtensionsStatuses(value)
}
/**
@@ -111,19 +120,20 @@ class ExtensionManager(
val extensions = ExtensionLoader.loadExtensions(context)
installedExtensions = extensions
.filterIsInstance<LoadResult.Success>()
.map { it.extension }
.filterNotBlacklisted()
.filterIsInstance<LoadResult.Success>()
.map { it.extension }
.filterNotBlacklisted()
installedExtensions
.flatMap { it.sources }
// overwrite is needed until the bundled sources are removed
.forEach { sourceManager.registerSource(it, true) }
.flatMap { it.sources }
// overwrite is needed until the bundled sources are removed
.forEach { sourceManager.registerSource(it, true) }
untrustedExtensions = extensions
.filterIsInstance<LoadResult.Untrusted>()
.map { it.extension }
.filterNotBlacklisted()
.filterIsInstance<LoadResult.Untrusted>()
.map { it.extension }
.filterNotBlacklisted()
}
// EXH -->
fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
val blacklistEnabled = preferences.eh_enableSourceBlacklist().getOrDefault()
@@ -166,11 +176,16 @@ class ExtensionManager(
* Finds the available extensions in the [api] and updates [availableExtensions].
*/
fun findAvailableExtensions() {
api.findExtensions()
.onErrorReturn { emptyList() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { availableExtensions = it.filterNotBlacklisted() }
launchNow {
val extensions: List<Extension.Available> = try {
api.findExtensions()
} catch (e: Exception) {
context.toast(e.message)
emptyList()
}
availableExtensions = extensions.filterNotBlacklisted()
}
}
/**
@@ -178,23 +193,34 @@ class ExtensionManager(
*
* @param availableExtensions The list of extensions given by the [api].
*/
private fun setUpdateFieldOfInstalledExtensions(availableExtensions: List<Extension.Available>) {
private fun updatedInstalledExtensionsStatuses(availableExtensions: List<Extension.Available>) {
if (availableExtensions.isEmpty()) {
preferences.extensionUpdatesCount().set(0)
return
}
val mutInstalledExtensions = installedExtensions.toMutableList()
var changed = false
for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
val pkgName = installedExt.pkgName
val availableExt = availableExtensions.find { it.pkgName == pkgName } ?: continue
val availableExt = availableExtensions.find { it.pkgName == pkgName }
val hasUpdate = availableExt.versionCode > installedExt.versionCode
if (installedExt.hasUpdate != hasUpdate) {
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
if (availableExt == null && !installedExt.isObsolete) {
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
changed = true
} else if (availableExt != null) {
val hasUpdate = availableExt.versionCode > installedExt.versionCode
if (installedExt.hasUpdate != hasUpdate) {
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
changed = true
}
}
}
if (changed) {
installedExtensions = mutInstalledExtensions.filterNotBlacklisted()
}
updatePendingUpdatesCount()
}
/**
@@ -215,9 +241,9 @@ class ExtensionManager(
*
* @param extension The extension to be updated.
*/
fun updateExtension(extension: Extension.Installed): Observable<InstallStep> {
fun updateExtension(extension: Extension.Installed): Observable<InstallStep> {
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
?: return Observable.empty()
return installExtension(availableExt)
}
@@ -252,7 +278,7 @@ class ExtensionManager(
ExtensionLoader.trustedSignatures += signature
val preference = preferences.trustedSignatures()
preference.set(preference.getOrDefault() + signature)
preference.set(preference.get() + signature)
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
untrustedExtensions -= nowTrustedExtensions
@@ -260,15 +286,15 @@ class ExtensionManager(
val ctx = context
launchNow {
nowTrustedExtensions
.map { extension ->
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
}
.map { it.await() }
.forEach { result ->
if (result is LoadResult.Success) {
registerNewExtension(result.extension)
}
.map { extension ->
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
}
.map { it.await() }
.forEach { result ->
if (result is LoadResult.Success) {
registerNewExtension(result.extension)
}
}
}
}
@@ -335,10 +361,12 @@ class ExtensionManager(
override fun onExtensionInstalled(extension: Extension.Installed) {
registerNewExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUpdated(extension: Extension.Installed) {
registerUpdatedExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
@@ -347,6 +375,7 @@ class ExtensionManager(
override fun onPackageUninstalled(pkgName: String) {
unregisterExtension(pkgName)
updatePendingUpdatesCount()
}
}
@@ -361,4 +390,7 @@ class ExtensionManager(
return this
}
private fun updatePendingUpdatesCount() {
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
}
}

View File

@@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.util.system.notification
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.coroutineScope
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = coroutineScope {
val pendingUpdates = try {
ExtensionGithubApi().checkForUpdates(context)
} catch (e: Exception) {
return@coroutineScope Result.failure()
}
if (pendingUpdates.isNotEmpty()) {
createUpdateNotification(pendingUpdates.map { it.name })
}
Result.success()
}
private fun createUpdateNotification(names: List<String>) {
NotificationManagerCompat.from(context).apply {
notify(
Notifications.ID_UPDATES_TO_EXTS,
context.notification(Notifications.CHANNEL_UPDATES_TO_EXTS) {
setContentTitle(
context.resources.getQuantityString(
R.plurals.update_check_notification_ext_updates,
names.size,
names.size
)
)
val extNames = names.joinToString(", ")
setContentText(extNames)
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
setSmallIcon(R.drawable.ic_extension_24dp)
setContentIntent(NotificationReceiver.openExtensionsPendingActivity(context))
setAutoCancel(true)
}
)
}
}
companion object {
private const val TAG = "ExtensionUpdate"
fun setupTask(context: Context, forceAutoUpdateJob: Boolean? = null) {
val preferences = Injekt.get<PreferencesHelper>()
val autoUpdateJob = forceAutoUpdateJob ?: preferences.automaticExtUpdates().get()
if (autoUpdateJob) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
12, TimeUnit.HOURS,
1, TimeUnit.HOURS
)
.addTag(TAG)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
}
}
}

View File

@@ -1,34 +1,67 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import java.util.Date
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi {
private val network: NetworkHelper by injectLazy()
private val client get() = network.client
private val preferences: PreferencesHelper by injectLazy()
private val gson: Gson by injectLazy()
private val repoUrl = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
suspend fun findExtensions(): List<Extension.Available> {
val call = GET(EXT_URL)
fun findExtensions(): Observable<List<Extension.Available>> {
val call = GET("$repoUrl/index.json")
return withContext(Dispatchers.IO) {
val response = network.client.newCall(call).await()
if (response.isSuccessful) {
parseResponse(response)
} else {
response.close()
throw Exception("Failed to get extensions")
}
}
}
return client.newCall(call).asObservableSuccess()
.map(::parseResponse)
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
val extensions = findExtensions()
preferences.lastExtCheck().set(Date().time)
val installedExtensions = ExtensionLoader.loadExtensions(context)
.filterIsInstance<LoadResult.Success>()
.map { it.extension }
val extensionsWithUpdate = mutableListOf<Extension.Installed>()
for (installedExt in installedExtensions) {
val pkgName = installedExt.pkgName
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdate = availableExt.versionCode > installedExt.versionCode
if (hasUpdate) {
extensionsWithUpdate.add(installedExt)
}
}
return extensionsWithUpdate
}
private fun parseResponse(response: Response): List<Extension.Available> {
@@ -36,20 +69,31 @@ internal class ExtensionGithubApi {
val json = gson.fromJson<JsonArray>(text)
return json.map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ")
val pkgName = element["pkg"].string
val apkName = element["apk"].string
val versionName = element["version"].string
val versionCode = element["code"].int
val lang = element["lang"].string
val icon = "$repoUrl/icon/${apkName.replace(".apk", ".png")}"
return json
.filter { element ->
val versionName = element["version"].string
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ")
val pkgName = element["pkg"].string
val apkName = element["apk"].string
val versionName = element["version"].string
val versionCode = element["code"].int
val lang = element["lang"].string
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
}
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
}
}
fun getApkUrl(extension: Extension.Available): String {
return "$repoUrl/apk/${extension.apkName}"
return "$REPO_URL/apk/${extension.apkName}"
}
companion object {
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
private const val EXT_URL = "$REPO_URL/index.json"
}
}

View File

@@ -10,27 +10,33 @@ sealed class Extension {
abstract val versionCode: Int
abstract val lang: String?
data class Installed(override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val sources: List<Source>,
override val lang: String,
val hasUpdate: Boolean = false) : Extension()
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val sources: List<Source>,
override val lang: String,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false
) : Extension()
data class Available(override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
val apkName: String,
val iconUrl: String) : Extension()
data class Untrusted(override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val signatureHash: String,
override val lang: String? = null) : Extension()
data class Available(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
val apkName: String,
val iconUrl: String
) : Extension()
data class Untrusted(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val signatureHash: String,
override val lang: String? = null
) : Extension()
}

View File

@@ -4,7 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -18,9 +18,9 @@ class ExtensionInstallActivity : Activity() {
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)
.setDataAndType(intent.data, intent.type)
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)

View File

@@ -6,8 +6,7 @@ import android.content.Intent
import android.content.IntentFilter
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.util.launchNow
import kotlinx.coroutines.withContext
import eu.kanade.tachiyomi.util.lang.launchNow
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -20,7 +19,7 @@ import kotlinx.coroutines.async
* @param listener The listener that should be notified of extension installation events.
*/
internal class ExtensionInstallReceiver(private val listener: Listener) :
BroadcastReceiver() {
BroadcastReceiver() {
/**
* Registers this broadcast receiver
@@ -32,12 +31,13 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
/**
* 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")
}
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,
@@ -49,8 +49,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
if (!isReplacing(intent)) launchNow {
val result = getExtensionFromIntent(context, intent)
when (result) {
when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
}
@@ -58,11 +57,11 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
}
Intent.ACTION_PACKAGE_REPLACED -> {
launchNow {
val result = getExtensionFromIntent(context, intent)
when (result) {
when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
// Not needed as a package can't be upgraded if the signature is different
is LoadResult.Untrusted -> {}
is LoadResult.Untrusted -> {
}
}
}
}
@@ -93,9 +92,9 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
* @param intent The intent containing the package name of the extension.
*/
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
val pkgName = getPackageNameFromIntent(intent) ?:
return LoadResult.Error("Package name not found")
return withContext(Dispatchers.Default) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }
val pkgName = getPackageNameFromIntent(intent)
?: return LoadResult.Error("Package name not found")
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await()
}
/**
@@ -114,5 +113,4 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
fun onExtensionUntrusted(extension: Extension.Untrusted)
fun onPackageUninstalled(pkgName: String)
}
}

View File

@@ -6,16 +6,16 @@ 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 com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.storage.getUriCompat
import java.io.File
import java.util.concurrent.TimeUnit
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
/**
* The installer which installs, updates and uninstalls the extensions.
@@ -63,26 +63,28 @@ internal class ExtensionInstaller(private val context: Context) {
// Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register()
val request = DownloadManager.Request(Uri.parse(url))
.setTitle(extension.name)
.setMimeType(APK_MIME)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val downloadUri = Uri.parse(url)
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))
// Force an error if the download takes more than 3 minutes
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
// 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) }
.map { it.second }
// Poll download status
.mergeWith(pollStatus(id))
// Force an error if the download takes more than 3 minutes
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
// 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) }
}
/**
@@ -95,25 +97,25 @@ internal class ExtensionInstaller(private val context: Context) {
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.getColumnIndex(DownloadManager.COLUMN_STATUS))
}
// Get the current download status
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndex(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()
}
}
// 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()
}
}
}
/**
@@ -123,9 +125,9 @@ internal class ExtensionInstaller(private val context: Context) {
*/
fun installApk(downloadId: Long, uri: Uri) {
val intent = Intent(context, ExtensionInstallActivity::class.java)
.setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
.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)
}
@@ -138,7 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
fun uninstallApk(pkgName: String) {
val packageUri = Uri.parse("package:$pkgName")
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
@@ -159,7 +161,7 @@ internal class ExtensionInstaller(private val context: Context) {
*
* @param pkgName The package name of the download to delete.
*/
fun deleteDownload(pkgName: String) {
private fun deleteDownload(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName)
if (downloadId != null) {
downloadManager.remove(downloadId)
@@ -221,20 +223,15 @@ internal class ExtensionInstaller(private val context: Context) {
return
}
// Due to a bug in Android versions prior to N, the installer can't open files that do
// not contain the extension in the path, even if you specify the correct MIME.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
val query = DownloadManager.Query().setFilterById(id)
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
@Suppress("DEPRECATION")
val uriCompat = File(cursor.getString(cursor.getColumnIndex(
DownloadManager.COLUMN_LOCAL_FILENAME))).getUriCompat(context)
installApk(id, uriCompat)
}
val query = DownloadManager.Query().setFilterById(id)
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val localUri = cursor.getString(
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
).removePrefix(FILE_SCHEME)
installApk(id, File(localUri).getUriCompat(context))
}
} else {
installApk(id, uri)
}
}
}
@@ -242,6 +239,6 @@ internal class ExtensionInstaller(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 FILE_SCHEME = "file://"
}
}

View File

@@ -6,13 +6,12 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.Hash
import eu.kanade.tachiyomi.util.lang.Hash
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import timber.log.Timber
@@ -27,8 +26,8 @@ internal object ExtensionLoader {
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val LIB_VERSION_MIN = 1
private const val LIB_VERSION_MAX = 1
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
@@ -36,9 +35,9 @@ internal object ExtensionLoader {
* List of the trusted signatures.
*/
var trustedSignatures = mutableSetOf<String>() +
Injekt.get<PreferencesHelper>().trustedSignatures().getOrDefault() +
// inorichi's key
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
Injekt.get<PreferencesHelper>().trustedSignatures().get() +
// inorichi's key
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* Return a list of all the installed extensions initialized concurrently.
@@ -95,16 +94,23 @@ internal object ExtensionLoader {
return LoadResult.Error(error)
}
val extName = pkgManager.getApplicationLabel(appInfo)?.toString()
.orEmpty().substringAfter("Tachiyomi: ")
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
val versionName = pkgInfo.versionName
val versionCode = pkgInfo.versionCode
if (versionName.isNullOrEmpty()) {
val exception = Exception("Missing versionName for extension $extName")
Timber.w(exception)
return LoadResult.Error(exception)
}
// Validate lib version
val majorLibVersion = versionName.substringBefore('.').toInt()
if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
val exception = Exception("Lib version is $majorLibVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
val libVersion = versionName.substringBeforeLast('.').toDouble()
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
val exception = Exception(
"Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
)
Timber.w(exception)
return LoadResult.Error(exception)
}
@@ -122,30 +128,30 @@ internal object ExtensionLoader {
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
.split(";")
.map {
val sourceClass = it.trim()
if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass
} else {
sourceClass
}
.flatMap {
try {
val obj = Class.forName(it, false, classLoader).newInstance()
when (obj) {
is Source -> listOf(obj)
is SourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName.")
return LoadResult.Error(e)
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is Source -> listOf(obj)
is SourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName.")
return LoadResult.Error(e)
}
}
val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang }
.toSet()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
@@ -173,11 +179,10 @@ internal object ExtensionLoader {
*/
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && !signatures.isEmpty()) {
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
}
}