fix permission poups and add settings to see permission state
This commit is contained in:
@@ -15,12 +15,36 @@ object RootManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun hasRoot(): Boolean = withContext(Dispatchers.IO) {
|
/**
|
||||||
Shell.isAppGrantedRoot() == true
|
* Check if root was previously granted (cached status, no popup).
|
||||||
|
* Returns true if granted, false if denied, null if unknown.
|
||||||
|
*/
|
||||||
|
fun isRootGrantedCached(): Boolean? {
|
||||||
|
return Shell.isAppGrantedRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun requestRoot(): Boolean = withContext(Dispatchers.IO) {
|
/**
|
||||||
|
* Check if root is available. Will trigger Magisk popup if not previously decided.
|
||||||
|
*/
|
||||||
|
suspend fun hasRoot(): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// getShell() initializes the shell and checks root status with Magisk
|
||||||
|
// If already granted, Magisk will auto-approve silently
|
||||||
Shell.getShell().isRoot
|
Shell.getShell().isRoot
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request root access. Will trigger Magisk popup.
|
||||||
|
*/
|
||||||
|
suspend fun requestRoot(): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Shell.getShell().isRoot
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun executeCommand(command: String): CommandResult = withContext(Dispatchers.IO) {
|
suspend fun executeCommand(command: String): CommandResult = withContext(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package sh.sar.isodroid.ui.screens
|
package sh.sar.isodroid.ui.screens
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -10,14 +14,19 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.Code
|
import androidx.compose.material.icons.filled.Code
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
import androidx.compose.material.icons.filled.Folder
|
import androidx.compose.material.icons.filled.Folder
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.Security
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -40,9 +49,13 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import sh.sar.isodroid.viewmodel.MainViewModel
|
import sh.sar.isodroid.viewmodel.MainViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -53,9 +66,46 @@ fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val activity = context as? androidx.activity.ComponentActivity
|
||||||
var showPathDialog by remember { mutableStateOf(false) }
|
var showPathDialog by remember { mutableStateOf(false) }
|
||||||
var tempPath by remember(uiState.currentPath) { mutableStateOf(uiState.isoDirectory) }
|
var tempPath by remember(uiState.currentPath) { mutableStateOf(uiState.isoDirectory) }
|
||||||
|
|
||||||
|
// Track notification permission with lifecycle-aware refresh
|
||||||
|
var hasNotificationPermission by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-check permissions when returning to the app
|
||||||
|
if (activity != null) {
|
||||||
|
androidx.compose.runtime.DisposableEffect(activity) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
// Re-check notification permission
|
||||||
|
hasNotificationPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else true
|
||||||
|
|
||||||
|
// Re-check root status (in case granted via Magisk)
|
||||||
|
viewModel.refreshRootStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity.lifecycle.addObserver(observer)
|
||||||
|
onDispose {
|
||||||
|
activity.lifecycle.removeObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun openUrl(url: String) {
|
fun openUrl(url: String) {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
@@ -98,6 +148,61 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||||
|
|
||||||
|
// Permissions section
|
||||||
|
SectionHeader(title = "Permissions")
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Root Access status
|
||||||
|
val hasRoot = uiState.hasRoot
|
||||||
|
val rootDenied = uiState.rootDenied
|
||||||
|
PermissionStatusItem(
|
||||||
|
icon = Icons.Default.Security,
|
||||||
|
title = "Root Access",
|
||||||
|
granted = hasRoot,
|
||||||
|
grantedText = "Granted",
|
||||||
|
deniedText = if (rootDenied) {
|
||||||
|
"Denied. Open your root manager (Magisk/KernelSU) to grant access."
|
||||||
|
} else {
|
||||||
|
"Not granted. Tap to request root access."
|
||||||
|
},
|
||||||
|
onClick = if (!hasRoot && !rootDenied) {
|
||||||
|
{ viewModel.requestRootAccess() }
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp))
|
||||||
|
|
||||||
|
// Notification permission status (Android 13+ only)
|
||||||
|
PermissionStatusItem(
|
||||||
|
icon = Icons.Default.Notifications,
|
||||||
|
title = "Notifications",
|
||||||
|
granted = hasNotificationPermission,
|
||||||
|
grantedText = "Enabled",
|
||||||
|
deniedText = "Disabled. Tap to open system settings and enable notifications.",
|
||||||
|
onClick = if (!hasNotificationPermission) {
|
||||||
|
{
|
||||||
|
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||||
|
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||||
|
|
||||||
// About section
|
// About section
|
||||||
SectionHeader(title = "About")
|
SectionHeader(title = "About")
|
||||||
|
|
||||||
@@ -312,7 +417,7 @@ private fun SectionHeader(title: String) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsItem(
|
private fun SettingsItem(
|
||||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
icon: ImageVector,
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
@@ -343,3 +448,53 @@ private fun SettingsItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PermissionStatusItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
granted: Boolean,
|
||||||
|
grantedText: String,
|
||||||
|
deniedText: String,
|
||||||
|
onClick: (() -> Unit)?
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (onClick != null) Modifier.clickable(onClick = onClick)
|
||||||
|
else Modifier
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = if (granted) Icons.Default.CheckCircle else Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (granted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = if (granted) grantedText else deniedText,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (granted) MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,14 +130,25 @@ private fun RootAccessStep(
|
|||||||
onNext: () -> Unit,
|
onNext: () -> Unit,
|
||||||
onSkip: () -> Unit
|
onSkip: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var hasRoot by remember { mutableStateOf<Boolean?>(null) }
|
var hasRoot by remember { mutableStateOf<Boolean?>(null) }
|
||||||
var isChecking by remember { mutableStateOf(false) }
|
var isChecking by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun saveRootGranted(granted: Boolean) {
|
||||||
|
context.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean("root_granted", granted)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
PermissionCard(
|
PermissionCard(
|
||||||
icon = Icons.Default.Security,
|
icon = Icons.Default.Security,
|
||||||
title = "Root Access",
|
title = "Root Access",
|
||||||
description = "ISO Drive needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation.",
|
description = when (hasRoot) {
|
||||||
|
false -> "Root access was denied. The app requires root to mount ISO files. You can try again or skip and grant access later from your root manager."
|
||||||
|
else -> "ISO Drive needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation."
|
||||||
|
},
|
||||||
granted = hasRoot
|
granted = hasRoot
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -150,12 +161,27 @@ private fun RootAccessStep(
|
|||||||
) {
|
) {
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
}
|
}
|
||||||
|
} else if (hasRoot == false) {
|
||||||
|
// Magisk won't show the dialog again after denial
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
saveRootGranted(false)
|
||||||
|
onSkip()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Continue Without Root")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
isChecking = true
|
isChecking = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
hasRoot = RootManager.requestRoot()
|
val granted = RootManager.requestRoot()
|
||||||
|
hasRoot = granted
|
||||||
|
if (granted) {
|
||||||
|
saveRootGranted(true)
|
||||||
|
}
|
||||||
isChecking = false
|
isChecking = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -168,7 +194,10 @@ private fun RootAccessStep(
|
|||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onSkip,
|
onClick = {
|
||||||
|
saveRootGranted(false)
|
||||||
|
onSkip()
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text("Skip for now")
|
Text("Skip for now")
|
||||||
@@ -182,6 +211,7 @@ private fun NotificationStep(
|
|||||||
onSkip: () -> Unit
|
onSkip: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val activity = context as? android.app.Activity
|
||||||
|
|
||||||
// Check if we need to show this step at all (only Android 13+)
|
// Check if we need to show this step at all (only Android 13+)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -198,18 +228,34 @@ private fun NotificationStep(
|
|||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
var wasRequested by remember { mutableStateOf(false) }
|
||||||
|
var canRequestAgain by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
val wasDenied = wasRequested && !hasPermission
|
||||||
|
val permanentlyDenied = wasDenied && !canRequestAgain
|
||||||
|
|
||||||
val permissionLauncher = rememberLauncherForActivityResult(
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission()
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
) { granted ->
|
) { granted ->
|
||||||
hasPermission = granted
|
hasPermission = granted
|
||||||
|
wasRequested = true
|
||||||
|
// Check if Android will show the permission dialog again
|
||||||
|
if (!granted && activity != null) {
|
||||||
|
canRequestAgain = androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale(
|
||||||
|
activity, Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PermissionCard(
|
PermissionCard(
|
||||||
icon = Icons.Default.Notifications,
|
icon = Icons.Default.Notifications,
|
||||||
title = "Notifications",
|
title = "Notifications",
|
||||||
description = "ISO Drive shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status.",
|
description = when {
|
||||||
granted = hasPermission
|
permanentlyDenied -> "Notification permission was denied. You can enable it later in system settings if you change your mind."
|
||||||
|
wasDenied -> "Notification permission was denied. You can try again or continue without notifications."
|
||||||
|
else -> "ISO Drive shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status."
|
||||||
|
},
|
||||||
|
granted = if (wasDenied) false else if (hasPermission) true else null
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
@@ -221,6 +267,14 @@ private fun NotificationStep(
|
|||||||
) {
|
) {
|
||||||
Text("Continue")
|
Text("Continue")
|
||||||
}
|
}
|
||||||
|
} else if (permanentlyDenied) {
|
||||||
|
// Android won't show the popup again
|
||||||
|
Button(
|
||||||
|
onClick = onSkip,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Continue Without Notifications")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -228,7 +282,7 @@ private fun NotificationStep(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text("Enable Notifications")
|
Text(if (wasDenied) "Try Again" else "Enable Notifications")
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
@@ -237,7 +291,7 @@ private fun NotificationStep(
|
|||||||
onClick = onSkip,
|
onClick = onSkip,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text("Skip for now")
|
Text(if (wasDenied) "Continue Without Notifications" else "Skip for now")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,26 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
_uiState.update { it.copy(isoDirectory = savedDirectory, currentPath = savedDirectory) }
|
_uiState.update { it.copy(isoDirectory = savedDirectory, currentPath = savedDirectory) }
|
||||||
navigationStack.add(savedDirectory)
|
navigationStack.add(savedDirectory)
|
||||||
|
|
||||||
// Check root access (don't request, just check - wizard handles requesting)
|
// First check cached root status (no popup) - user may have granted in Magisk settings
|
||||||
val hasRoot = RootManager.hasRoot()
|
val cachedRootStatus = RootManager.isRootGrantedCached()
|
||||||
|
|
||||||
|
val hasRoot = when {
|
||||||
|
// If cached status says granted, root is available
|
||||||
|
cachedRootStatus == true -> true
|
||||||
|
// If cached status says denied, check if user wants to retry
|
||||||
|
cachedRootStatus == false -> false
|
||||||
|
// If unknown, check preference from wizard
|
||||||
|
else -> {
|
||||||
|
val prefs = getApplication<android.app.Application>()
|
||||||
|
.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE)
|
||||||
|
val rootGrantedInWizard = prefs.getBoolean("root_granted", false)
|
||||||
|
if (rootGrantedInWizard) {
|
||||||
|
RootManager.hasRoot()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_uiState.update { it.copy(hasRoot = hasRoot) }
|
_uiState.update { it.copy(hasRoot = hasRoot) }
|
||||||
|
|
||||||
if (hasRoot) {
|
if (hasRoot) {
|
||||||
@@ -267,6 +285,72 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return navigationStack.size > 1
|
return navigationStack.size > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestRootAccess() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val granted = RootManager.requestRoot()
|
||||||
|
if (granted) {
|
||||||
|
// Save to preferences
|
||||||
|
getApplication<android.app.Application>()
|
||||||
|
.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean("root_granted", true)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
// Re-initialize with root
|
||||||
|
_uiState.update { it.copy(hasRoot = true, rootDenied = false) }
|
||||||
|
isoDriveManager.initialize()
|
||||||
|
|
||||||
|
val supportStatus = isoDriveManager.isSupported()
|
||||||
|
val isSupported = supportStatus == SupportStatus.CONFIGFS_SUPPORTED ||
|
||||||
|
supportStatus == SupportStatus.SYSFS_SUPPORTED
|
||||||
|
|
||||||
|
_uiState.update { it.copy(isSupported = isSupported) }
|
||||||
|
|
||||||
|
if (isSupported) {
|
||||||
|
loadFiles()
|
||||||
|
checkMountStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.update { it.copy(successMessage = "Root access granted") }
|
||||||
|
} else {
|
||||||
|
// User denied root access
|
||||||
|
_uiState.update { it.copy(rootDenied = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshRootStatus() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Check cached status (no popup)
|
||||||
|
val cachedStatus = RootManager.isRootGrantedCached()
|
||||||
|
if (cachedStatus == true && !_uiState.value.hasRoot) {
|
||||||
|
// Root was granted externally (e.g., in Magisk settings)
|
||||||
|
_uiState.update { it.copy(hasRoot = true, rootDenied = false) }
|
||||||
|
|
||||||
|
// Save to preferences
|
||||||
|
getApplication<android.app.Application>()
|
||||||
|
.getSharedPreferences("iso_drive_prefs", android.content.Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean("root_granted", true)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
// Initialize if needed
|
||||||
|
isoDriveManager.initialize()
|
||||||
|
|
||||||
|
val supportStatus = isoDriveManager.isSupported()
|
||||||
|
val isSupported = supportStatus == SupportStatus.CONFIGFS_SUPPORTED ||
|
||||||
|
supportStatus == SupportStatus.SYSFS_SUPPORTED
|
||||||
|
|
||||||
|
_uiState.update { it.copy(isSupported = isSupported) }
|
||||||
|
|
||||||
|
if (isSupported) {
|
||||||
|
loadFiles()
|
||||||
|
checkMountStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun clearError() {
|
fun clearError() {
|
||||||
_uiState.update { it.copy(errorMessage = null) }
|
_uiState.update { it.copy(errorMessage = null) }
|
||||||
}
|
}
|
||||||
@@ -279,6 +363,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
data class MainUiState(
|
data class MainUiState(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val hasRoot: Boolean = false,
|
val hasRoot: Boolean = false,
|
||||||
|
val rootDenied: Boolean = false, // True if user denied root in Magisk
|
||||||
val isSupported: Boolean = false,
|
val isSupported: Boolean = false,
|
||||||
val mountStatus: MountStatus = MountStatus.UNMOUNTED,
|
val mountStatus: MountStatus = MountStatus.UNMOUNTED,
|
||||||
val isoFiles: List<IsoFile> = emptyList(),
|
val isoFiles: List<IsoFile> = emptyList(),
|
||||||
|
|||||||
Reference in New Issue
Block a user