mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 05:29:18 +01:00
Merge branch 'ExtensionsUsingPackageInstaller'
This commit is contained in:
commit
49e0d561b2
@ -175,6 +175,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
@ -7,13 +7,13 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
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.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||||
import eu.kanade.tachiyomi.util.system.launchNow
|
import eu.kanade.tachiyomi.util.system.launchNow
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@ -238,7 +238,7 @@ class ExtensionManager(
|
|||||||
*
|
*
|
||||||
* @param extension The extension to be installed.
|
* @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)
|
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,7 +249,7 @@ class ExtensionManager(
|
|||||||
*
|
*
|
||||||
* @param extension The extension to be updated.
|
* @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 }
|
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
|
||||||
?: return Observable.empty()
|
?: return Observable.empty()
|
||||||
return installExtension(availableExt)
|
return installExtension(availableExt)
|
||||||
@ -265,6 +265,24 @@ class ExtensionManager(
|
|||||||
installer.setInstallationResult(downloadId, result)
|
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.
|
* Uninstalls the extension that matches the given package name.
|
||||||
*
|
*
|
||||||
@ -293,13 +311,16 @@ class ExtensionManager(
|
|||||||
|
|
||||||
val ctx = context
|
val ctx = context
|
||||||
launchNow {
|
launchNow {
|
||||||
nowTrustedExtensions.map { extension ->
|
nowTrustedExtensions
|
||||||
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
|
.map { extension ->
|
||||||
}.map { it.await() }.forEach { result ->
|
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
|
||||||
if (result is LoadResult.Success) {
|
}
|
||||||
registerNewExtension(result.extension)
|
.map { it.await() }
|
||||||
|
.forEach { result ->
|
||||||
|
if (result is LoadResult.Success) {
|
||||||
|
registerNewExtension(result.extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
package eu.kanade.tachiyomi.extension.model
|
||||||
|
|
||||||
enum class InstallStep {
|
enum class InstallStep {
|
||||||
Pending, Downloading, Installing, Installed, Error;
|
Pending, Downloading, Loading, Installing, Installed, Error;
|
||||||
|
|
||||||
fun isCompleted(): Boolean {
|
fun isCompleted(): Boolean {
|
||||||
return this == Installed || this == Error
|
return this == Installed || this == Error
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.content.pm.PackageInstaller.SessionParams
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.injectLazy
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity used to install extensions, because we can only receive the result of the installation
|
* 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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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 {
|
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) {
|
} catch (error: Exception) {
|
||||||
// Either install package can't be found (probably bots) or there's a security exception
|
// Either install package can't be found (probably bots) or there's a security exception
|
||||||
// with the download manager. Nothing we can workaround.
|
// with the download manager. Nothing we can workaround.
|
||||||
toast(error.message)
|
toast(error.message)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (requestCode == INSTALL_REQUEST_CODE) {
|
|
||||||
checkInstallationResult(resultCode)
|
|
||||||
}
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkInstallationResult(resultCode: Int) {
|
private fun packageInstallStep(intent: Intent) {
|
||||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
val extras = intent.extras ?: return
|
||||||
val success = resultCode == RESULT_OK
|
if (PACKAGE_INSTALLED_ACTION == intent.action) {
|
||||||
|
val downloadId = extras.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||||
val extensionManager = Injekt.get<ExtensionManager>()
|
val extensionManager: ExtensionManager by injectLazy()
|
||||||
extensionManager.setInstallationResult(downloadId, success)
|
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 {
|
private companion object {
|
||||||
const val INSTALL_REQUEST_CODE = 500
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
|
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -46,6 +48,15 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
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
|
* Adds the given extension to the downloads queue and returns an observable containing its
|
||||||
* step in the installation process.
|
* step in the installation process.
|
||||||
@ -75,13 +86,19 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
activeDownloads[pkgName] = id
|
activeDownloads[pkgName] = id
|
||||||
|
|
||||||
downloadsRelay.filter { it.first == 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
|
// Poll download status
|
||||||
.mergeWith(pollStatus(id))
|
.mergeWith(pollStatus(id))
|
||||||
|
// Poll installation status
|
||||||
|
.mergeWith(pollInstallStatus(id))
|
||||||
// Force an error if the download takes more than 3 minutes
|
// 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
|
// Stop when the application is installed or errors
|
||||||
.takeUntil { it.isCompleted() }
|
.takeUntil { it.first.isCompleted() }
|
||||||
// Always notify on main thread
|
// Always notify on main thread
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Always remove the download when unsubscribed
|
// 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.
|
* @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)
|
val query = DownloadManager.Query().setFilterById(id)
|
||||||
|
|
||||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
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 }
|
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
|
||||||
// Map to our model
|
// Map to our model
|
||||||
.flatMap { status ->
|
.flatMap { status ->
|
||||||
when (status) {
|
val step = when (status) {
|
||||||
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
|
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
||||||
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
|
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
||||||
else -> Observable.empty()
|
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 {
|
.doOnError {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
@ -149,6 +179,22 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
context.startActivity(intent)
|
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.
|
* 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) {
|
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
||||||
val step = if (result) InstallStep.Installed else InstallStep.Error
|
val step = if (result) InstallStep.Installed else InstallStep.Error
|
||||||
|
downloadInstallerMap.remove(downloadId)
|
||||||
downloadsRelay.call(downloadId to step)
|
downloadsRelay.call(downloadId to step)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +267,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
|
|
||||||
// Set next installation step
|
// Set next installation step
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
downloadsRelay.call(id to InstallStep.Installing)
|
downloadsRelay.call(id to InstallStep.Loading)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Couldn't locate downloaded APK")
|
Timber.e("Couldn't locate downloaded APK")
|
||||||
downloadsRelay.call(id to InstallStep.Error)
|
downloadsRelay.call(id to InstallStep.Error)
|
||||||
|
@ -19,9 +19,10 @@ class ExtensionAdapter(val listener: OnButtonClickListener) :
|
|||||||
/**
|
/**
|
||||||
* Listener for browse item clicks.
|
* Listener for browse item clicks.
|
||||||
*/
|
*/
|
||||||
val buttonClickListener: ExtensionAdapter.OnButtonClickListener = listener
|
val buttonClickListener: OnButtonClickListener = listener
|
||||||
|
|
||||||
interface OnButtonClickListener {
|
interface OnButtonClickListener {
|
||||||
fun onButtonClick(position: Int)
|
fun onButtonClick(position: Int)
|
||||||
|
fun onCancelClick(position: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.extension
|
package eu.kanade.tachiyomi.ui.extension
|
||||||
|
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -29,6 +30,7 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
typealias ExtensionTuple =
|
typealias ExtensionTuple =
|
||||||
Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
||||||
|
typealias ExtensionIntallInfo = Pair<InstallStep, PackageInstaller.SessionInfo?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [ExtensionBottomSheet].
|
* Presenter of [ExtensionBottomSheet].
|
||||||
@ -47,7 +49,7 @@ class ExtensionBottomPresenter(
|
|||||||
var mangaItems = hashMapOf<Long, List<MangaItem>>()
|
var mangaItems = hashMapOf<Long, List<MangaItem>>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var currentDownloads = hashMapOf<String, InstallStep>()
|
private var currentDownloads = hashMapOf<String, ExtensionIntallInfo>()
|
||||||
|
|
||||||
private val sourceManager: SourceManager = Injekt.get()
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
|
|
||||||
@ -211,12 +213,19 @@ class ExtensionBottomPresenter(
|
|||||||
fun getAutoCheckPref() = preferences.automaticExtUpdates()
|
fun getAutoCheckPref() = preferences.automaticExtUpdates()
|
||||||
|
|
||||||
@Synchronized
|
@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 extensions = extensions.toMutableList()
|
||||||
val position = extensions.indexOfFirst { it.extension.pkgName == extension.pkgName }
|
val position = extensions.indexOfFirst { it.extension.pkgName == extension.pkgName }
|
||||||
|
|
||||||
return if (position != -1) {
|
return if (position != -1) {
|
||||||
val item = extensions[position].copy(installStep = state)
|
val item = extensions[position].copy(
|
||||||
|
installStep = state,
|
||||||
|
session = session
|
||||||
|
)
|
||||||
extensions[position] = item
|
extensions[position] = item
|
||||||
|
|
||||||
this.extensions = extensions
|
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) {
|
fun installExtension(extension: Extension.Available) {
|
||||||
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
||||||
}
|
}
|
||||||
@ -234,10 +248,10 @@ class ExtensionBottomPresenter(
|
|||||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
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 }
|
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
||||||
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
||||||
.map { state -> updateInstallStep(extension, state) }
|
.map { state -> updateInstallStep(extension, state.first, state.second) }
|
||||||
.subscribe { item ->
|
.subscribe { item ->
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
bottomSheet.downloadUpdate(item)
|
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 {
|
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||||
when (binding.tabs.selectedTabPosition) {
|
when (binding.tabs.selectedTabPosition) {
|
||||||
0 -> {
|
0 -> {
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.extension
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import coil.clear
|
import coil.clear
|
||||||
import coil.load
|
import coil.load
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -26,6 +27,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
binding.extButton.setOnClickListener {
|
binding.extButton.setOnClickListener {
|
||||||
adapter.buttonClickListener.onButtonClick(flexibleAdapterPosition)
|
adapter.buttonClickListener.onButtonClick(flexibleAdapterPosition)
|
||||||
}
|
}
|
||||||
|
binding.cancelButton.setOnClickListener {
|
||||||
|
adapter.buttonClickListener.onCancelClick(flexibleAdapterPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val shouldLabelNsfw by lazy {
|
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 is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.unofficial)
|
||||||
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.nsfw_short)
|
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.nsfw_short)
|
||||||
else -> ""
|
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()
|
binding.sourceImage.clear()
|
||||||
|
|
||||||
@ -65,6 +72,9 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
isClickable = true
|
isClickable = true
|
||||||
isActivated = false
|
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 extension = item.extension
|
||||||
val installStep = item.installStep
|
val installStep = item.installStep
|
||||||
strokeColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
strokeColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||||
@ -73,6 +83,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
|
|||||||
when (installStep) {
|
when (installStep) {
|
||||||
InstallStep.Pending -> R.string.pending
|
InstallStep.Pending -> R.string.pending
|
||||||
InstallStep.Downloading -> R.string.downloading
|
InstallStep.Downloading -> R.string.downloading
|
||||||
|
InstallStep.Loading -> R.string.loading
|
||||||
InstallStep.Installing -> R.string.installing
|
InstallStep.Installing -> R.string.installing
|
||||||
InstallStep.Installed -> R.string.installed
|
InstallStep.Installed -> R.string.installed
|
||||||
InstallStep.Error -> R.string.retry
|
InstallStep.Error -> R.string.retry
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.ui.extension
|
package eu.kanade.tachiyomi.ui.extension
|
||||||
|
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@ -19,10 +20,20 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||||||
data class ExtensionItem(
|
data class ExtensionItem(
|
||||||
val extension: Extension,
|
val extension: Extension,
|
||||||
val header: ExtensionGroupItem? = null,
|
val header: ExtensionGroupItem? = null,
|
||||||
val installStep: InstallStep? = null
|
val installStep: InstallStep? = null,
|
||||||
|
val session: PackageInstaller.SessionInfo? = null
|
||||||
) :
|
) :
|
||||||
AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
|
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.
|
* Returns the layout resource of this item.
|
||||||
*/
|
*/
|
||||||
@ -46,7 +57,7 @@ data class ExtensionItem(
|
|||||||
position: Int,
|
position: Int,
|
||||||
payloads: MutableList<Any?>?
|
payloads: MutableList<Any?>?
|
||||||
) {
|
) {
|
||||||
if (payloads == null || payloads.isEmpty()) {
|
if (payloads.isNullOrEmpty()) {
|
||||||
holder.bind(this)
|
holder.bind(this)
|
||||||
} else {
|
} else {
|
||||||
holder.bindButton(this)
|
holder.bindButton(this)
|
||||||
|
@ -11,6 +11,23 @@
|
|||||||
android:layout_height="64dp"
|
android:layout_height="64dp"
|
||||||
android:background="@drawable/list_item_selector">
|
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
|
<ImageView
|
||||||
android:id="@+id/source_image"
|
android:id="@+id/source_image"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -33,7 +50,7 @@
|
|||||||
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/lang"
|
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_constraintStart_toEndOf="@id/source_image"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
@ -78,20 +95,48 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@+id/ext_title"
|
app:layout_constraintTop_toBottomOf="@+id/ext_title"
|
||||||
tools:text="Warning" />
|
tools:text="Warning" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/ext_button"
|
android:id="@+id/button_layout"
|
||||||
style="@style/Theme.Widget.Button.OutlinedButtonAccent"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
app:layout_constraintStart_toEndOf="@id/ext_title"
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
@ -304,6 +304,7 @@
|
|||||||
<string name="may_contain_nsfw">May contain NSFW (18+) content</string>
|
<string name="may_contain_nsfw">May contain NSFW (18+) content</string>
|
||||||
<string name="app_info">App info</string>
|
<string name="app_info">App info</string>
|
||||||
<string name="_must_be_enabled_first">%1$s must be enabled first</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">
|
<plurals name="_updates_pending">
|
||||||
<item quantity="one">%d update pending</item>
|
<item quantity="one">%d update pending</item>
|
||||||
<item quantity="other">%d updates pending</item>
|
<item quantity="other">%d updates pending</item>
|
||||||
|
Loading…
Reference in New Issue
Block a user