From e06b28a60e5ac77bf023116e4a1a315f782bca8f Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 3 Jan 2025 09:26:49 +0700 Subject: [PATCH] fix: Handle version check for AboutController --- .../kanade/tachiyomi/ui/main/MainActivity.kt | 7 +-- .../onboarding/steps/PermissionStep.kt | 56 +++++++----------- .../settings/screen/about/AboutDialogs.kt | 17 ++++++ .../settings/screen/about/AboutScreen.kt | 59 +++++++++++++++---- 4 files changed, 90 insertions(+), 49 deletions(-) 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 53cfe06c78..cc0ecb6cc5 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 @@ -117,6 +117,7 @@ import eu.kanade.tachiyomi.util.system.prepareSideNavContext import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.BackHandlerControllerInterface import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.blurBehindWindow @@ -136,12 +137,10 @@ import kotlin.collections.set import kotlin.math.abs import kotlin.math.min import kotlin.math.roundToLong -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.withContext import uy.kohesive.injekt.injectLazy import yokai.core.migration.Migrator import yokai.domain.base.BasePreferences @@ -198,6 +197,7 @@ open class MainActivity : BaseActivity() { dimenW to dimenH } + @Deprecated("Create contract directly from Composable") private val requestNotificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (!isGranted) { @@ -1001,7 +1001,6 @@ open class MainActivity : BaseActivity() { } private fun checkForAppUpdates() { - // FIXME: Show Compose version of NewUpdateDialog for AboutController if (isUpdaterEnabled && router.backstack.lastOrNull()?.controller !is AboutController) { lifecycleScope.launchIO { try { @@ -1012,7 +1011,7 @@ open class MainActivity : BaseActivity() { val isBeta = result.release.preRelease == true // Create confirmation window - withContext(Dispatchers.Main) { + withUIContext { showNotificationPermissionPrompt() AppUpdateNotifier.releasePageUrl = result.release.releaseLink AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router) diff --git a/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt b/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt index 2f4aa9c3a5..27596046f2 100644 --- a/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt +++ b/app/src/main/java/yokai/presentation/onboarding/steps/PermissionStep.kt @@ -21,23 +21,18 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.core.content.getSystemService -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LocalLifecycleOwner -import eu.kanade.tachiyomi.R +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.util.system.isShizukuInstalled import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource import yokai.presentation.component.Gap import yokai.presentation.theme.Size @@ -53,35 +48,26 @@ internal class PermissionStep : OnboardingStep { @Composable override fun Content() { val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner.lifecycle) { - val observer = object : DefaultLifecycleObserver { - override fun onResume(owner: LifecycleOwner) { - installGranted = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.packageManager.canRequestPackageInstalls() - } else { - @Suppress("DEPRECATION") - Settings.Secure.getInt( - context.contentResolver, - Settings.Secure.INSTALL_NON_MARKET_APPS - ) != 0 - } || context.isShizukuInstalled - notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED - } else { - true - } - batteryGranted = context.getSystemService()!! - .isIgnoringBatteryOptimizations(context.packageName) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + installGranted = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.packageManager.canRequestPackageInstalls() + } else { + @Suppress("DEPRECATION") + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.INSTALL_NON_MARKET_APPS + ) != 0 + } || context.isShizukuInstalled + notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + } else { + true } + batteryGranted = context.getSystemService()!! + .isIgnoringBatteryOptimizations(context.packageName) } Column( diff --git a/app/src/main/java/yokai/presentation/settings/screen/about/AboutDialogs.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutDialogs.kt index 58d2c2895d..8fb10c81a8 100644 --- a/app/src/main/java/yokai/presentation/settings/screen/about/AboutDialogs.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutDialogs.kt @@ -10,14 +10,17 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import com.google.android.material.textview.MaterialTextView import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob import eu.kanade.tachiyomi.ui.more.parseReleaseNotes import java.io.Serializable +import kotlin.coroutines.resume import yokai.domain.DialogHostState import yokai.i18n.MR +import android.R as AR data class NewUpdateData( val body: String, @@ -89,3 +92,17 @@ private fun MarkdownText(text: String) { }, ) } + +suspend fun DialogHostState.awaitNotificationPermissionDeniedDialog(): Unit = dialog { cont -> + // cont.resume(Unit) so that new update dialog will be shown next + AlertDialog( + onDismissRequest = { if (cont.isActive) cont.resume(Unit) }, + title = { Text(text = stringResource(MR.strings.warning)) }, + text = { Text(text = stringResource(MR.strings.allow_notifications_recommended)) }, + confirmButton = { + TextButton(onClick = { if (cont.isActive) cont.resume(Unit) }) { + Text(text = stringResource(AR.string.ok)) + } + }, + ) +} diff --git a/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt b/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt index a541e73609..b120337de1 100644 --- a/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt +++ b/app/src/main/java/yokai/presentation/settings/screen/about/AboutScreen.kt @@ -4,6 +4,8 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow @@ -25,8 +27,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect import cafe.adriel.voyager.navigator.LocalNavigator import co.touchlab.kermit.Logger +import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.core.storage.preference.asDateFormat @@ -39,7 +44,9 @@ import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.compose.LocalDialogHostState import eu.kanade.tachiyomi.util.compose.currentOrThrow import eu.kanade.tachiyomi.util.lang.toTimestampString +import eu.kanade.tachiyomi.util.showNotificationPermissionPrompt import eu.kanade.tachiyomi.util.system.isOnline +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.localeContext import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.withUIContext @@ -49,7 +56,8 @@ import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone import kotlinx.coroutines.launch -import uy.kohesive.injekt.injectLazy +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import yokai.domain.DialogHostState import yokai.i18n.MR import yokai.presentation.component.preference.widget.TextPreferenceWidget @@ -70,11 +78,34 @@ class AboutScreen : Screen() { val dialogHostState = LocalDialogHostState.currentOrThrow val uriHandler = LocalUriHandler.current + val preferences = remember { Injekt.get() } + val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val listState = rememberLazyListState() - val preferences: PreferencesHelper by injectLazy() + val requestNotificationPermission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (!isGranted) { + scope.launch { dialogHostState.awaitNotificationPermissionDeniedDialog() } + } + } + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + // FIXME: Move this to MainActivity once the app is fully migrated to Compose + scope.launchIO { + context.checkVersion( + dialogState = dialogHostState, + isUserPrompt = false, + notificationPrompt = { + context.showNotificationPermissionPrompt( + requestNotificationPermission, + false, + preferences, + ) + } + ) + } + } + val dateFormat by lazy { preferences.dateFormatRaw().get().asDateFormat() } SettingsScaffold( @@ -108,7 +139,7 @@ class AboutScreen : Screen() { onPreferenceClick = { if (context.isOnline()) { scope.launch { - context.checkVersion(dialogHostState) + context.checkVersion(dialogHostState, true) } } else { context.toast(MR.strings.no_network_connection) @@ -192,16 +223,25 @@ class AboutScreen : Screen() { else -> "Release ${BuildConfig.VERSION_NAME}" } - private suspend fun Context.checkVersion(dialogState: DialogHostState) { + private fun Context.toastIfNotUserPrompt(message: StringResource, isUserPrompt: Boolean) { + toastIfNotUserPrompt(getString(message), isUserPrompt) + } + + private fun Context.toastIfNotUserPrompt(message: String?, isUserPrompt: Boolean) { + if (!isUserPrompt) return + toast(message) + } + + private suspend fun Context.checkVersion(dialogState: DialogHostState, isUserPrompt: Boolean, notificationPrompt: () -> Unit = {}) { val updateChecker = AppUpdateChecker() - withUIContext { toast(MR.strings.searching_for_updates) } + withUIContext { toastIfNotUserPrompt(MR.strings.searching_for_updates, isUserPrompt) } val result = try { - updateChecker.checkForUpdate(this, true) + updateChecker.checkForUpdate(this, isUserPrompt) } catch (error: Exception) { withUIContext { - toast(error.message) + toastIfNotUserPrompt(error.message, isUserPrompt) Logger.e(error) { "Couldn't check new update" } } } @@ -215,14 +255,13 @@ class AboutScreen : Screen() { // Create confirmation window withUIContext { + if (!isUserPrompt) { notificationPrompt() } AppUpdateNotifier.releasePageUrl = result.release.releaseLink dialogState.awaitNewUpdateDialog(data) } } is AppUpdateResult.NoNewUpdate -> { - withUIContext { - toast(MR.strings.no_new_updates_available) - } + withUIContext { toastIfNotUserPrompt(MR.strings.no_new_updates_available, isUserPrompt) } } } }