diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e0644282..1ab38d1d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Add comparison operators (`>`, `>=`, `<`, `<=`, `=`) for `id`, `added`, `fetchinterval` / `fi`, `nextupdate` / `nu`, `unread`, `read`, and `total` (Uses `yyyy-MM-dd` dates for `added` and `nextupdate`) - Support field-specific prefixes (`title`, `author`, `artist`, `description` / `desc`, `genre` / `tag`, `source` / `src`, `source_id` / `src_id`, `notes` / `note`, `language` / `lang`) and empty field matching (`""`, `''`) - Always use Private installer to update privately installed extensions ([@MajorTanya](https://github.com/MajorTanya)) ([#3614](https://github.com/mihonapp/mihon/pull/3614)) +- App update prompt flow experience ([@AntsyLich](https://github.com/AntsyLich)) ([#3669](https://github.com/mihonapp/mihon/pull/3669)) ### Fixed - Fixed app bars remaining visible after changing pages by tapping in the paged reader after using the chapter navigator slider ([@AntsyLich](https://github.com/AntsyLich)) ([#3567](https://github.com/mihonapp/mihon/pull/3567)) diff --git a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt index f6b5f4b0b..9dcac9e15 100644 --- a/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/NewUpdateScreen.kt @@ -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 = {}, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt index 01e35c1ec..01b203917 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt @@ -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) } 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 e23b3f78c..7689f471f 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 @@ -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. * 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 cdc540eec..a0664f37f 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 @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt index c6781277e..c62e40f5c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt index c1aa46626..ee19e8942 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateDownloadJob.kt @@ -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() .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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt deleted file mode 100644 index 466ba693e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 3a7d6bf7a..ed3f940d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -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(app) addSingletonFactory { synchronized(lock) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index ab1fb6713..ffdfc30aa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt index eae88d838..5fa273f24 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreen.kt @@ -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( + 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, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreenModel.kt new file mode 100644 index 000000000..beb77f252 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/NewUpdateScreenModel.kt @@ -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(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() + val DOWNLOAD_LINK_KEY = CreationExtras.Key() + + val Factory = viewModelFactory { + initializer { + NewUpdateScreenModel( + changelogInfo = get(CHANGELOG_INFO_KEY)!!, + downloadLink = get(DOWNLOAD_LINK_KEY)!!, + ) + } + } + } +} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index bf398a7c4..a713612ab 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -362,6 +362,7 @@ Shizuku is not running Install and start Shizuku to use Shizuku as extension installer. Revoke trusted unknown extensions + Downloading… (%d%%) Open source repo @@ -949,8 +950,6 @@ Downloading… - Tap to install update - Download error New version available!