diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d9e377c7f4..dd419fe399 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -204,6 +204,10 @@
android:name=".data.library.LibraryUpdateService"
android:exported="false" />
+
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
index a1cb5ff53c..c9ccbd1c30 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
@@ -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
*
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
index 2b0fe72714..a9dea6e342 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
@@ -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"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt
new file mode 100644
index 0000000000..cb85b0b8c9
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt
@@ -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()
+ )
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt
new file mode 100644
index 0000000000..0c9918ed57
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt
@@ -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(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): Intent {
+ return Intent(context, ExtensionInstallService::class.java).apply {
+ val info = extensions.map(::ExtensionInfo)
+ putParcelableArrayListExtra(KEY_EXTENSION, ArrayList(info))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
index 8ab5d70421..b9d3ff7dc5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
@@ -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 {
- 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 {
- val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
- ?: return emptyFlow()
- return installExtension(availableExt)
+ suspend fun installExtension(extension: ExtensionInfo, scope: CoroutineScope): Flow {
+ 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 {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
index ce9dbe322a..7f546e0db2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
@@ -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) {
+ private fun createUpdateNotification(extensions: List) {
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)
}
)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
index a2ff814ce2..c5caeac509 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
@@ -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 {
+ suspend fun checkForUpdates(context: Context): List {
return withContext(Dispatchers.IO) {
val extensions = findExtensions()
@@ -38,7 +39,7 @@ internal class ExtensionGithubApi {
.filterIsInstance()
.map { it.extension }
- val extensionsWithUpdate = mutableListOf()
+ val extensionsWithUpdate = mutableListOf()
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}"
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt
index a8d6622b2a..8d081dd37c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt
@@ -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
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt
index 2c9e8722f8..cc8966b858 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt
@@ -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()
+ val activeDownloads = hashMapOf()
/**
* 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()
@@ -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 {
+ suspend fun downloadAndInstall(url: String, extension: ExtensionManager.ExtensionInfo, scope: CoroutineScope): Flow {
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
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt
index f3c8377d31..777d89602d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt
@@ -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): List {
@@ -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.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)
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 28396c1835..fcc9f427c9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -313,6 +313,7 @@
%1$s must be enabled first
Could not install extension
Update all
+ Updating extensions
- %d update pending
- %d updates pending