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.
This commit is contained in:
parent
3afcee81f4
commit
f0710df356
7 changed files with 97 additions and 42 deletions
|
@ -26,10 +26,10 @@ class BasePreferences(
|
||||||
|
|
||||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||||
|
|
||||||
enum class ExtensionInstaller(val titleRes: StringResource) {
|
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||||
LEGACY(MR.strings.ext_installer_legacy),
|
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
|
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
|
||||||
SHIZUKU(MR.strings.ext_installer_shizuku),
|
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||||
PRIVATE(MR.strings.ext_installer_private),
|
PRIVATE(MR.strings.ext_installer_private, false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.presentation.browse
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
@ -42,12 +43,15 @@ import androidx.compose.ui.unit.dp
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
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.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
|
@ -127,11 +131,24 @@ private fun ExtensionContent(
|
||||||
onOpenExtension: (Extension.Installed) -> Unit,
|
onOpenExtension: (Extension.Installed) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||||
|
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
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) ->
|
state.items.forEach { (header, items) ->
|
||||||
item(
|
item(
|
||||||
contentType = "header",
|
contentType = "header",
|
||||||
|
@ -384,6 +401,13 @@ private fun ExtensionItemActions(
|
||||||
installStep == InstallStep.Idle -> {
|
installStep == InstallStep.Idle -> {
|
||||||
when (extension) {
|
when (extension) {
|
||||||
is Extension.Installed -> {
|
is Extension.Installed -> {
|
||||||
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Settings,
|
||||||
|
contentDescription = stringResource(MR.strings.action_settings),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
if (extension.hasUpdate) {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
Icon(
|
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 -> {
|
is Extension.Untrusted -> {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
|
|
|
@ -11,8 +11,6 @@ import android.provider.Settings
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Column
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
@ -35,33 +33,29 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
internal class PermissionStep : OnboardingStep {
|
internal class PermissionStep : OnboardingStep {
|
||||||
|
|
||||||
private var installGranted by mutableStateOf(false)
|
|
||||||
private var notificationGranted by mutableStateOf(false)
|
private var notificationGranted by mutableStateOf(false)
|
||||||
private var batteryGranted by mutableStateOf(false)
|
private var batteryGranted by mutableStateOf(false)
|
||||||
|
|
||||||
override val isComplete: Boolean
|
override val isComplete: Boolean = true
|
||||||
get() = installGranted
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner.lifecycle) {
|
DisposableEffect(lifecycleOwner.lifecycle) {
|
||||||
val observer = object : DefaultLifecycleObserver {
|
val observer = object : DefaultLifecycleObserver {
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
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) {
|
notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
||||||
PackageManager.PERMISSION_GRANTED
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
@ -78,31 +72,16 @@ internal class PermissionStep : OnboardingStep {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column {
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
|
||||||
) {
|
|
||||||
SectionHeader(stringResource(MR.strings.onboarding_permission_type_required))
|
|
||||||
|
|
||||||
PermissionItem(
|
PermissionItem(
|
||||||
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
||||||
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
||||||
granted = installGranted,
|
granted = installGranted,
|
||||||
onButtonClick = {
|
onButtonClick = {
|
||||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
context.launchRequestPackageInstallsPermission()
|
||||||
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val permissionRequester = rememberLauncherForActivityResult(
|
val permissionRequester = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission(),
|
contract = ActivityResultContracts.RequestPermission(),
|
||||||
|
|
41
app/src/main/java/eu/kanade/presentation/util/Permissions.kt
Normal file
41
app/src/main/java/eu/kanade/presentation/util/Permissions.kt
Normal 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
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
|
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
|
||||||
|
@ -34,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class ExtensionsScreenModel(
|
class ExtensionsScreenModel(
|
||||||
preferences: SourcePreferences = Injekt.get(),
|
preferences: SourcePreferences = Injekt.get(),
|
||||||
|
basePreferences: BasePreferences = Injekt.get(),
|
||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
||||||
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
|
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
|
||||||
|
@ -124,6 +126,10 @@ class ExtensionsScreenModel(
|
||||||
preferences.extensionUpdatesCount().changes()
|
preferences.extensionUpdatesCount().changes()
|
||||||
.onEach { mutableState.update { state -> state.copy(updates = it) } }
|
.onEach { mutableState.update { state -> state.copy(updates = it) } }
|
||||||
.launchIn(screenModelScope)
|
.launchIn(screenModelScope)
|
||||||
|
|
||||||
|
basePreferences.extensionInstaller().changes()
|
||||||
|
.onEach { mutableState.update { state -> state.copy(installer = it) } }
|
||||||
|
.launchIn(screenModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String?) {
|
fun search(query: String?) {
|
||||||
|
@ -199,6 +205,7 @@ class ExtensionsScreenModel(
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val items: ItemGroups = mutableMapOf(),
|
val items: ItemGroups = mutableMapOf(),
|
||||||
val updates: Int = 0,
|
val updates: Int = 0,
|
||||||
|
val installer: BasePreferences.ExtensionInstaller? = null,
|
||||||
val searchQuery: String? = null,
|
val searchQuery: String? = null,
|
||||||
) {
|
) {
|
||||||
val isEmpty = items.isEmpty()
|
val isEmpty = items.isEmpty()
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
@ -167,3 +168,14 @@ fun Context.isInstalledFromFDroid(): Boolean {
|
||||||
// F-Droid builds typically disable the updater
|
// F-Droid builds typically disable the updater
|
||||||
(!BuildConfig.INCLUDE_UPDATER && !isDevFlavor)
|
(!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)
|
||||||
|
}
|
||||||
|
|
|
@ -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_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_action_select">Select a folder</string>
|
||||||
<string name="onboarding_storage_selection_required">A folder must be selected</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">Install apps permission</string>
|
||||||
<string name="onboarding_permission_install_apps_description">To install source extensions.</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">Notification permission</string>
|
||||||
|
@ -329,6 +327,7 @@
|
||||||
<string name="ext_info_age_rating">Age rating</string>
|
<string name="ext_info_age_rating">Age rating</string>
|
||||||
<string name="ext_nsfw_short">18+</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_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_install_service_notif">Installing extension…</string>
|
||||||
<string name="ext_installer_pref">Installer</string>
|
<string name="ext_installer_pref">Installer</string>
|
||||||
<string name="ext_installer_legacy">Legacy</string>
|
<string name="ext_installer_legacy">Legacy</string>
|
||||||
|
|
Reference in a new issue