mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Replace AppUpdateService with a WorkManager job
Fixes #7773 Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
This commit is contained in:
		| @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.BackupRestoreJob | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| import eu.kanade.tachiyomi.data.updater.AppUpdateService | ||||
| import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| @@ -85,6 +85,8 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             ACTION_CANCEL_RESTORE -> cancelRestore(context) | ||||
|             // Cancel library update and dismiss notification | ||||
|             ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context) | ||||
|             // Start downloading app update | ||||
|             ACTION_START_APP_UPDATE -> startDownloadAppUpdate(context, intent) | ||||
|             // Cancel downloading app update | ||||
|             ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context) | ||||
|             // Open reader activity | ||||
| @@ -209,8 +211,13 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|         LibraryUpdateJob.stop(context) | ||||
|     } | ||||
|  | ||||
|     private fun startDownloadAppUpdate(context: Context, intent: Intent) { | ||||
|         val url = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL) ?: return | ||||
|         AppUpdateDownloadJob.start(context, url) | ||||
|     } | ||||
|  | ||||
|     private fun cancelDownloadAppUpdate(context: Context) { | ||||
|         AppUpdateService.stop(context) | ||||
|         AppUpdateDownloadJob.stop(context) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -268,6 +275,7 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|  | ||||
|         private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE" | ||||
|  | ||||
|         private const val ACTION_START_APP_UPDATE = "$ID.$NAME.ACTION_START_APP_UPDATE" | ||||
|         private const val ACTION_CANCEL_APP_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_APP_UPDATE_DOWNLOAD" | ||||
|  | ||||
|         private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ" | ||||
| @@ -499,10 +507,25 @@ class NotificationReceiver : BroadcastReceiver() { | ||||
|             return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Returns [PendingIntent] that starts the [AppUpdateDownloadJob] to download an app update. | ||||
|          * | ||||
|          * @param context context of application | ||||
|          * @return [PendingIntent] | ||||
|          */ | ||||
|         internal fun downloadAppUpdatePendingBroadcast(context: Context, url: String, title: String? = null): PendingIntent { | ||||
|             return Intent(context, NotificationReceiver::class.java).run { | ||||
|                 action = ACTION_START_APP_UPDATE | ||||
|                 putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL, url) | ||||
|                 title?.let { putExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE, it) } | ||||
|                 PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * | ||||
|          */ | ||||
|         internal fun cancelUpdateDownloadPendingBroadcast(context: Context): PendingIntent { | ||||
|         internal fun cancelDownloadAppUpdatePendingBroadcast(context: Context): PendingIntent { | ||||
|             val intent = Intent(context, NotificationReceiver::class.java).apply { | ||||
|                 action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD | ||||
|             } | ||||
|   | ||||
| @@ -0,0 +1,148 @@ | ||||
| package eu.kanade.tachiyomi.data.updater | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.CoroutineWorker | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| import androidx.work.ForegroundInfo | ||||
| import androidx.work.NetworkType | ||||
| import androidx.work.OneTimeWorkRequestBuilder | ||||
| import androidx.work.WorkerParameters | ||||
| import androidx.work.workDataOf | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.network.ProgressListener | ||||
| import eu.kanade.tachiyomi.network.await | ||||
| import eu.kanade.tachiyomi.network.newCachelessCallWithProgress | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.storage.saveTo | ||||
| import eu.kanade.tachiyomi.util.system.workManager | ||||
| import logcat.LogPriority | ||||
| import okhttp3.internal.http2.ErrorCode | ||||
| import okhttp3.internal.http2.StreamResetException | ||||
| import tachiyomi.core.util.lang.withIOContext | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import kotlin.coroutines.cancellation.CancellationException | ||||
|  | ||||
| class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     CoroutineWorker(context, workerParams) { | ||||
|  | ||||
|     private val notifier = AppUpdateNotifier(context) | ||||
|     private val network: NetworkHelper by injectLazy() | ||||
|  | ||||
|     override suspend fun doWork(): Result { | ||||
|         val url = inputData.getString(EXTRA_DOWNLOAD_URL) | ||||
|         val title = inputData.getString(EXTRA_DOWNLOAD_TITLE) ?: context.getString(R.string.app_name) | ||||
|  | ||||
|         if (url.isNullOrEmpty()) { | ||||
|             return Result.failure() | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             setForeground(getForegroundInfo()) | ||||
|         } catch (e: IllegalStateException) { | ||||
|             logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" } | ||||
|         } | ||||
|  | ||||
|         withIOContext { | ||||
|             downloadApk(title, url) | ||||
|         } | ||||
|  | ||||
|         return Result.success() | ||||
|     } | ||||
|  | ||||
|     override suspend fun getForegroundInfo(): ForegroundInfo { | ||||
|         return ForegroundInfo( | ||||
|             Notifications.ID_APP_UPDATER, | ||||
|             notifier.onDownloadStarted().build(), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called to start downloading apk of new update | ||||
|      * | ||||
|      * @param url url location of file | ||||
|      */ | ||||
|     private suspend fun downloadApk(title: String, url: String) { | ||||
|         // Show notification download starting. | ||||
|         notifier.onDownloadStarted(title) | ||||
|  | ||||
|         val progressListener = object : ProgressListener { | ||||
|             // Progress of the download | ||||
|             var savedProgress = 0 | ||||
|  | ||||
|             // Keep track of the last notification sent to avoid posting too many. | ||||
|             var lastTick = 0L | ||||
|  | ||||
|             override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { | ||||
|                 val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt() | ||||
|                 val currentTime = System.currentTimeMillis() | ||||
|                 if (progress > savedProgress && currentTime - 200 > lastTick) { | ||||
|                     savedProgress = progress | ||||
|                     lastTick = currentTime | ||||
|                     notifier.onProgressChange(progress) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             // Download the new update. | ||||
|             val response = network.client.newCachelessCallWithProgress(GET(url), progressListener) | ||||
|                 .await() | ||||
|  | ||||
|             // File where the apk will be saved. | ||||
|             val apkFile = File(context.externalCacheDir, "update.apk") | ||||
|  | ||||
|             if (response.isSuccessful) { | ||||
|                 response.body.source().saveTo(apkFile) | ||||
|             } else { | ||||
|                 response.close() | ||||
|                 throw Exception("Unsuccessful response") | ||||
|             } | ||||
|             notifier.cancel() | ||||
|             notifier.promptInstall(apkFile.getUriCompat(context)) | ||||
|         } catch (e: Exception) { | ||||
|             val shouldCancel = e is CancellationException || | ||||
|                 (e is StreamResetException && e.errorCode == ErrorCode.CANCEL) | ||||
|             if (shouldCancel) { | ||||
|                 notifier.cancel() | ||||
|             } else { | ||||
|                 notifier.onDownloadError(url) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG = "AppUpdateDownload" | ||||
|  | ||||
|         const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL" | ||||
|         const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE" | ||||
|  | ||||
|         fun start(context: Context, url: String, title: String? = null) { | ||||
|             val constraints = Constraints( | ||||
|                 requiredNetworkType = NetworkType.CONNECTED, | ||||
|             ) | ||||
|  | ||||
|             val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>() | ||||
|                 .setConstraints(constraints) | ||||
|                 .addTag(TAG) | ||||
|                 .setInputData( | ||||
|                     workDataOf( | ||||
|                         EXTRA_DOWNLOAD_URL to url, | ||||
|                         EXTRA_DOWNLOAD_TITLE to title, | ||||
|                     ), | ||||
|                 ) | ||||
|                 .build() | ||||
|  | ||||
|             context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) | ||||
|         } | ||||
|  | ||||
|         fun stop(context: Context) { | ||||
|             context.workManager.cancelUniqueWork(TAG) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -34,11 +34,11 @@ internal class AppUpdateNotifier(private val context: Context) { | ||||
|  | ||||
|     @SuppressLint("LaunchActivityFromNotification") | ||||
|     fun promptUpdate(release: Release) { | ||||
|         val updateIntent = Intent(context, AppUpdateService::class.java).run { | ||||
|             putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink()) | ||||
|             putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version) | ||||
|             PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) | ||||
|         } | ||||
|         val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast( | ||||
|             context, | ||||
|             release.getDownloadLink(), | ||||
|             release.version, | ||||
|         ) | ||||
|  | ||||
|         val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run { | ||||
|             flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP | ||||
| @@ -82,7 +82,7 @@ internal class AppUpdateNotifier(private val context: Context) { | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel), | ||||
|                 NotificationReceiver.cancelUpdateDownloadPendingBroadcast(context), | ||||
|                 NotificationReceiver.cancelDownloadAppUpdatePendingBroadcast(context), | ||||
|             ) | ||||
|         } | ||||
|         notificationBuilder.show() | ||||
| @@ -164,7 +164,7 @@ internal class AppUpdateNotifier(private val context: Context) { | ||||
|             addAction( | ||||
|                 R.drawable.ic_refresh_24dp, | ||||
|                 context.getString(R.string.action_retry), | ||||
|                 AppUpdateService.downloadApkPendingService(context, url), | ||||
|                 NotificationReceiver.downloadAppUpdatePendingBroadcast(context, url), | ||||
|             ) | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|   | ||||
| @@ -1,195 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.updater | ||||
|  | ||||
| import android.app.PendingIntent | ||||
| import android.app.Service | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.os.IBinder | ||||
| import android.os.PowerManager | ||||
| import androidx.core.content.ContextCompat | ||||
| import eu.kanade.tachiyomi.BuildConfig | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.network.ProgressListener | ||||
| import eu.kanade.tachiyomi.network.await | ||||
| import eu.kanade.tachiyomi.network.newCachelessCallWithProgress | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.storage.saveTo | ||||
| import eu.kanade.tachiyomi.util.system.acquireWakeLock | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
| import kotlinx.coroutines.CancellationException | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.SupervisorJob | ||||
| import kotlinx.coroutines.cancel | ||||
| import kotlinx.coroutines.launch | ||||
| import okhttp3.internal.http2.ErrorCode | ||||
| import okhttp3.internal.http2.StreamResetException | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
|  | ||||
| class AppUpdateService : Service() { | ||||
|  | ||||
|     private val network: NetworkHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Wake lock that will be held until the service is destroyed. | ||||
|      */ | ||||
|     private lateinit var wakeLock: PowerManager.WakeLock | ||||
|     private lateinit var notifier: AppUpdateNotifier | ||||
|  | ||||
|     private val job = SupervisorJob() | ||||
|     private val serviceScope = CoroutineScope(Dispatchers.IO + job) | ||||
|  | ||||
|     override fun onCreate() { | ||||
|         notifier = AppUpdateNotifier(this) | ||||
|         wakeLock = acquireWakeLock(javaClass.name) | ||||
|  | ||||
|         startForeground(Notifications.ID_APP_UPDATER, notifier.onDownloadStarted().build()) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This method needs to be implemented, but it's not used/needed. | ||||
|      */ | ||||
|     override fun onBind(intent: Intent): IBinder? = null | ||||
|  | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         if (intent == null) return START_NOT_STICKY | ||||
|  | ||||
|         val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY | ||||
|         val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name) | ||||
|  | ||||
|         serviceScope.launch { | ||||
|             downloadApk(title, url) | ||||
|         } | ||||
|  | ||||
|         job.invokeOnCompletion { stopSelf(startId) } | ||||
|         return START_NOT_STICKY | ||||
|     } | ||||
|  | ||||
|     override fun stopService(name: Intent?): Boolean { | ||||
|         destroyJob() | ||||
|         return super.stopService(name) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroy() { | ||||
|         destroyJob() | ||||
|     } | ||||
|  | ||||
|     private fun destroyJob() { | ||||
|         serviceScope.cancel() | ||||
|         job.cancel() | ||||
|         if (wakeLock.isHeld) { | ||||
|             wakeLock.release() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called to start downloading apk of new update | ||||
|      * | ||||
|      * @param url url location of file | ||||
|      */ | ||||
|     private suspend fun downloadApk(title: String, url: String) { | ||||
|         // Show notification download starting. | ||||
|         notifier.onDownloadStarted(title) | ||||
|  | ||||
|         val progressListener = object : ProgressListener { | ||||
|             // Progress of the download | ||||
|             var savedProgress = 0 | ||||
|  | ||||
|             // Keep track of the last notification sent to avoid posting too many. | ||||
|             var lastTick = 0L | ||||
|  | ||||
|             override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { | ||||
|                 val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt() | ||||
|                 val currentTime = System.currentTimeMillis() | ||||
|                 if (progress > savedProgress && currentTime - 200 > lastTick) { | ||||
|                     savedProgress = progress | ||||
|                     lastTick = currentTime | ||||
|                     notifier.onProgressChange(progress) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             // Download the new update. | ||||
|             val response = network.client.newCachelessCallWithProgress(GET(url), progressListener) | ||||
|                 .await() | ||||
|  | ||||
|             // File where the apk will be saved. | ||||
|             val apkFile = File(externalCacheDir, "update.apk") | ||||
|  | ||||
|             if (response.isSuccessful) { | ||||
|                 response.body.source().saveTo(apkFile) | ||||
|             } else { | ||||
|                 response.close() | ||||
|                 throw Exception("Unsuccessful response") | ||||
|             } | ||||
|             notifier.cancel() | ||||
|             notifier.promptInstall(apkFile.getUriCompat(this)) | ||||
|         } catch (e: Exception) { | ||||
|             val shouldCancel = e is CancellationException || | ||||
|                 (e is StreamResetException && e.errorCode == ErrorCode.CANCEL) | ||||
|             if (shouldCancel) { | ||||
|                 notifier.cancel() | ||||
|             } else { | ||||
|                 notifier.onDownloadError(url) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|  | ||||
|         internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL" | ||||
|         internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE" | ||||
|  | ||||
|         /** | ||||
|          * Returns the status of the service. | ||||
|          * | ||||
|          * @param context the application context. | ||||
|          * @return true if the service is running, false otherwise. | ||||
|          */ | ||||
|         private fun isRunning(context: Context): Boolean = | ||||
|             context.isServiceRunning(AppUpdateService::class.java) | ||||
|  | ||||
|         /** | ||||
|          * Downloads a new update and let the user install the new version from a notification. | ||||
|          * | ||||
|          * @param context the application context. | ||||
|          * @param url the url to the new update. | ||||
|          */ | ||||
|         fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) { | ||||
|             if (isRunning(context)) return | ||||
|  | ||||
|             Intent(context, AppUpdateService::class.java).apply { | ||||
|                 putExtra(EXTRA_DOWNLOAD_TITLE, title) | ||||
|                 putExtra(EXTRA_DOWNLOAD_URL, url) | ||||
|                 ContextCompat.startForegroundService(context, this) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Stops the service. | ||||
|          * | ||||
|          * @param context the application context | ||||
|          */ | ||||
|         fun stop(context: Context) { | ||||
|             context.stopService(Intent(context, AppUpdateService::class.java)) | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Returns [PendingIntent] that starts a service which downloads the apk specified in url. | ||||
|          * | ||||
|          * @param url the url to the new update. | ||||
|          * @return [PendingIntent] | ||||
|          */ | ||||
|         internal fun downloadApkPendingService(context: Context, url: String): PendingIntent { | ||||
|             return Intent(context, AppUpdateService::class.java).run { | ||||
|                 putExtra(EXTRA_DOWNLOAD_URL, url) | ||||
|                 PendingIntent.getService(context, 0, this, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -7,7 +7,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.presentation.more.NewUpdateScreen | ||||
| import eu.kanade.presentation.util.Screen | ||||
| import eu.kanade.tachiyomi.data.updater.AppUpdateService | ||||
| import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob | ||||
| import eu.kanade.tachiyomi.util.system.openInBrowser | ||||
|  | ||||
| class NewUpdateScreen( | ||||
| @@ -31,7 +31,7 @@ class NewUpdateScreen( | ||||
|             onOpenInBrowser = { context.openInBrowser(releaseLink) }, | ||||
|             onRejectUpdate = navigator::pop, | ||||
|             onAcceptUpdate = { | ||||
|                 AppUpdateService.start( | ||||
|                 AppUpdateDownloadJob.start( | ||||
|                     context = context, | ||||
|                     url = downloadLink, | ||||
|                     title = versionName, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user