mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 04:59:17 +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:name=".data.library.LibraryUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".extension.ExtensionInstallService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.download.DownloadService"
|
android:name=".data.download.DownloadService"
|
||||||
android:exported="false" />
|
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.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
import eu.kanade.tachiyomi.data.updater.UpdaterService
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionInstallService
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||||
@ -71,6 +72,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
)
|
)
|
||||||
// Cancel library update and dismiss notification
|
// Cancel library update and dismiss notification
|
||||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
||||||
|
ACTION_CANCEL_EXTENSION_UPDATE -> cancelExtensionUpdate(context)
|
||||||
ACTION_CANCEL_UPDATE_DOWNLOAD -> cancelDownloadUpdate(context)
|
ACTION_CANCEL_UPDATE_DOWNLOAD -> cancelDownloadUpdate(context)
|
||||||
ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context)
|
ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context)
|
||||||
// Share backup file
|
// Share backup file
|
||||||
@ -199,6 +201,17 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
Handler().post { dismissNotification(context, Notifications.ID_LIBRARY_PROGRESS) }
|
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
|
* Method called when user wants to mark as read
|
||||||
*
|
*
|
||||||
@ -251,6 +264,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Called to cancel library update.
|
// Called to cancel library update.
|
||||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.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"
|
private const val ACTION_CANCEL_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_UPDATE_DOWNLOAD"
|
||||||
|
|
||||||
// Called to mark as read
|
// Called to mark as read
|
||||||
@ -568,6 +584,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
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
|
* 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 CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
|
const val ID_EXTENSION_PROGRESS = -402
|
||||||
|
|
||||||
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
|
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
|
||||||
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
|
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.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Parcelable
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
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.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||||
import eu.kanade.tachiyomi.util.system.launchNow
|
import eu.kanade.tachiyomi.util.system.launchNow
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -49,6 +51,15 @@ class ExtensionManager(
|
|||||||
*/
|
*/
|
||||||
private val installer by lazy { ExtensionInstaller(context) }
|
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.
|
* Relay used to notify the installed extensions.
|
||||||
*/
|
*/
|
||||||
@ -240,21 +251,8 @@ class ExtensionManager(
|
|||||||
*
|
*
|
||||||
* @param extension The extension to be installed.
|
* @param extension The extension to be installed.
|
||||||
*/
|
*/
|
||||||
fun installExtension(extension: Extension.Available): Flow<ExtensionIntallInfo> {
|
suspend fun installExtension(extension: ExtensionInfo, scope: CoroutineScope): Flow<ExtensionIntallInfo> {
|
||||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
return installer.downloadAndInstall(api.getApkUrl(extension), extension, scope)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -407,6 +405,19 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
return this
|
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 {
|
interface ExtensionsChangedListener {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.extension
|
package eu.kanade.tachiyomi.extension
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
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.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.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -35,15 +38,20 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pendingUpdates.isNotEmpty()) {
|
if (pendingUpdates.isNotEmpty()) {
|
||||||
createUpdateNotification(pendingUpdates.map { it.name })
|
createUpdateNotification(pendingUpdates)
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.success()
|
Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUpdateNotification(names: List<String>) {
|
private fun createUpdateNotification(extensions: List<Extension.Available>) {
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
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 {
|
NotificationManagerCompat.from(context).apply {
|
||||||
notify(
|
notify(
|
||||||
Notifications.ID_UPDATES_TO_EXTS,
|
Notifications.ID_UPDATES_TO_EXTS,
|
||||||
@ -51,11 +59,11 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||||||
setContentTitle(
|
setContentTitle(
|
||||||
context.resources.getQuantityString(
|
context.resources.getQuantityString(
|
||||||
R.plurals.extension_updates_available,
|
R.plurals.extension_updates_available,
|
||||||
names.size,
|
extensions.size,
|
||||||
names.size
|
extensions.size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val extNames = names.joinToString(", ")
|
val extNames = extensions.joinToString(", ") { it.name }
|
||||||
setContentText(extNames)
|
setContentText(extNames)
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
|
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
|
||||||
setSmallIcon(R.drawable.ic_extension_update_24dp)
|
setSmallIcon(R.drawable.ic_extension_update_24dp)
|
||||||
@ -65,6 +73,16 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
|
|||||||
context
|
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)
|
setAutoCancel(true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
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) {
|
return withContext(Dispatchers.IO) {
|
||||||
val extensions = findExtensions()
|
val extensions = findExtensions()
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ internal class ExtensionGithubApi {
|
|||||||
.filterIsInstance<LoadResult.Success>()
|
.filterIsInstance<LoadResult.Success>()
|
||||||
.map { it.extension }
|
.map { it.extension }
|
||||||
|
|
||||||
val extensionsWithUpdate = mutableListOf<Extension.Installed>()
|
val extensionsWithUpdate = mutableListOf<Extension.Available>()
|
||||||
val mutInstalledExtensions = installedExtensions.toMutableList()
|
val mutInstalledExtensions = installedExtensions.toMutableList()
|
||||||
for (installedExt in mutInstalledExtensions) {
|
for (installedExt in mutInstalledExtensions) {
|
||||||
val pkgName = installedExt.pkgName
|
val pkgName = installedExt.pkgName
|
||||||
@ -46,7 +47,7 @@ internal class ExtensionGithubApi {
|
|||||||
|
|
||||||
val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
||||||
if (hasUpdate) {
|
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}"
|
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.DownloadManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.content.pm.PackageInstaller.SessionParams
|
import android.content.pm.PackageInstaller.SessionParams
|
||||||
@ -56,6 +58,9 @@ class ExtensionInstallActivity : Activity() {
|
|||||||
session.commit(statusReceiver)
|
session.commit(statusReceiver)
|
||||||
val extensionManager: ExtensionManager by injectLazy()
|
val extensionManager: ExtensionManager by injectLazy()
|
||||||
extensionManager.setInstalling(downloadId, sessionId)
|
extensionManager.setInstalling(downloadId, sessionId)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
(getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).remove(downloadId)
|
||||||
|
}
|
||||||
data.close()
|
data.close()
|
||||||
} 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
|
||||||
|
@ -5,14 +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 eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
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.ui.extension.ExtensionIntallInfo
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -29,6 +29,8 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.transformWhile
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
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
|
* The currently requested downloads, with the package name (unique id) as key, and the id
|
||||||
* returned by the download manager.
|
* 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.
|
* 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 */
|
/** Map of download id to installer session id */
|
||||||
val downloadInstallerMap = hashMapOf<Long, Int>()
|
val downloadInstallerMap = hashMapOf<Long, Int>()
|
||||||
@ -71,7 +73,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param url The url of the apk.
|
* @param url The url of the apk.
|
||||||
* @param extension The extension to install.
|
* @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 pkgName = extension.pkgName
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
val oldDownload = activeDownloads[pkgName]
|
||||||
@ -96,13 +98,10 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
val id = downloadManager.enqueue(request)
|
val id = downloadManager.enqueue(request)
|
||||||
activeDownloads[pkgName] = id
|
activeDownloads[pkgName] = id
|
||||||
|
|
||||||
return flowOf(
|
scope.launch {
|
||||||
|
flowOf(
|
||||||
pollStatus(id),
|
pollStatus(id),
|
||||||
pollInstallStatus(id),
|
pollInstallStatus(id)
|
||||||
downloadsStateFlow.filter { it.first == id }
|
|
||||||
.map {
|
|
||||||
it.second to findSession(it.first)
|
|
||||||
}
|
|
||||||
).flattenMerge()
|
).flattenMerge()
|
||||||
.transformWhile {
|
.transformWhile {
|
||||||
emit(it)
|
emit(it)
|
||||||
@ -116,11 +115,20 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
.onCompletion {
|
.onCompletion {
|
||||||
deleteDownload(pkgName)
|
deleteDownload(pkgName)
|
||||||
}
|
}
|
||||||
|
.collect {
|
||||||
|
downloadsStateFlow.emit(id to it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findSession(downloadId: Long): PackageInstaller.SessionInfo? {
|
return downloadsStateFlow.filter { it.first == id }.map { it.second }
|
||||||
val sessionId = downloadInstallerMap[downloadId] ?: return null
|
.flowOn(Dispatchers.IO)
|
||||||
return context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
.transformWhile {
|
||||||
|
emit(it)
|
||||||
|
!it.first.isCompleted()
|
||||||
|
}
|
||||||
|
.onCompletion {
|
||||||
|
deleteDownload(pkgName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,11 +142,16 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
|
|
||||||
return flow {
|
return flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
val newDownloadState = downloadManager.query(query).use { cursor ->
|
val newDownloadState = try {
|
||||||
|
downloadManager.query(query)?.use { cursor ->
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
}
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
if (newDownloadState != null) {
|
||||||
emit(newDownloadState)
|
emit(newDownloadState)
|
||||||
|
}
|
||||||
delay(1000)
|
delay(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,7 +226,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param downloadId The id of the download.
|
* @param downloadId The id of the download.
|
||||||
*/
|
*/
|
||||||
fun setInstalling(downloadId: Long, sessionId: Int) {
|
fun setInstalling(downloadId: Long, sessionId: Int) {
|
||||||
downloadsStateFlow.tryEmit(downloadId to InstallStep.Installing)
|
downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(InstallStep.Installing, null))
|
||||||
downloadInstallerMap[downloadId] = sessionId
|
downloadInstallerMap[downloadId] = sessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +245,11 @@ 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)
|
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
|
// Set next installation step
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
downloadsStateFlow.tryEmit(id to InstallStep.Loading)
|
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Loading, null))
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Couldn't locate downloaded APK")
|
Timber.e("Couldn't locate downloaded APK")
|
||||||
downloadsStateFlow.tryEmit(id to InstallStep.Error)
|
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Error, null))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,11 +23,9 @@ import eu.kanade.tachiyomi.util.system.withUIContext
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -81,7 +79,10 @@ class ExtensionBottomPresenter(
|
|||||||
sourceItems = findSourcesWithManga(favs)
|
sourceItems = findSourcesWithManga(favs)
|
||||||
mangaItems = HashMap(
|
mangaItems = HashMap(
|
||||||
sourceItems.associate {
|
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) {
|
withContext(Dispatchers.Main) {
|
||||||
@ -94,6 +95,27 @@ class ExtensionBottomPresenter(
|
|||||||
}
|
}
|
||||||
listOf(migrationJob, extensionJob).awaitAll()
|
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> {
|
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
|
||||||
@ -249,15 +271,19 @@ class ExtensionBottomPresenter(
|
|||||||
fun installExtension(extension: Extension.Available) {
|
fun installExtension(extension: Extension.Available) {
|
||||||
if (isNotMIUIOptimized()) {
|
if (isNotMIUIOptimized()) {
|
||||||
presenterScope.launch {
|
presenterScope.launch {
|
||||||
extensionManager.installExtension(extension).collectForInstallUpdate(extension)
|
extensionManager.installExtension(ExtensionManager.ExtensionInfo(extension), presenterScope)
|
||||||
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateExtension(extension: Extension.Installed) {
|
fun updateExtension(extension: Extension.Installed) {
|
||||||
if (isNotMIUIOptimized()) {
|
if (isNotMIUIOptimized()) {
|
||||||
|
val availableExt =
|
||||||
|
extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return
|
||||||
presenterScope.launch {
|
presenterScope.launch {
|
||||||
extensionManager.updateExtension(extension).collectForInstallUpdate(extension)
|
extensionManager.installExtension(ExtensionManager.ExtensionInfo(availableExt), presenterScope)
|
||||||
|
.launchIn(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,24 +296,6 @@ class ExtensionBottomPresenter(
|
|||||||
return true
|
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) {
|
fun uninstallExtension(pkgName: String) {
|
||||||
extensionManager.uninstallExtension(pkgName)
|
extensionManager.uninstallExtension(pkgName)
|
||||||
}
|
}
|
||||||
|
@ -313,6 +313,7 @@
|
|||||||
<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>
|
<string name="could_not_install_extension">Could not install extension</string>
|
||||||
<string name="update_all">Update all</string>
|
<string name="update_all">Update all</string>
|
||||||
|
<string name="updating_extensions">Updating extensions</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