diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt index c7a3d85866..c5fd8c2faf 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/OnboardingScreen.kt @@ -36,7 +36,7 @@ fun OnboardingScreen( listOf( ThemeStep(), StorageStep(), - // TODO: prompt for notification permissions when bumping target to Android 13 + PermissionStep(), GuidesStep(onRestoreBackup = onRestoreBackup), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt new file mode 100644 index 0000000000..e7e3ec598f --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt @@ -0,0 +1,181 @@ +package eu.kanade.presentation.more.onboarding + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +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.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp +import androidx.core.content.getSystemService +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.secondaryItemAlpha + +internal class PermissionStep : OnboardingStep { + + private var installGranted by mutableStateOf(false) + private var notificationGranted by mutableStateOf(false) + private var batteryGranted by mutableStateOf(false) + + override val isComplete: Boolean + get() = installGranted + + @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 + } + 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) + } + } + + Column( + modifier = Modifier.padding(vertical = 16.dp), + ) { + SectionHeader(stringResource(MR.strings.onboarding_permission_type_required)) + + PermissionItem( + title = stringResource(MR.strings.onboarding_permission_install_apps), + subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description), + granted = installGranted, + onButtonClick = { + val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { + data = Uri.parse("package:${context.packageName}") + } + } else { + Intent(Settings.ACTION_SECURITY_SETTINGS) + } + context.startActivity(intent) + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SectionHeader(stringResource(MR.strings.onboarding_permission_type_optional)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val permissionRequester = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { + // no-op. resulting checks is being done on resume + }, + ) + PermissionItem( + title = stringResource(MR.strings.onboarding_permission_notifications), + subtitle = stringResource(MR.strings.onboarding_permission_notifications_description), + granted = notificationGranted, + onButtonClick = { permissionRequester.launch(Manifest.permission.POST_NOTIFICATIONS) }, + ) + } + + PermissionItem( + title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts), + subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description), + granted = batteryGranted, + onButtonClick = { + @SuppressLint("BatteryLife") + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${context.packageName}") + } + context.startActivity(intent) + }, + ) + } + } + + @Composable + private fun SectionHeader( + text: String, + modifier: Modifier = Modifier, + ) { + Text( + text = text, + style = MaterialTheme.typography.titleLarge, + modifier = modifier + .padding(horizontal = 16.dp) + .secondaryItemAlpha(), + ) + } + + @Composable + private fun PermissionItem( + title: String, + subtitle: String, + granted: Boolean, + modifier: Modifier = Modifier, + onButtonClick: () -> Unit, + ) { + ListItem( + modifier = modifier, + headlineContent = { Text(text = title) }, + supportingContent = { Text(text = subtitle) }, + trailingContent = { + OutlinedButton( + enabled = !granted, + onClick = onButtonClick, + ) { + if (granted) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } else { + Text(stringResource(MR.strings.onboarding_permission_action_grant)) + } + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } +} diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index 6d4d312707..7a3d388338 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -1,6 +1,6 @@ object AndroidConfig { const val compileSdk = 34 const val minSdk = 23 - const val targetSdk = 32 + const val targetSdk = 34 const val ndk = "22.1.7171670" } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 1714d16c45..5711a53ccc 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -183,6 +183,15 @@ Select a folder where %1$s will store chapter downloads, backups, and more.\n\nA dedicated folder is recommended.\n\nSelected folder: %2$s Select a folder A folder must be selected + Required + Optional + Install apps permission + To install source extensions. + Notification permission + Get notified for library updates and more. + Background battery usage + Avoid interruptions to long-running library updates, downloads, and backup restores. + Grant New to %s? We recommend checking out the getting started guide. Already used %s before?