Browse Source

Don't make install permission required during onboarding

Closes #10257

We show a warning banner in the extensions list and also rely on the system
alert popup if someone attempts to install without the permission already
granted.
arkon 1 year ago
parent
commit
f0710df356

+ 5 - 5
app/src/main/java/eu/kanade/domain/base/BasePreferences.kt

@@ -26,10 +26,10 @@ class BasePreferences(
 
     fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
 
-    enum class ExtensionInstaller(val titleRes: StringResource) {
-        LEGACY(MR.strings.ext_installer_legacy),
-        PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
-        SHIZUKU(MR.strings.ext_installer_shizuku),
-        PRIVATE(MR.strings.ext_installer_private),
+    enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
+        LEGACY(MR.strings.ext_installer_legacy, true),
+        PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
+        SHIZUKU(MR.strings.ext_installer_shizuku, false),
+        PRIVATE(MR.strings.ext_installer_private, false),
     }
 }

+ 24 - 7
app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt

@@ -1,6 +1,7 @@
 package eu.kanade.presentation.browse
 
 import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -42,12 +43,15 @@ import androidx.compose.ui.unit.dp
 import dev.icerock.moko.resources.StringResource
 import eu.kanade.presentation.browse.components.BaseBrowseItem
 import eu.kanade.presentation.browse.components.ExtensionIcon
+import eu.kanade.presentation.components.WarningBanner
 import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
+import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
 import eu.kanade.tachiyomi.extension.model.Extension
 import eu.kanade.tachiyomi.extension.model.InstallStep
 import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
 import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
 import eu.kanade.tachiyomi.util.system.LocaleHelper
+import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
 import tachiyomi.i18n.MR
 import tachiyomi.presentation.core.components.FastScrollLazyColumn
 import tachiyomi.presentation.core.components.material.PullRefresh
@@ -127,11 +131,24 @@ private fun ExtensionContent(
     onOpenExtension: (Extension.Installed) -> Unit,
     onClickUpdateAll: () -> Unit,
 ) {
+    val context = LocalContext.current
     var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
+    val installGranted = rememberRequestPackageInstallsPermissionState()
 
     FastScrollLazyColumn(
         contentPadding = contentPadding + topSmallPaddingValues,
     ) {
+        if (!installGranted && state.installer?.requiresSystemPermission == true) {
+            item {
+                WarningBanner(
+                    textRes = MR.strings.ext_permission_install_apps_warning,
+                    modifier = Modifier.clickable {
+                        context.launchRequestPackageInstallsPermission()
+                    },
+                )
+            }
+        }
+
         state.items.forEach { (header, items) ->
             item(
                 contentType = "header",
@@ -384,6 +401,13 @@ private fun ExtensionItemActions(
             installStep == InstallStep.Idle -> {
                 when (extension) {
                     is Extension.Installed -> {
+                        IconButton(onClick = { onClickItemAction(extension) }) {
+                            Icon(
+                                imageVector = Icons.Outlined.Settings,
+                                contentDescription = stringResource(MR.strings.action_settings),
+                            )
+                        }
+
                         if (extension.hasUpdate) {
                             IconButton(onClick = { onClickItemAction(extension) }) {
                                 Icon(
@@ -392,13 +416,6 @@ private fun ExtensionItemActions(
                                 )
                             }
                         }
-
-                        IconButton(onClick = { onClickItemAction(extension) }) {
-                            Icon(
-                                imageVector = Icons.Outlined.Settings,
-                                contentDescription = stringResource(MR.strings.action_settings),
-                            )
-                        }
                     }
                     is Extension.Untrusted -> {
                         IconButton(onClick = { onClickItemAction(extension) }) {

+ 7 - 28
app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt

@@ -11,8 +11,6 @@ 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
@@ -35,33 +33,29 @@ import androidx.compose.ui.unit.dp
 import androidx.core.content.getSystemService
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
+import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
+import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
 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
+    override val isComplete: Boolean = true
 
     @Composable
     override fun Content() {
         val context = LocalContext.current
         val lifecycleOwner = LocalLifecycleOwner.current
 
+        val installGranted = rememberRequestPackageInstallsPermissionState()
+
         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
@@ -78,31 +72,16 @@ internal class PermissionStep : OnboardingStep {
             }
         }
 
-        Column(
-            modifier = Modifier.padding(vertical = 16.dp),
-        ) {
-            SectionHeader(stringResource(MR.strings.onboarding_permission_type_required))
-
+        Column {
             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)
+                    context.launchRequestPackageInstallsPermission()
                 },
             )
 
-            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(),

+ 41 - 0
app/src/main/java/eu/kanade/presentation/util/Permissions.kt

@@ -0,0 +1,41 @@
+package eu.kanade.presentation.util
+
+import android.os.Build
+import android.provider.Settings
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+
+@Composable
+fun rememberRequestPackageInstallsPermissionState(): Boolean {
+    val context = LocalContext.current
+    val lifecycleOwner = LocalLifecycleOwner.current
+
+    var installGranted by remember { mutableStateOf(false) }
+
+    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
+                }
+            }
+        }
+        lifecycleOwner.lifecycle.addObserver(observer)
+        onDispose {
+            lifecycleOwner.lifecycle.removeObserver(observer)
+        }
+    }
+
+    return installGranted
+}

+ 7 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt

@@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
 import cafe.adriel.voyager.core.model.StateScreenModel
 import cafe.adriel.voyager.core.model.screenModelScope
 import dev.icerock.moko.resources.StringResource
+import eu.kanade.domain.base.BasePreferences
 import eu.kanade.domain.extension.interactor.GetExtensionsByType
 import eu.kanade.domain.source.service.SourcePreferences
 import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
@@ -34,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds
 
 class ExtensionsScreenModel(
     preferences: SourcePreferences = Injekt.get(),
+    basePreferences: BasePreferences = Injekt.get(),
     private val extensionManager: ExtensionManager = Injekt.get(),
     private val getExtensions: GetExtensionsByType = Injekt.get(),
 ) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
@@ -124,6 +126,10 @@ class ExtensionsScreenModel(
         preferences.extensionUpdatesCount().changes()
             .onEach { mutableState.update { state -> state.copy(updates = it) } }
             .launchIn(screenModelScope)
+
+        basePreferences.extensionInstaller().changes()
+            .onEach { mutableState.update { state -> state.copy(installer = it) } }
+            .launchIn(screenModelScope)
     }
 
     fun search(query: String?) {
@@ -199,6 +205,7 @@ class ExtensionsScreenModel(
         val isRefreshing: Boolean = false,
         val items: ItemGroups = mutableMapOf(),
         val updates: Int = 0,
+        val installer: BasePreferences.ExtensionInstaller? = null,
         val searchQuery: String? = null,
     ) {
         val isEmpty = items.isEmpty()

+ 12 - 0
app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt

@@ -9,6 +9,7 @@ import android.content.res.Configuration
 import android.net.Uri
 import android.os.Build
 import android.os.PowerManager
+import android.provider.Settings
 import androidx.appcompat.view.ContextThemeWrapper
 import androidx.core.content.getSystemService
 import androidx.core.net.toUri
@@ -167,3 +168,14 @@ fun Context.isInstalledFromFDroid(): Boolean {
         // F-Droid builds typically disable the updater
         (!BuildConfig.INCLUDE_UPDATER && !isDevFlavor)
 }
+
+fun Context.launchRequestPackageInstallsPermission() {
+    val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
+            data = Uri.parse("package:$packageName")
+        }
+    } else {
+        Intent(Settings.ACTION_SECURITY_SETTINGS)
+    }
+    startActivity(intent)
+}

+ 1 - 2
i18n/src/commonMain/resources/MR/base/strings.xml

@@ -183,8 +183,6 @@
     <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>
@@ -329,6 +327,7 @@
     <string name="ext_info_age_rating">Age rating</string>
     <string name="ext_nsfw_short">18+</string>
     <string name="ext_nsfw_warning">Sources from this extension may contain NSFW (18+) content</string>
+    <string name="ext_permission_install_apps_warning">Permissions are needed to install extensions. Tap here to grant.</string>
     <string name="ext_install_service_notif">Installing extension…</string>
     <string name="ext_installer_pref">Installer</string>
     <string name="ext_installer_legacy">Legacy</string>