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:
parent
ab371a6e50
commit
fe53d7b7fb
@ -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.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
@ -128,11 +132,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",
|
||||||
@ -387,6 +404,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(
|
||||||
@ -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 -> {
|
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()) {
|
||||||
@ -123,6 +125,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?) {
|
||||||
@ -198,6 +204,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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user