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.

(cherry picked from commit f0710df35696c1f6cf7bb5371dfd6ad91d53fae1)
This commit is contained in:
arkon 2023-12-28 15:21:42 -05:00 committed by Jobobby04
parent ab371a6e50
commit fe53d7b7fb
7 changed files with 97 additions and 42 deletions

View File

@ -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),
}
}

View File

@ -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.i18n.sy.SYMR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
@ -128,11 +132,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",
@ -387,6 +404,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(
@ -395,13 +419,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) }) {

View File

@ -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(),

View File

@ -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
}

View File

@ -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()) {
@ -123,6 +125,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?) {
@ -198,6 +204,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()

View File

@ -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)
}

View File

@ -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>