mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 19:11:49 +01:00
Merge branch 'ExtensionsUsingPackageInstaller'
This commit is contained in:
commit
49e0d561b2
@ -175,6 +175,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".extension.util.ExtensionInstallActivity"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
||||
|
||||
<activity
|
||||
|
@ -7,13 +7,13 @@ 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
|
||||
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.ui.extension.ExtensionIntallInfo
|
||||
import eu.kanade.tachiyomi.util.system.launchNow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@ -238,7 +238,7 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension The extension to be installed.
|
||||
*/
|
||||
fun installExtension(extension: Extension.Available): Observable<InstallStep> {
|
||||
fun installExtension(extension: Extension.Available): Observable<ExtensionIntallInfo> {
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||
}
|
||||
|
||||
@ -249,7 +249,7 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension The extension to be updated.
|
||||
*/
|
||||
fun updateExtension(extension: Extension.Installed): Observable<InstallStep> {
|
||||
fun updateExtension(extension: Extension.Installed): Observable<ExtensionIntallInfo> {
|
||||
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
|
||||
?: return Observable.empty()
|
||||
return installExtension(availableExt)
|
||||
@ -265,6 +265,24 @@ class ExtensionManager(
|
||||
installer.setInstallationResult(downloadId, result)
|
||||
}
|
||||
|
||||
/** Sets the result of the installation of an extension.
|
||||
*
|
||||
* @param sessionId The id of the download.
|
||||
* @param result Whether the extension was installed or not.
|
||||
*/
|
||||
fun cancelInstallation(sessionId: Int) {
|
||||
installer.cancelInstallation(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the result of the installation of an extension.
|
||||
*
|
||||
* @param downloadId The id of the download.
|
||||
*/
|
||||
fun setInstalling(downloadId: Long, sessionId: Int) {
|
||||
installer.setInstalling(downloadId, sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls the extension that matches the given package name.
|
||||
*
|
||||
@ -293,13 +311,16 @@ 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)
|
||||
nowTrustedExtensions
|
||||
.map { extension ->
|
||||
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
|
||||
}
|
||||
.map { it.await() }
|
||||
.forEach { result ->
|
||||
if (result is LoadResult.Success) {
|
||||
registerNewExtension(result.extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
enum class InstallStep {
|
||||
Pending, Downloading, Installing, Installed, Error;
|
||||
Pending, Downloading, Loading, Installing, Installed, Error;
|
||||
|
||||
fun isCompleted(): Boolean {
|
||||
return this == Installed || this == Error
|
||||
|
@ -1,12 +1,16 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageInstaller.SessionParams
|
||||
import android.os.Bundle
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Activity used to install extensions, because we can only receive the result of the installation
|
||||
@ -16,37 +20,87 @@ class ExtensionInstallActivity : Activity() {
|
||||
|
||||
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)
|
||||
|
||||
try {
|
||||
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
|
||||
if (PACKAGE_INSTALLED_ACTION == intent.action) {
|
||||
packageInstallStep(intent)
|
||||
return
|
||||
}
|
||||
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val packageInstaller = packageManager.packageInstaller
|
||||
val data = UniFile.fromUri(this, intent.data).openInputStream()
|
||||
|
||||
val params = SessionParams(
|
||||
SessionParams.MODE_FULL_INSTALL
|
||||
)
|
||||
// TODO: Add once compiling via SDK 31
|
||||
// if (Build.VERSION.SDK_INT >= 31) {
|
||||
// params.setRequireUserAction(USER_ACTION_NOT_REQUIRED)
|
||||
// }
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
val session = packageInstaller.openSession(sessionId)
|
||||
session.openWrite("package", 0, -1).use { packageInSession ->
|
||||
data.use { `is` ->
|
||||
val buffer = ByteArray(16384)
|
||||
var n: Int
|
||||
while (`is`.read(buffer).also { n = it } >= 0) {
|
||||
packageInSession.write(buffer, 0, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val newIntent = Intent(this, ExtensionInstallActivity::class.java)
|
||||
.setAction(PACKAGE_INSTALLED_ACTION)
|
||||
.putExtra(ExtensionInstaller.EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.putExtra(EXTRA_SESSION_ID, sessionId)
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(this, downloadId.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val statusReceiver = pendingIntent.intentSender
|
||||
session.commit(statusReceiver)
|
||||
val extensionManager: ExtensionManager by injectLazy()
|
||||
extensionManager.setInstalling(downloadId, sessionId)
|
||||
} 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 (requestCode == INSTALL_REQUEST_CODE) {
|
||||
checkInstallationResult(resultCode)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun checkInstallationResult(resultCode: Int) {
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val success = resultCode == RESULT_OK
|
||||
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
extensionManager.setInstallationResult(downloadId, success)
|
||||
private fun packageInstallStep(intent: Intent) {
|
||||
val extras = intent.extras ?: return
|
||||
if (PACKAGE_INSTALLED_ACTION == intent.action) {
|
||||
val downloadId = extras.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val extensionManager: ExtensionManager by injectLazy()
|
||||
when (val status = extras.getInt(PackageInstaller.EXTRA_STATUS)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmIntent = extras[Intent.EXTRA_INTENT] as? Intent
|
||||
startActivityForResult(confirmIntent, INSTALL_REQUEST_CODE)
|
||||
finish()
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
extensionManager.setInstallationResult(downloadId, true)
|
||||
finish()
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE, PackageInstaller.STATUS_FAILURE_ABORTED, PackageInstaller.STATUS_FAILURE_BLOCKED, PackageInstaller.STATUS_FAILURE_CONFLICT, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, PackageInstaller.STATUS_FAILURE_INVALID, PackageInstaller.STATUS_FAILURE_STORAGE -> {
|
||||
extensionManager.setInstallationResult(downloadId, false)
|
||||
if (status != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
toast(R.string.could_not_install_extension)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
else -> {
|
||||
extensionManager.setInstallationResult(downloadId, false)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val INSTALL_REQUEST_CODE = 500
|
||||
const val EXTRA_SESSION_ID = "ExtensionInstaller.extra.SESSION_ID"
|
||||
const val PACKAGE_INSTALLED_ACTION =
|
||||
"eu.kanade.tachiyomi.SESSION_API_PACKAGE_INSTALLED"
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,14 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.net.toUri
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@ -46,6 +48,15 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
*/
|
||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
||||
|
||||
/** Map of download id to installer session id */
|
||||
val downloadInstallerMap = hashMapOf<Long, Int>()
|
||||
|
||||
data class DownloadSessionInfo(
|
||||
val downloadId: Long,
|
||||
val session: PackageInstaller.Session,
|
||||
val sessionId: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||
* step in the installation process.
|
||||
@ -75,13 +86,19 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
activeDownloads[pkgName] = id
|
||||
|
||||
downloadsRelay.filter { it.first == id }
|
||||
.map { it.second }
|
||||
.map {
|
||||
val sessionId = downloadInstallerMap[it.first] ?: return@map it.second to null
|
||||
val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||
it.second to session
|
||||
}
|
||||
// Poll download status
|
||||
.mergeWith(pollStatus(id))
|
||||
// Poll installation status
|
||||
.mergeWith(pollInstallStatus(id))
|
||||
// Force an error if the download takes more than 3 minutes
|
||||
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
|
||||
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error to null })
|
||||
// Stop when the application is installed or errors
|
||||
.takeUntil { it.isCompleted() }
|
||||
.takeUntil { it.first.isCompleted() }
|
||||
// Always notify on main thread
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
// Always remove the download when unsubscribed
|
||||
@ -94,7 +111,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
*
|
||||
* @param id The id of the download to poll.
|
||||
*/
|
||||
private fun pollStatus(id: Long): Observable<InstallStep> {
|
||||
private fun pollStatus(id: Long): Observable<ExtensionIntallInfo> {
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
|
||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
||||
@ -111,11 +128,24 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
.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()
|
||||
val step = when (status) {
|
||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
||||
else -> return@flatMap Observable.empty()
|
||||
}
|
||||
Observable.just(ExtensionIntallInfo(step, null))
|
||||
}
|
||||
.doOnError {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollInstallStatus(id: Long): Observable<ExtensionIntallInfo> {
|
||||
return Observable.interval(0, 500, TimeUnit.MILLISECONDS)
|
||||
.flatMap {
|
||||
val sessionId = downloadInstallerMap[id] ?: return@flatMap Observable.empty()
|
||||
val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||
Observable.just(InstallStep.Installing to session)
|
||||
}
|
||||
.doOnError {
|
||||
Timber.e(it)
|
||||
@ -149,6 +179,22 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the result of the installation of an extension.
|
||||
*
|
||||
* @param downloadId The id of the download.
|
||||
*/
|
||||
fun setInstalling(downloadId: Long, sessionId: Int) {
|
||||
downloadsRelay.call(downloadId to InstallStep.Installing)
|
||||
downloadInstallerMap[downloadId] = sessionId
|
||||
}
|
||||
|
||||
fun cancelInstallation(sessionId: Int) {
|
||||
val downloadId = downloadInstallerMap.entries.find { it.value == sessionId }?.key ?: return
|
||||
setInstallationResult(downloadId, false)
|
||||
context.packageManager.packageInstaller.abandonSession(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the result of the installation of an extension.
|
||||
*
|
||||
@ -157,6 +203,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
*/
|
||||
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
||||
val step = if (result) InstallStep.Installed else InstallStep.Error
|
||||
downloadInstallerMap.remove(downloadId)
|
||||
downloadsRelay.call(downloadId to step)
|
||||
}
|
||||
|
||||
@ -220,7 +267,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
// Set next installation step
|
||||
if (uri != null) {
|
||||
downloadsRelay.call(id to InstallStep.Installing)
|
||||
downloadsRelay.call(id to InstallStep.Loading)
|
||||
} else {
|
||||
Timber.e("Couldn't locate downloaded APK")
|
||||
downloadsRelay.call(id to InstallStep.Error)
|
||||
|
@ -19,9 +19,10 @@ class ExtensionAdapter(val listener: OnButtonClickListener) :
|
||||
/**
|
||||
* Listener for browse item clicks.
|
||||
*/
|
||||
val buttonClickListener: ExtensionAdapter.OnButtonClickListener = listener
|
||||
val buttonClickListener: OnButtonClickListener = listener
|
||||
|
||||
interface OnButtonClickListener {
|
||||
fun onButtonClick(position: Int)
|
||||
fun onCancelClick(position: Int)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.content.pm.PackageInstaller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@ -29,6 +30,7 @@ import uy.kohesive.injekt.api.get
|
||||
|
||||
typealias ExtensionTuple =
|
||||
Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
||||
typealias ExtensionIntallInfo = Pair<InstallStep, PackageInstaller.SessionInfo?>
|
||||
|
||||
/**
|
||||
* Presenter of [ExtensionBottomSheet].
|
||||
@ -47,7 +49,7 @@ class ExtensionBottomPresenter(
|
||||
var mangaItems = hashMapOf<Long, List<MangaItem>>()
|
||||
private set
|
||||
|
||||
private var currentDownloads = hashMapOf<String, InstallStep>()
|
||||
private var currentDownloads = hashMapOf<String, ExtensionIntallInfo>()
|
||||
|
||||
private val sourceManager: SourceManager = Injekt.get()
|
||||
|
||||
@ -211,12 +213,19 @@ class ExtensionBottomPresenter(
|
||||
fun getAutoCheckPref() = preferences.automaticExtUpdates()
|
||||
|
||||
@Synchronized
|
||||
private fun updateInstallStep(extension: Extension, state: InstallStep): ExtensionItem? {
|
||||
private fun updateInstallStep(
|
||||
extension: Extension,
|
||||
state: InstallStep,
|
||||
session: PackageInstaller.SessionInfo?
|
||||
): ExtensionItem? {
|
||||
val extensions = extensions.toMutableList()
|
||||
val position = extensions.indexOfFirst { it.extension.pkgName == extension.pkgName }
|
||||
|
||||
return if (position != -1) {
|
||||
val item = extensions[position].copy(installStep = state)
|
||||
val item = extensions[position].copy(
|
||||
installStep = state,
|
||||
session = session
|
||||
)
|
||||
extensions[position] = item
|
||||
|
||||
this.extensions = extensions
|
||||
@ -226,6 +235,11 @@ class ExtensionBottomPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelExtensionInstall(extItem: ExtensionItem) {
|
||||
val sessionId = extItem.session?.sessionId ?: return
|
||||
extensionManager.cancelInstallation(sessionId)
|
||||
}
|
||||
|
||||
fun installExtension(extension: Extension.Available) {
|
||||
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
||||
}
|
||||
@ -234,10 +248,10 @@ class ExtensionBottomPresenter(
|
||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
||||
}
|
||||
|
||||
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
|
||||
private fun Observable<ExtensionIntallInfo>.subscribeToInstallUpdate(extension: Extension) {
|
||||
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
||||
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
||||
.map { state -> updateInstallStep(extension, state) }
|
||||
.map { state -> updateInstallStep(extension, state.first, state.second) }
|
||||
.subscribe { item ->
|
||||
if (item != null) {
|
||||
bottomSheet.downloadUpdate(item)
|
||||
|
@ -196,6 +196,11 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelClick(position: Int) {
|
||||
val extension = (extAdapter?.getItem(position) as? ExtensionItem) ?: return
|
||||
presenter.cancelExtensionInstall(extension)
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||
when (binding.tabs.selectedTabPosition) {
|
||||
0 -> {
|
||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.extension
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import coil.clear
|
||||
import coil.load
|
||||
import eu.kanade.tachiyomi.R
|
||||
@ -26,6 +27,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
binding.extButton.setOnClickListener {
|
||||
adapter.buttonClickListener.onButtonClick(flexibleAdapterPosition)
|
||||
}
|
||||
binding.cancelButton.setOnClickListener {
|
||||
adapter.buttonClickListener.onCancelClick(flexibleAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private val shouldLabelNsfw by lazy {
|
||||
@ -45,7 +49,10 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.unofficial)
|
||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.nsfw_short)
|
||||
else -> ""
|
||||
}.toUpperCase(Locale.ROOT)
|
||||
}.uppercase(Locale.ROOT)
|
||||
binding.installProgress.progress = item.sessionProgress ?: 0
|
||||
binding.installProgress.isVisible = item.sessionProgress != null
|
||||
binding.cancelButton.isVisible = item.sessionProgress != null
|
||||
|
||||
binding.sourceImage.clear()
|
||||
|
||||
@ -65,6 +72,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
isClickable = true
|
||||
isActivated = false
|
||||
|
||||
binding.installProgress.progress = item.sessionProgress ?: 0
|
||||
binding.cancelButton.isVisible = item.sessionProgress != null
|
||||
binding.installProgress.isVisible = item.sessionProgress != null
|
||||
val extension = item.extension
|
||||
val installStep = item.installStep
|
||||
strokeColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
@ -73,6 +83,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
||||
when (installStep) {
|
||||
InstallStep.Pending -> R.string.pending
|
||||
InstallStep.Downloading -> R.string.downloading
|
||||
InstallStep.Loading -> R.string.loading
|
||||
InstallStep.Installing -> R.string.installing
|
||||
InstallStep.Installed -> R.string.installed
|
||||
InstallStep.Error -> R.string.retry
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.extension
|
||||
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
@ -19,10 +20,20 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
data class ExtensionItem(
|
||||
val extension: Extension,
|
||||
val header: ExtensionGroupItem? = null,
|
||||
val installStep: InstallStep? = null
|
||||
val installStep: InstallStep? = null,
|
||||
val session: PackageInstaller.SessionInfo? = null
|
||||
) :
|
||||
AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
|
||||
|
||||
constructor(
|
||||
extension: Extension,
|
||||
header: ExtensionGroupItem? = null,
|
||||
installInfo: ExtensionIntallInfo?
|
||||
) : this(extension, header, installInfo?.first, installInfo?.second)
|
||||
|
||||
val sessionProgress: Int?
|
||||
get() = (session?.progress?.times(100)?.toInt())
|
||||
|
||||
/**
|
||||
* Returns the layout resource of this item.
|
||||
*/
|
||||
@ -46,7 +57,7 @@ data class ExtensionItem(
|
||||
position: Int,
|
||||
payloads: MutableList<Any?>?
|
||||
) {
|
||||
if (payloads == null || payloads.isEmpty()) {
|
||||
if (payloads.isNullOrEmpty()) {
|
||||
holder.bind(this)
|
||||
} else {
|
||||
holder.bindButton(this)
|
||||
|
@ -11,6 +11,23 @@
|
||||
android:layout_height="64dp"
|
||||
android:background="@drawable/list_item_selector">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/install_progress"
|
||||
android:layout_width="0dp"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:progressBackgroundTint="?android:attr/colorBackground"
|
||||
android:progressTint="?colorAccent"
|
||||
android:layout_height="2dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:progress="45"
|
||||
android:max="100"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/source_image"
|
||||
android:layout_width="0dp"
|
||||
@ -33,7 +50,7 @@
|
||||
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/lang"
|
||||
app:layout_constraintEnd_toStartOf="@id/ext_button"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_layout"
|
||||
app:layout_constraintStart_toEndOf="@id/source_image"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
@ -78,20 +95,48 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/ext_title"
|
||||
tools:text="Warning" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ext_button"
|
||||
style="@style/Theme.Widget.Button.OutlinedButtonAccent"
|
||||
android:textAllCaps="false"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/button_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintStart_toEndOf="@id/ext_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Details" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ext_button"
|
||||
style="@style/Theme.Widget.Button.OutlinedButtonAccent"
|
||||
android:textAllCaps="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/cancel_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Details" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cancel_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_max="wrap"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/stop"
|
||||
android:padding="12dp"
|
||||
android:tooltipText="@string/stop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toEndOf="@id/ext_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_close_24dp"
|
||||
app:tint="?colorAccent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
@ -304,6 +304,7 @@
|
||||
<string name="may_contain_nsfw">May contain NSFW (18+) content</string>
|
||||
<string name="app_info">App info</string>
|
||||
<string name="_must_be_enabled_first">%1$s must be enabled first</string>
|
||||
<string name="could_not_install_extension">Could not install extension</string>
|
||||
<plurals name="_updates_pending">
|
||||
<item quantity="one">%d update pending</item>
|
||||
<item quantity="other">%d updates pending</item>
|
||||
|
Loading…
Reference in New Issue
Block a user