Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions app/src/main/java/org/randomcoder/udroid/runtime/CapabilityProbe.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import android.content.Context
import android.hardware.HardwareBuffer
import android.os.Build
import android.os.storage.StorageManager
import android.provider.Settings
import android.system.Os
import android.system.OsConstants
import java.io.File

internal data class ProbeRead(
val succeeded: Boolean,
val value: String? = null,
)

object CapabilityProbe {
fun run(context: Context): List<CapabilityResult> {
val results = mutableListOf<CapabilityResult>()
Expand All @@ -33,6 +39,8 @@ object CapabilityProbe {
required = false,
)

results += phantomProcessMonitorResult(context)

val dmaHeapDirectory = File("/dev/dma_heap")
val heaps =
dmaHeapDirectory.list()
Expand Down Expand Up @@ -107,6 +115,37 @@ object CapabilityProbe {
Os.close(descriptor)
}.isSuccess

private fun phantomProcessMonitorResult(context: Context): CapabilityResult {
if (Build.VERSION.SDK_INT < 31) {
return classifyPhantomProcessMonitor(Build.VERSION.SDK_INT, ProbeRead(false), ProbeRead(false))
}

val global =
runCatching {
Settings.Global.getString(
context.contentResolver,
PHANTOM_PROCESS_MONITOR_SETTING,
)
}.fold(
onSuccess = { ProbeRead(succeeded = true, value = it?.trim()) },
onFailure = { ProbeRead(succeeded = false) },
)
val property =
if (global.succeeded && global.value.isNullOrBlank()) {
readSystemProperty(PHANTOM_PROCESS_MONITOR_PROPERTY)
} else {
ProbeRead(succeeded = false)
}
return classifyPhantomProcessMonitor(Build.VERSION.SDK_INT, global, property)
}

private fun readSystemProperty(name: String): ProbeRead =
runCatching {
val process = ProcessBuilder("/system/bin/getprop", name).start()
val value = process.inputStream.bufferedReader().use { it.readText().trim() }
if (process.waitFor() == 0) ProbeRead(succeeded = true, value = value) else ProbeRead(false)
}.getOrElse { ProbeRead(succeeded = false) }

private fun result(
name: String,
passed: Boolean,
Expand All @@ -123,4 +162,89 @@ object CapabilityProbe {
val gib = bytes.toDouble() / (1024.0 * 1024.0 * 1024.0)
return "%.1f GiB".format(gib)
}

private const val PHANTOM_PROCESS_MONITOR_SETTING =
"settings_enable_monitor_phantom_procs"
private const val PHANTOM_PROCESS_MONITOR_PROPERTY =
"persist.sys.fflag.override.settings_enable_monitor_phantom_procs"
}

internal fun classifyPhantomProcessMonitor(
sdkInt: Int,
global: ProbeRead,
property: ProbeRead,
): CapabilityResult {
if (sdkInt < 31) {
return CapabilityResult(
name = "Child process restrictions",
status = CapabilityStatus.PASS,
detail = "Not used by this Android version",
required = false,
)
}

if (!global.succeeded) return unknownPhantomProcessMonitorResult(sdkInt)
val selected = if (global.value.isNullOrBlank()) property else global
if (!selected.succeeded) return unknownPhantomProcessMonitorResult(sdkInt)

val value = selected.value.orEmpty()
val guidance = phantomProcessMonitorGuidance(sdkInt)
if (value.isBlank()) {
return CapabilityResult(
name = "Child process restrictions",
status = CapabilityStatus.WARNING,
detail =
"No override is set; Android's default is enabled on standard builds and may " +
"stop Linux child processes. $guidance",
required = false,
showDeveloperOptionsAction = sdkInt >= 34,
)
}

return when (value.lowercase()) {
"false" ->
CapabilityResult(
name = "Child process restrictions",
status = CapabilityStatus.PASS,
detail = "Disabled; Linux child processes are not restricted by this monitor",
required = false,
)
"true" ->
CapabilityResult(
name = "Child process restrictions",
status = CapabilityStatus.WARNING,
detail = activePhantomProcessMonitorDetail(sdkInt),
required = false,
showDeveloperOptionsAction = sdkInt >= 34,
linuxProcessRestrictionActive = true,
)
else -> unknownPhantomProcessMonitorResult(sdkInt)
}
}

private fun unknownPhantomProcessMonitorResult(sdkInt: Int) =
CapabilityResult(
name = "Child process restrictions",
status = CapabilityStatus.WARNING,
detail = "State unknown. ${phantomProcessMonitorGuidance(sdkInt)}",
required = false,
showDeveloperOptionsAction = sdkInt >= 34,
)

private fun phantomProcessMonitorGuidance(sdkInt: Int): String =
if (sdkInt >= 34) {
"Enable Developer options > Disable child process restrictions. uDroid cannot change it."
} else {
"This Android version has no built-in toggle and uDroid cannot change it. Run: " +
"adb shell settings put global settings_enable_monitor_phantom_procs false"
}

private fun activePhantomProcessMonitorDetail(sdkInt: Int): String =
if (sdkInt >= 34) {
"Android can stop desktops and larger Linux apps when they start many background " +
"processes. In Developer options, turn on “Disable child process restrictions.”"
} else {
"Android can stop desktops and larger Linux apps when they start many background " +
"processes. This Android version has no settings toggle. Use ADB: adb shell " +
"settings put global settings_enable_monitor_phantom_procs false"
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class RuntimeSnapshot(
enum class CapabilityStatus {
PASS,
FAIL,
WARNING,
INFO,
}

Expand All @@ -31,4 +32,6 @@ data class CapabilityResult(
val status: CapabilityStatus,
val detail: String,
val required: Boolean,
val showDeveloperOptionsAction: Boolean = false,
val linuxProcessRestrictionActive: Boolean = false,
)
53 changes: 50 additions & 3 deletions app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package org.randomcoder.udroid.ui

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
Expand Down Expand Up @@ -834,6 +838,10 @@ private fun ManagementPane(
scanMessage = desktopScanMessage,
audioConfiguration = audioConfiguration,
audioConfigurationMessage = audioConfigurationMessage,
childProcessRestriction =
capabilities.firstOrNull {
it.linuxProcessRestrictionActive
},
resetAvailable =
selectedRootfs.name in resettableRootfsNames ||
selectedDistro != null,
Expand Down Expand Up @@ -863,6 +871,7 @@ private fun ManagementPane(
onGraphicsProfileChanged = onGraphicsProfileChanged,
onAudioOutputChanged = onAudioOutputChanged,
onMicrophoneChanged = onMicrophoneChanged,
onRefreshCapabilities = onRefresh,
onStartDesktop = onStartDesktop,
onStopTerminal = onStop,
onStopDesktop = onStopDesktop,
Expand Down Expand Up @@ -1241,6 +1250,7 @@ private fun DevicePage(
capabilities: List<CapabilityResult>,
onRefresh: () -> Unit,
) {
val openDeveloperOptions = rememberDeveloperOptionsAction(onRefresh)
LazyColumn(
modifier =
Modifier
Expand All @@ -1265,14 +1275,20 @@ private fun DevicePage(
)
}
items(capabilities) { capability ->
CapabilityRow(capability)
CapabilityRow(
capability = capability,
onOpenDeveloperOptions = openDeveloperOptions,
)
}
item { Spacer(Modifier.height(16.dp)) }
}
}

@Composable
private fun CapabilityRow(capability: CapabilityResult) {
private fun CapabilityRow(
capability: CapabilityResult,
onOpenDeveloperOptions: () -> Unit,
) {
val (icon, tint, label) =
when (capability.status) {
CapabilityStatus.PASS ->
Expand All @@ -1283,6 +1299,8 @@ private fun CapabilityRow(capability: CapabilityResult) {
if (capability.required) MaterialTheme.colorScheme.error else UdroidWarning,
if (capability.required) "Required" else "Unavailable",
)
CapabilityStatus.WARNING ->
Triple(Icons.Rounded.ErrorOutline, UdroidWarning, "Review")
CapabilityStatus.INFO ->
Triple(Icons.Rounded.Info, MaterialTheme.colorScheme.tertiary, "Detected")
}
Expand All @@ -1300,7 +1318,16 @@ private fun CapabilityRow(capability: CapabilityResult) {
Text(capability.name, style = MaterialTheme.typography.titleMedium)
},
supportingContent = {
Text(capability.detail, style = MaterialTheme.typography.bodySmall)
Column {
Text(capability.detail, style = MaterialTheme.typography.bodySmall)
if (capability.showDeveloperOptionsAction) {
TextButton(
onClick = onOpenDeveloperOptions,
) {
Text("Open Developer options")
}
}
}
},
trailingContent = {
Text(
Expand All @@ -1312,6 +1339,26 @@ private fun CapabilityRow(capability: CapabilityResult) {
)
}

@Composable
internal fun rememberDeveloperOptionsAction(onReturn: () -> Unit): () -> Unit {
val context = LocalContext.current
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
onReturn()
}
return {
runCatching {
launcher.launch(Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS))
}.onFailure {
Toast.makeText(
context,
"Developer options are unavailable on this device",
Toast.LENGTH_LONG,
).show()
}
}
}

@Composable
private fun AboutPage(
journalLines: List<String>,
Expand Down
Loading
Loading