mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	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:
		@@ -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),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) }) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
								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.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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user