Browse Source

Target Android 14 (SDK 34) and add permission onboarding step

(cherry picked from commit 9e0068715f3ba3d1627c4b7539b90fb782f8122f)
Ivan Iskandar 1 year ago
parent
commit
13b3bec8ad

+ 1 - 1
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),
         )
     }

+ 181 - 0
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<PowerManager>()!!
+                        .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),
+        )
+    }
+}

+ 1 - 1
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"
 }

+ 9 - 0
i18n/src/commonMain/resources/MR/base/strings.xml

@@ -183,6 +183,15 @@
     <string name="onboarding_storage_info">Select a folder where %1$s will store chapter downloads, backups, and more.\n\nA dedicated folder is recommended.\n\nSelected folder: %2$s</string>
     <string name="onboarding_storage_action_select">Select a folder</string>
     <string name="onboarding_storage_selection_required">A folder must be selected</string>
+    <string name="onboarding_permission_type_required">Required</string>
+    <string name="onboarding_permission_type_optional">Optional</string>
+    <string name="onboarding_permission_install_apps">Install apps permission</string>
+    <string name="onboarding_permission_install_apps_description">To install source extensions.</string>
+    <string name="onboarding_permission_notifications">Notification permission</string>
+    <string name="onboarding_permission_notifications_description">Get notified for library updates and more.</string>
+    <string name="onboarding_permission_ignore_battery_opts">Background battery usage</string>
+    <string name="onboarding_permission_ignore_battery_opts_description">Avoid interruptions to long-running library updates, downloads, and backup restores.</string>
+    <string name="onboarding_permission_action_grant">Grant</string>
     <string name="onboarding_guides_new_user">New to %s? We recommend checking out the getting started guide.</string>
     <string name="onboarding_guides_returning_user">Already used %s before?</string>