mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 14:31:50 +01:00
Add update all button for extensions update notifications
This commit is contained in:
parent
2ea3142b32
commit
ea0d8224b2
@ -204,6 +204,10 @@
|
||||
android:name=".data.library.LibraryUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".extension.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.download.DownloadService"
|
||||
android:exported="false" />
|
||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||
import eu.kanade.tachiyomi.extension.ExtensionInstallService
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||
@ -71,6 +72,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
)
|
||||
// Cancel library update and dismiss notification
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
||||
ACTION_CANCEL_EXTENSION_UPDATE -> cancelExtensionUpdate(context)
|
||||
ACTION_CANCEL_UPDATE_DOWNLOAD -> cancelDownloadUpdate(context)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context)
|
||||
// Share backup file
|
||||
@ -199,6 +201,17 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
Handler().post { dismissNotification(context, Notifications.ID_LIBRARY_PROGRESS) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to stop a library update
|
||||
*
|
||||
* @param context context of application
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
private fun cancelExtensionUpdate(context: Context) {
|
||||
ExtensionInstallService.stop(context)
|
||||
Handler().post { dismissNotification(context, Notifications.ID_EXTENSION_PROGRESS) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to mark as read
|
||||
*
|
||||
@ -251,6 +264,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
// Called to cancel library update.
|
||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||
|
||||
// Called to cancel extension update.
|
||||
private const val ACTION_CANCEL_EXTENSION_UPDATE = "$ID.$NAME.CANCEL_EXTENSION_UPDATE"
|
||||
|
||||
private const val ACTION_CANCEL_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_UPDATE_DOWNLOAD"
|
||||
|
||||
// Called to mark as read
|
||||
@ -568,6 +584,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which stops the library update
|
||||
*
|
||||
* @param context context of application
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun cancelExtensionUpdatePendingBroadcast(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_EXTENSION_UPDATE
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that cancels the download for a Tachiyomi update
|
||||
*
|
||||
|
@ -47,6 +47,7 @@ object Notifications {
|
||||
*/
|
||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||
const val ID_UPDATES_TO_EXTS = -401
|
||||
const val ID_EXTENSION_PROGRESS = -402
|
||||
|
||||
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
|
||||
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
||||
|
@ -0,0 +1,62 @@
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
|
||||
class ExtensionInstallNotifier(private val context: Context) {
|
||||
|
||||
/**
|
||||
* Bitmap of the app for notifications.
|
||||
*/
|
||||
private val notificationBitmap by lazy {
|
||||
BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pending intent of action that cancels the library update
|
||||
*/
|
||||
private val cancelIntent by lazy {
|
||||
NotificationReceiver.cancelExtensionUpdatePendingBroadcast(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached progress notification to avoid creating a lot.
|
||||
*/
|
||||
val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_UPDATES_TO_EXTS) {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
setLargeIcon(notificationBitmap)
|
||||
setContentTitle(context.getString(R.string.updating_extensions))
|
||||
setProgress(0, 0, true)
|
||||
setOngoing(true)
|
||||
setSilent(true)
|
||||
setOnlyAlertOnce(true)
|
||||
color = ContextCompat.getColor(context, R.color.colorAccent)
|
||||
addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the notification containing the currently updating manga and the progress.
|
||||
*
|
||||
* @param manga the manga that's being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification() {
|
||||
context.notificationManager.notify(
|
||||
Notifications.ID_EXTENSION_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(context.getString(R.string.updating_extensions))
|
||||
.setProgress(0, 0, true)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.ArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ExtensionInstallService(
|
||||
val extensionManager: ExtensionManager = Injekt.get(),
|
||||
) : Service() {
|
||||
|
||||
/**
|
||||
* Wake lock that will be held until the service is destroyed.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var notifier: ExtensionInstallNotifier
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private var serviceScope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
private val requestSemaphore = Semaphore(3)
|
||||
|
||||
/**
|
||||
* This method needs to be implemented, but it's not used/needed.
|
||||
*/
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service receives an intent.
|
||||
*
|
||||
* @param intent the start intent from.
|
||||
* @param flags the flags of the command.
|
||||
* @param startId the start id of this command.
|
||||
* @return the start value of the command.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) return START_NOT_STICKY
|
||||
|
||||
instance = this
|
||||
|
||||
val list = intent.getParcelableArrayListExtra<ExtensionInfo>(KEY_EXTENSION)
|
||||
?: return START_NOT_STICKY
|
||||
job = serviceScope.launch {
|
||||
val results = list.map {
|
||||
async {
|
||||
installExtension(it)
|
||||
}
|
||||
}
|
||||
results.awaitAll()
|
||||
}
|
||||
job?.invokeOnCompletion { stopSelf(startId) }
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
suspend fun installExtension(extension: ExtensionInfo) {
|
||||
requestSemaphore.withPermit {
|
||||
extensionManager.installExtension(extension, serviceScope)
|
||||
.collect {
|
||||
notifier.showProgressNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service is created. It injects dagger dependencies and acquire
|
||||
* the wake lock.
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS)
|
||||
notifier = ExtensionInstallNotifier(this)
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"ExtensionInstallService:WakeLock"
|
||||
)
|
||||
wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
|
||||
startForeground(Notifications.ID_EXTENSION_PROGRESS, notifier.progressNotificationBuilder.build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when the service is destroyed. It cancels jobs and releases the wake lock.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
job?.cancel()
|
||||
serviceScope.cancel()
|
||||
if (instance == this) {
|
||||
instance = null
|
||||
}
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private var instance: ExtensionInstallService? = null
|
||||
|
||||
/**
|
||||
* Stops the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
instance?.serviceScope?.cancel()
|
||||
context.stopService(Intent(context, ExtensionUpdateJob::class.java))
|
||||
}
|
||||
|
||||
/**
|
||||
* Key that defines what should be updated.
|
||||
*/
|
||||
private const val KEY_EXTENSION = "extension"
|
||||
|
||||
fun jobIntent(context: Context, extensions: List<Extension.Available>): Intent {
|
||||
return Intent(context, ExtensionInstallService::class.java).apply {
|
||||
val info = extensions.map(::ExtensionInfo)
|
||||
putParcelableArrayListExtra(KEY_EXTENSION, ArrayList(info))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Parcelable
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
@ -15,11 +16,12 @@ 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.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -49,6 +51,15 @@ class ExtensionManager(
|
||||
*/
|
||||
private val installer by lazy { ExtensionInstaller(context) }
|
||||
|
||||
val downloadRelay
|
||||
get() = installer.downloadsStateFlow
|
||||
|
||||
fun getExtension(downloadId: Long): String? {
|
||||
return installer.activeDownloads.entries.find { downloadId == it.value }?.key
|
||||
}
|
||||
|
||||
fun getActiveInstalls(): Int = installer.activeDownloads.size
|
||||
|
||||
/**
|
||||
* Relay used to notify the installed extensions.
|
||||
*/
|
||||
@ -240,21 +251,8 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension The extension to be installed.
|
||||
*/
|
||||
fun installExtension(extension: Extension.Available): Flow<ExtensionIntallInfo> {
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flow of the installation process for the given extension. It will complete
|
||||
* once the extension is updated or throws an error. The process will be canceled the scope
|
||||
* is canceled before its completion.
|
||||
*
|
||||
* @param extension The extension to be updated.
|
||||
*/
|
||||
fun updateExtension(extension: Extension.Installed): Flow<ExtensionIntallInfo> {
|
||||
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
|
||||
?: return emptyFlow()
|
||||
return installExtension(availableExt)
|
||||
suspend fun installExtension(extension: ExtensionInfo, scope: CoroutineScope): Flow<ExtensionIntallInfo> {
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -407,6 +405,19 @@ class ExtensionManager(
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ExtensionInfo(
|
||||
val apkName: String,
|
||||
val pkgName: String,
|
||||
val name: String,
|
||||
) : Parcelable {
|
||||
constructor(extension: Extension.Available) : this(
|
||||
apkName = extension.apkName,
|
||||
pkgName = extension.pkgName,
|
||||
name = extension.name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface ExtensionsChangedListener {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.extension
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -17,6 +19,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
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.util.system.notification
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -35,15 +38,20 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
}
|
||||
|
||||
if (pendingUpdates.isNotEmpty()) {
|
||||
createUpdateNotification(pendingUpdates.map { it.name })
|
||||
createUpdateNotification(pendingUpdates)
|
||||
}
|
||||
|
||||
Result.success()
|
||||
}
|
||||
|
||||
private fun createUpdateNotification(names: List<String>) {
|
||||
private fun createUpdateNotification(extensions: List<Extension.Available>) {
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
preferences.extensionUpdatesCount().set(names.size)
|
||||
preferences.extensionUpdatesCount().set(extensions.size)
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preferences.autoUpdateExtensions()) {
|
||||
// val intent = ExtensionInstallService.jobIntent(context, extensions)
|
||||
// context.startForegroundService(intent)
|
||||
// return
|
||||
// }
|
||||
NotificationManagerCompat.from(context).apply {
|
||||
notify(
|
||||
Notifications.ID_UPDATES_TO_EXTS,
|
||||
@ -51,11 +59,11 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
setContentTitle(
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.extension_updates_available,
|
||||
names.size,
|
||||
names.size
|
||||
extensions.size,
|
||||
extensions.size
|
||||
)
|
||||
)
|
||||
val extNames = names.joinToString(", ")
|
||||
val extNames = extensions.joinToString(", ") { it.name }
|
||||
setContentText(extNames)
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
|
||||
setSmallIcon(R.drawable.ic_extension_update_24dp)
|
||||
@ -65,6 +73,16 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
||||
context
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val intent = ExtensionInstallService.jobIntent(context, extensions)
|
||||
val pendingIntent =
|
||||
PendingIntent.getForegroundService(context, 0, intent, 0)
|
||||
addAction(
|
||||
R.drawable.ic_file_download_24dp,
|
||||
context.getString(R.string.update_all),
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
setAutoCancel(true)
|
||||
}
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
@ -30,7 +31,7 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
|
||||
suspend fun checkForUpdates(context: Context): List<Extension.Available> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val extensions = findExtensions()
|
||||
|
||||
@ -38,7 +39,7 @@ internal class ExtensionGithubApi {
|
||||
.filterIsInstance<LoadResult.Success>()
|
||||
.map { it.extension }
|
||||
|
||||
val extensionsWithUpdate = mutableListOf<Extension.Installed>()
|
||||
val extensionsWithUpdate = mutableListOf<Extension.Available>()
|
||||
val mutInstalledExtensions = installedExtensions.toMutableList()
|
||||
for (installedExt in mutInstalledExtensions) {
|
||||
val pkgName = installedExt.pkgName
|
||||
@ -46,7 +47,7 @@ internal class ExtensionGithubApi {
|
||||
|
||||
val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
||||
if (hasUpdate) {
|
||||
extensionsWithUpdate.add(installedExt)
|
||||
extensionsWithUpdate.add(availableExt)
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +76,7 @@ internal class ExtensionGithubApi {
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
fun getApkUrl(extension: ExtensionManager.ExtensionInfo): String {
|
||||
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageInstaller.SessionParams
|
||||
@ -56,6 +58,9 @@ class ExtensionInstallActivity : Activity() {
|
||||
session.commit(statusReceiver)
|
||||
val extensionManager: ExtensionManager by injectLazy()
|
||||
extensionManager.setInstalling(downloadId, sessionId)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
(getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).remove(downloadId)
|
||||
}
|
||||
data.close()
|
||||
} catch (error: Exception) {
|
||||
// Either install package can't be found (probably bots) or there's a security exception
|
||||
|
@ -5,14 +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 eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -29,6 +29,8 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@ -54,12 +56,12 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
||||
* returned by the download manager.
|
||||
*/
|
||||
private val activeDownloads = hashMapOf<String, Long>()
|
||||
val activeDownloads = hashMapOf<String, Long>()
|
||||
|
||||
/**
|
||||
* StateFlow used to notify the installation step of every download.
|
||||
*/
|
||||
private val downloadsStateFlow = MutableStateFlow(0L to InstallStep.Pending)
|
||||
val downloadsStateFlow = MutableStateFlow(0L to ExtensionIntallInfo(InstallStep.Pending, null))
|
||||
|
||||
/** Map of download id to installer session id */
|
||||
val downloadInstallerMap = hashMapOf<Long, Int>()
|
||||
@ -71,7 +73,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param url The url of the apk.
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: Extension): Flow<ExtensionIntallInfo> {
|
||||
suspend fun downloadAndInstall(url: String, extension: ExtensionManager.ExtensionInfo, scope: CoroutineScope): Flow<ExtensionIntallInfo> {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
val oldDownload = activeDownloads[pkgName]
|
||||
@ -96,33 +98,39 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
val id = downloadManager.enqueue(request)
|
||||
activeDownloads[pkgName] = id
|
||||
|
||||
return flowOf(
|
||||
pollStatus(id),
|
||||
pollInstallStatus(id),
|
||||
downloadsStateFlow.filter { it.first == id }
|
||||
.map {
|
||||
it.second to findSession(it.first)
|
||||
scope.launch {
|
||||
flowOf(
|
||||
pollStatus(id),
|
||||
pollInstallStatus(id)
|
||||
).flattenMerge()
|
||||
.transformWhile {
|
||||
emit(it)
|
||||
!it.first.isCompleted()
|
||||
}
|
||||
).flattenMerge()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.catch { e ->
|
||||
Timber.e(e)
|
||||
emit(InstallStep.Error to null)
|
||||
}
|
||||
.onCompletion {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
.collect {
|
||||
downloadsStateFlow.emit(id to it)
|
||||
}
|
||||
}
|
||||
|
||||
return downloadsStateFlow.filter { it.first == id }.map { it.second }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.transformWhile {
|
||||
emit(it)
|
||||
!it.first.isCompleted()
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.catch { e ->
|
||||
Timber.e(e)
|
||||
emit(InstallStep.Error to null)
|
||||
}
|
||||
.onCompletion {
|
||||
deleteDownload(pkgName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSession(downloadId: Long): PackageInstaller.SessionInfo? {
|
||||
val sessionId = downloadInstallerMap[downloadId] ?: return null
|
||||
return context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flow that polls the given download id for its status every second, as the
|
||||
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||
@ -134,11 +142,16 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
return flow {
|
||||
while (true) {
|
||||
val newDownloadState = downloadManager.query(query).use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
val newDownloadState = try {
|
||||
downloadManager.query(query)?.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
if (newDownloadState != null) {
|
||||
emit(newDownloadState)
|
||||
}
|
||||
emit(newDownloadState)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
@ -213,7 +226,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
* @param downloadId The id of the download.
|
||||
*/
|
||||
fun setInstalling(downloadId: Long, sessionId: Int) {
|
||||
downloadsStateFlow.tryEmit(downloadId to InstallStep.Installing)
|
||||
downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(InstallStep.Installing, null))
|
||||
downloadInstallerMap[downloadId] = sessionId
|
||||
}
|
||||
|
||||
@ -232,7 +245,11 @@ 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)
|
||||
downloadsStateFlow.tryEmit(downloadId to step)
|
||||
downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(step, null))
|
||||
}
|
||||
|
||||
fun softDeleteDownload(downloadId: Long) {
|
||||
downloadManager.remove(downloadId)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,10 +312,10 @@ internal class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
// Set next installation step
|
||||
if (uri != null) {
|
||||
downloadsStateFlow.tryEmit(id to InstallStep.Loading)
|
||||
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Loading, null))
|
||||
} else {
|
||||
Timber.e("Couldn't locate downloaded APK")
|
||||
downloadsStateFlow.tryEmit(id to InstallStep.Error)
|
||||
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Error, null))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -23,11 +23,9 @@ import eu.kanade.tachiyomi.util.system.withUIContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -81,7 +79,10 @@ class ExtensionBottomPresenter(
|
||||
sourceItems = findSourcesWithManga(favs)
|
||||
mangaItems = HashMap(
|
||||
sourceItems.associate {
|
||||
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(favs, it.source.id)
|
||||
it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(
|
||||
favs,
|
||||
it.source.id
|
||||
)
|
||||
}
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -94,6 +95,27 @@ class ExtensionBottomPresenter(
|
||||
}
|
||||
listOf(migrationJob, extensionJob).awaitAll()
|
||||
}
|
||||
presenterScope.launch {
|
||||
extensionManager.downloadRelay
|
||||
.collect {
|
||||
val extPageName = extensionManager.getExtension(it.first)
|
||||
val extension = extensions.find { item ->
|
||||
extPageName == item.extension.pkgName
|
||||
} ?: return@collect
|
||||
when (it.second.first) {
|
||||
InstallStep.Installed, InstallStep.Error -> {
|
||||
currentDownloads.remove(extension.extension.pkgName)
|
||||
}
|
||||
else -> {
|
||||
currentDownloads[extension.extension.pkgName] = it.second
|
||||
}
|
||||
}
|
||||
val item = updateInstallStep(extension.extension, it.second.first, it.second.second)
|
||||
if (item != null) {
|
||||
withUIContext { bottomSheet.downloadUpdate(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||
@ -249,15 +271,19 @@ class ExtensionBottomPresenter(
|
||||
fun installExtension(extension: Extension.Available) {
|
||||
if (isNotMIUIOptimized()) {
|
||||
presenterScope.launch {
|
||||
extensionManager.installExtension(extension).collectForInstallUpdate(extension)
|
||||
extensionManager.installExtension(ExtensionManager.ExtensionInfo(extension), presenterScope)
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateExtension(extension: Extension.Installed) {
|
||||
if (isNotMIUIOptimized()) {
|
||||
val availableExt =
|
||||
extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return
|
||||
presenterScope.launch {
|
||||
extensionManager.updateExtension(extension).collectForInstallUpdate(extension)
|
||||
extensionManager.installExtension(ExtensionManager.ExtensionInfo(availableExt), presenterScope)
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -270,24 +296,6 @@ class ExtensionBottomPresenter(
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun Flow<ExtensionIntallInfo>.collectForInstallUpdate(extension: Extension) {
|
||||
this
|
||||
.onEach { currentDownloads[extension.pkgName] = it }
|
||||
.onCompletion {
|
||||
currentDownloads.remove(extension.pkgName)
|
||||
val item = updateInstallStep(extension, null, null)
|
||||
if (item != null) {
|
||||
withUIContext { bottomSheet.downloadUpdate(item) }
|
||||
}
|
||||
}
|
||||
.collect { state ->
|
||||
val item = updateInstallStep(extension, state.first, state.second)
|
||||
if (item != null) {
|
||||
withUIContext { bottomSheet.downloadUpdate(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallExtension(pkgName: String) {
|
||||
extensionManager.uninstallExtension(pkgName)
|
||||
}
|
||||
|
@ -313,6 +313,7 @@
|
||||
<string name="_must_be_enabled_first">%1$s must be enabled first</string>
|
||||
<string name="could_not_install_extension">Could not install extension</string>
|
||||
<string name="update_all">Update all</string>
|
||||
<string name="updating_extensions">Updating extensions</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