Improve app update prompt flow experience (#3669)
This commit is contained in:
@@ -13,10 +13,12 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import eu.kanade.presentation.manga.components.MarkdownRender
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.ui.more.NewUpdateScreenModel
|
||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
@@ -27,16 +29,27 @@ import tachiyomi.presentation.core.screens.InfoScreen
|
||||
fun NewUpdateScreen(
|
||||
versionName: String,
|
||||
changelogInfo: String,
|
||||
stage: NewUpdateScreenModel.Stage,
|
||||
downloadProgress: () -> Int,
|
||||
onOpenInBrowser: () -> Unit,
|
||||
onRejectUpdate: () -> Unit,
|
||||
onAcceptUpdate: () -> Unit,
|
||||
onRejectUpdate: () -> Unit,
|
||||
) {
|
||||
InfoScreen(
|
||||
icon = Icons.Outlined.NewReleases,
|
||||
headingText = stringResource(MR.strings.update_check_notification_update_available),
|
||||
subtitleText = versionName,
|
||||
acceptText = stringResource(MR.strings.update_check_confirm),
|
||||
acceptText = when (stage) {
|
||||
NewUpdateScreenModel.Stage.Available -> stringResource(MR.strings.update_check_confirm)
|
||||
NewUpdateScreenModel.Stage.Downloading -> stringResource(
|
||||
MR.strings.downloading_with_progress,
|
||||
downloadProgress(),
|
||||
)
|
||||
NewUpdateScreenModel.Stage.Downloaded -> stringResource(MR.strings.action_install)
|
||||
NewUpdateScreenModel.Stage.Failed -> stringResource(MR.strings.action_retry)
|
||||
},
|
||||
onAcceptClick = onAcceptUpdate,
|
||||
canAccept = stage != NewUpdateScreenModel.Stage.Downloading,
|
||||
rejectText = stringResource(MR.strings.action_not_now),
|
||||
onRejectClick = onRejectUpdate,
|
||||
) {
|
||||
@@ -47,7 +60,7 @@ fun NewUpdateScreen(
|
||||
) {
|
||||
MarkdownRender(
|
||||
content = changelogInfo,
|
||||
flavour = GFMFlavourDescriptor(),
|
||||
flavour = remember { GFMFlavourDescriptor() },
|
||||
)
|
||||
|
||||
TextButton(
|
||||
@@ -76,9 +89,11 @@ private fun NewUpdateScreenPreview() {
|
||||
- Hello
|
||||
- World
|
||||
""".trimIndent(),
|
||||
stage = NewUpdateScreenModel.Stage.Available,
|
||||
downloadProgress = { 0 },
|
||||
onOpenInBrowser = {},
|
||||
onRejectUpdate = {},
|
||||
onAcceptUpdate = {},
|
||||
onRejectUpdate = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ object AboutScreen : Screen() {
|
||||
val updateChecker = AppUpdateChecker()
|
||||
withUIContext {
|
||||
try {
|
||||
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {
|
||||
when (val result = withIOContext { updateChecker.checkForUpdate(forceCheck = true) }) {
|
||||
is GetApplicationRelease.Result.NewUpdate -> {
|
||||
onAvailableUpdate(result)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
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.system.cancelNotification
|
||||
@@ -72,10 +71,6 @@ 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
|
||||
ACTION_OPEN_CHAPTER -> {
|
||||
openChapter(
|
||||
@@ -178,15 +173,6 @@ 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) {
|
||||
AppUpdateDownloadJob.stop(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to mark manga chapters as read
|
||||
*
|
||||
@@ -241,9 +227,6 @@ 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"
|
||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||
private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER"
|
||||
@@ -524,45 +507,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 cancelDownloadAppUpdatePendingBroadcast(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_APP_UPDATE_DOWNLOAD
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that opens the extensions controller.
|
||||
*
|
||||
|
||||
@@ -70,8 +70,6 @@ object Notifications {
|
||||
private const val GROUP_APK_UPDATES = "group_apk_updates"
|
||||
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
|
||||
const val ID_APP_UPDATER = 1
|
||||
const val ID_APP_UPDATE_PROMPT = 2
|
||||
const val ID_APP_UPDATE_ERROR = 3
|
||||
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
||||
const val ID_UPDATES_TO_EXTS = -401
|
||||
const val ID_EXTENSION_INSTALLER = -402
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.util.system.isFossBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
@@ -12,7 +11,7 @@ class AppUpdateChecker {
|
||||
|
||||
private val getApplicationRelease: GetApplicationRelease by injectLazy()
|
||||
|
||||
suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result {
|
||||
suspend fun checkForUpdate(forceCheck: Boolean = false): GetApplicationRelease.Result {
|
||||
// Disable app update checks for older Android versions that we're going to drop support for
|
||||
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
// return GetApplicationRelease.Result.OsTooOld
|
||||
@@ -30,11 +29,6 @@ class AppUpdateChecker {
|
||||
),
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,30 +15,26 @@ 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.awaitSuccess
|
||||
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.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import okhttp3.internal.http2.ErrorCode
|
||||
import okhttp3.internal.http2.StreamResetException
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.i18n.MR
|
||||
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.stringResource(MR.strings.app_name)
|
||||
|
||||
if (url.isNullOrEmpty()) {
|
||||
return Result.failure()
|
||||
@@ -46,17 +42,27 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
withIOContext {
|
||||
downloadApk(title, url)
|
||||
return try {
|
||||
withIOContext { downloadApk(url) }
|
||||
Result.success(workDataOf(PROGRESS to 100, EXTRA_DOWNLOAD_URL to url))
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
val notification = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE)
|
||||
.setContentTitle(context.stringResource(MR.strings.update_check_notification_update_available))
|
||||
.setContentText(context.stringResource(MR.strings.update_check_notification_download_in_progress))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_APP_UPDATER,
|
||||
notifier.onDownloadStarted().build(),
|
||||
notification,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
@@ -70,10 +76,7 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
|
||||
*
|
||||
* @param url url location of file
|
||||
*/
|
||||
private suspend fun downloadApk(title: String, url: String) {
|
||||
// Show notification download starting.
|
||||
notifier.onDownloadStarted(title)
|
||||
|
||||
private suspend fun downloadApk(url: String) {
|
||||
val progressListener = object : ProgressListener {
|
||||
// Progress of the download
|
||||
var savedProgress = 0
|
||||
@@ -87,45 +90,27 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
|
||||
if (progress > savedProgress && currentTime - 200 > lastTick) {
|
||||
savedProgress = progress
|
||||
lastTick = currentTime
|
||||
notifier.onProgressChange(progress)
|
||||
setProgressAsync(workDataOf(PROGRESS to progress, EXTRA_DOWNLOAD_URL to url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Download the new update.
|
||||
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener)
|
||||
.await()
|
||||
val response = network.client.newCachelessCallWithProgress(GET(url), progressListener).awaitSuccess()
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
val apkFile = updateApk(context)
|
||||
response.body.source().saveTo(apkFile)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AppUpdateDownload"
|
||||
const val TAG = "AppUpdateDownload"
|
||||
|
||||
const val PROGRESS = "progress"
|
||||
|
||||
const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL"
|
||||
const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE"
|
||||
|
||||
fun start(context: Context, url: String, title: String? = null) {
|
||||
fun updateApk(context: Context): File = File(context.externalCacheDir, "update.apk")
|
||||
|
||||
fun start(context: Context, url: String) {
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = NetworkType.CONNECTED,
|
||||
)
|
||||
@@ -133,12 +118,7 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
|
||||
val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG)
|
||||
.setInputData(
|
||||
workDataOf(
|
||||
EXTRA_DOWNLOAD_URL to url,
|
||||
EXTRA_DOWNLOAD_TITLE to title,
|
||||
),
|
||||
)
|
||||
.setInputData(workDataOf(EXTRA_DOWNLOAD_URL to url))
|
||||
.build()
|
||||
|
||||
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
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.notify
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.domain.release.model.Release
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
internal class AppUpdateNotifier(private val context: Context) {
|
||||
|
||||
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE)
|
||||
|
||||
/**
|
||||
* Call to show notification.
|
||||
*
|
||||
* @param id id of the notification channel.
|
||||
*/
|
||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_APP_UPDATER) {
|
||||
context.notify(id, build())
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
|
||||
}
|
||||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
fun promptUpdate(release: Release) {
|
||||
val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast(
|
||||
context,
|
||||
release.downloadLink,
|
||||
release.version,
|
||||
)
|
||||
|
||||
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
release.hashCode(),
|
||||
this,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(context.stringResource(MR.strings.update_check_notification_update_available))
|
||||
setContentText(release.version)
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setContentIntent(updateIntent)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
android.R.drawable.stat_sys_download_done,
|
||||
context.stringResource(MR.strings.action_download),
|
||||
updateIntent,
|
||||
)
|
||||
addAction(
|
||||
R.drawable.ic_info_24dp,
|
||||
context.stringResource(MR.strings.whats_new),
|
||||
releaseIntent,
|
||||
)
|
||||
}
|
||||
notificationBuilder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download starts.
|
||||
*
|
||||
* @param title tile of notification.
|
||||
*/
|
||||
fun onDownloadStarted(title: String? = null): NotificationCompat.Builder {
|
||||
with(notificationBuilder) {
|
||||
title?.let { setContentTitle(title) }
|
||||
setContentText(context.stringResource(MR.strings.update_check_notification_download_in_progress))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
setOngoing(true)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.stringResource(MR.strings.action_cancel),
|
||||
NotificationReceiver.cancelDownloadAppUpdatePendingBroadcast(context),
|
||||
)
|
||||
}
|
||||
notificationBuilder.show()
|
||||
return notificationBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download progress changes.
|
||||
*
|
||||
* @param progress progress of download (xx%/100).
|
||||
*/
|
||||
fun onProgressChange(progress: Int) {
|
||||
with(notificationBuilder) {
|
||||
setProgress(100, progress, false)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
notificationBuilder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download is finished.
|
||||
*
|
||||
* @param uri path location of apk.
|
||||
*/
|
||||
fun promptInstall(uri: Uri) {
|
||||
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
|
||||
with(notificationBuilder) {
|
||||
setContentText(context.stringResource(MR.strings.update_check_notification_download_complete))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
setContentIntent(installIntent)
|
||||
setOngoing(true)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_system_update_alt_white_24dp,
|
||||
context.stringResource(MR.strings.action_install),
|
||||
installIntent,
|
||||
)
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.stringResource(MR.strings.action_cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_PROMPT),
|
||||
)
|
||||
}
|
||||
notificationBuilder.show(Notifications.ID_APP_UPDATE_PROMPT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when apk download throws a error
|
||||
*
|
||||
* @param url web location of apk to download.
|
||||
*/
|
||||
fun onDownloadError(url: String) {
|
||||
with(notificationBuilder) {
|
||||
setContentText(context.stringResource(MR.strings.update_check_notification_download_error))
|
||||
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||
setOnlyAlertOnce(false)
|
||||
setProgress(0, 0, false)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_refresh_24dp,
|
||||
context.stringResource(MR.strings.action_retry),
|
||||
NotificationReceiver.downloadAppUpdatePendingBroadcast(context, url),
|
||||
)
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
context.stringResource(MR.strings.action_cancel),
|
||||
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_ERROR),
|
||||
)
|
||||
}
|
||||
notificationBuilder.show(Notifications.ID_APP_UPDATE_ERROR)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
@@ -53,6 +54,7 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
addSingleton<Context>(app)
|
||||
|
||||
addSingletonFactory<SqlDriver> {
|
||||
synchronized(lock) {
|
||||
|
||||
@@ -311,7 +311,7 @@ class MainActivity : BaseActivity() {
|
||||
LaunchedEffect(Unit) {
|
||||
if (updaterEnabled) {
|
||||
try {
|
||||
val result = AppUpdateChecker().checkForUpdate(context)
|
||||
val result = AppUpdateChecker().checkForUpdate()
|
||||
if (result is GetApplicationRelease.Result.NewUpdate) {
|
||||
val updateScreen = NewUpdateScreen(
|
||||
versionName = result.release.version,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package eu.kanade.tachiyomi.ui.more
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.AppUpdateDownloadJob
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
|
||||
class NewUpdateScreen(
|
||||
@@ -21,23 +23,30 @@ class NewUpdateScreen(
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val changelogInfoNoChecksum = remember {
|
||||
changelogInfo.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
|
||||
}
|
||||
val viewModel = viewModel<NewUpdateScreenModel>(
|
||||
factory = NewUpdateScreenModel.Factory,
|
||||
extras = CreationExtras {
|
||||
set(NewUpdateScreenModel.CHANGELOG_INFO_KEY, changelogInfo)
|
||||
set(NewUpdateScreenModel.DOWNLOAD_LINK_KEY, downloadLink)
|
||||
},
|
||||
)
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
NewUpdateScreen(
|
||||
versionName = versionName,
|
||||
changelogInfo = changelogInfoNoChecksum,
|
||||
stage = state.stage,
|
||||
downloadProgress = { state.downloadProgress },
|
||||
changelogInfo = state.changelogInfo,
|
||||
onOpenInBrowser = { context.openInBrowser(releaseLink) },
|
||||
onRejectUpdate = navigator::pop,
|
||||
onAcceptUpdate = {
|
||||
AppUpdateDownloadJob.start(
|
||||
context = context,
|
||||
url = downloadLink,
|
||||
title = versionName,
|
||||
)
|
||||
navigator.pop()
|
||||
when (state.stage) {
|
||||
NewUpdateScreenModel.Stage.Available, NewUpdateScreenModel.Stage.Failed -> viewModel.startDownload()
|
||||
NewUpdateScreenModel.Stage.Downloaded -> viewModel.installUpdate()
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
onRejectUpdate = navigator::pop,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package eu.kanade.tachiyomi.ui.more
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import androidx.work.WorkInfo
|
||||
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.viewmodel.StateViewModel
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class NewUpdateScreenModel(
|
||||
changelogInfo: String,
|
||||
private val downloadLink: String,
|
||||
private val context: Context = Injekt.get(),
|
||||
) : StateViewModel<NewUpdateScreenModel.State>(State(changelogInfo = changelogInfo)) {
|
||||
|
||||
init {
|
||||
context.workManager.getWorkInfosByTagFlow(AppUpdateDownloadJob.TAG)
|
||||
.mapNotNull { it.firstOrNull() }
|
||||
.map { workInfo ->
|
||||
val progress = if (workInfo.state.isFinished) {
|
||||
workInfo.outputData.getInt(AppUpdateDownloadJob.PROGRESS, 0)
|
||||
} else {
|
||||
workInfo.progress.getInt(AppUpdateDownloadJob.PROGRESS, 0)
|
||||
}
|
||||
val url = if (workInfo.state.isFinished) {
|
||||
workInfo.outputData.getString(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL)
|
||||
} else {
|
||||
workInfo.progress.getString(AppUpdateDownloadJob.EXTRA_DOWNLOAD_URL)
|
||||
}
|
||||
|
||||
if (url != downloadLink) {
|
||||
return@map 0 to Stage.Available
|
||||
}
|
||||
|
||||
val stage = when {
|
||||
workInfo.state == WorkInfo.State.FAILED -> Stage.Failed
|
||||
workInfo.state.isFinished && progress == 100 -> {
|
||||
if (AppUpdateDownloadJob.updateApk(context).exists()) {
|
||||
Stage.Downloaded
|
||||
} else {
|
||||
Stage.Available
|
||||
}
|
||||
}
|
||||
workInfo.state in listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING) -> Stage.Downloading
|
||||
else -> Stage.Available
|
||||
}
|
||||
progress to stage
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { (progress, stage) ->
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
downloadProgress = progress,
|
||||
stage = stage,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun startDownload() {
|
||||
mutableState.update { it.copy(downloadProgress = 0, stage = Stage.Downloading) }
|
||||
AppUpdateDownloadJob.start(context, downloadLink)
|
||||
}
|
||||
|
||||
fun installUpdate() {
|
||||
val apkFile = AppUpdateDownloadJob.updateApk(context)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(apkFile.getUriCompat(context), ExtensionInstaller.APK_MIME)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
AppUpdateDownloadJob.stop(context)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val changelogInfo: String,
|
||||
val downloadProgress: Int = 0,
|
||||
val stage: Stage = Stage.Available,
|
||||
)
|
||||
|
||||
enum class Stage {
|
||||
Available,
|
||||
Downloading,
|
||||
Downloaded,
|
||||
Failed,
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CHANGELOG_INFO_KEY = CreationExtras.Key<String>()
|
||||
val DOWNLOAD_LINK_KEY = CreationExtras.Key<String>()
|
||||
|
||||
val Factory = viewModelFactory {
|
||||
initializer {
|
||||
NewUpdateScreenModel(
|
||||
changelogInfo = get(CHANGELOG_INFO_KEY)!!,
|
||||
downloadLink = get(DOWNLOAD_LINK_KEY)!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user