mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 11:17:16 -04:00
Add wireless ADB process-limit repair
This commit is contained in:
1 parent
019949847a
commit
ddd4a0c24d
8 files changed
+296
-2
No files matched your search
@@ -121,6 +121,10 @@ android {
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
// Kadb's Bouncy Castle artifacts repeat the same multi-release JAR manifest.
|
||||
excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']
|
||||
}
|
||||
jniLibs {
|
||||
// The PulseAudio daemon is executed from the native library directory and its
|
||||
// libraries are read out of the apk by the class loader: both want the old,
|
||||
@@ -160,6 +164,9 @@ dependencies {
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
implementation 'io.coil-kt:coil-compose:2.6.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
// ADB-over-Wi-Fi pairing and authenticated shell client. Users still approve pairing from
|
||||
// Android's Wireless debugging screen; DroidDeck only runs its one fixed setting command.
|
||||
implementation 'com.flyfishxu:kadb-android:1.2.1'
|
||||
// Android terminal view/emulator used by the in-session secondary-display Linux shell.
|
||||
implementation 'com.termux.termux-app:terminal-view:0.118.0'
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Session logs go to Downloads so a failed run can be handed over as one folder. -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.droiddeck.launcher.ui.ProtonRow
|
||||
import com.droiddeck.launcher.core.CpuCores
|
||||
import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
import com.droiddeck.launcher.core.WirelessAdbFix
|
||||
import com.droiddeck.launcher.ui.CoreRow
|
||||
import com.droiddeck.launcher.ui.PerformancePage
|
||||
import com.droiddeck.launcher.ui.ModeSettingsPage
|
||||
@@ -354,6 +355,31 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onRefreshPhantomStatus = { refreshPhantomStatus() },
|
||||
onOpenDeveloperOptions = { openDeveloperOptions() },
|
||||
onWirelessAdbPair = { host, port, code, complete ->
|
||||
Thread({
|
||||
val result = runCatching {
|
||||
kotlinx.coroutines.runBlocking { WirelessAdbFix.pair(this@MainActivity, host, port, code) }
|
||||
}
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless ADB pairing failed" }
|
||||
ui.post { complete(error) }
|
||||
}, "wireless-adb-pair").start()
|
||||
},
|
||||
onFindWirelessAdbPort = { host, complete ->
|
||||
Thread({
|
||||
val port = WirelessAdbFix.findConnectPort(this@MainActivity, host)
|
||||
ui.post { complete(port) }
|
||||
}, "wireless-adb-discovery").start()
|
||||
},
|
||||
onWirelessAdbApply = { host, port, complete ->
|
||||
Thread({
|
||||
val result = runCatching { WirelessAdbFix.disableChildProcessLimit(this@MainActivity, host, port) }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless ADB command failed" }
|
||||
ui.post {
|
||||
if (error == null) refreshPhantomStatus()
|
||||
complete(error)
|
||||
}
|
||||
}, "wireless-adb-fix").start()
|
||||
},
|
||||
onCopyPhantomCommand = {
|
||||
(getSystemService(CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("DroidDeck child-process setting", PhantomProcessLimit.ADB_COMMAND),
|
||||
|
||||
@@ -15,6 +15,7 @@ enum class PhantomProcessStatus {
|
||||
|
||||
object PhantomProcessLimit {
|
||||
const val ADB_COMMAND = "adb shell settings put global settings_enable_monitor_phantom_procs false"
|
||||
const val SHELL_COMMAND = "settings put global settings_enable_monitor_phantom_procs false"
|
||||
private const val SETTING = "settings_enable_monitor_phantom_procs"
|
||||
|
||||
fun read(resolver: ContentResolver, sdk: Int = Build.VERSION.SDK_INT): PhantomProcessStatus {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import com.flyfishxu.kadb.Kadb
|
||||
import com.flyfishxu.kadb.cert.KadbCert
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.ArrayDeque
|
||||
|
||||
/** A narrowly scoped Wireless debugging client for the child-process setting repair. */
|
||||
object WirelessAdbFix {
|
||||
private const val CERT_FILE = "wireless-adb-cert.pem"
|
||||
private const val KEY_FILE = "wireless-adb-key.pem"
|
||||
|
||||
@Synchronized
|
||||
private fun loadOrCreateIdentity(context: Context) {
|
||||
val directory = context.noBackupFilesDir
|
||||
val cert = File(directory, CERT_FILE)
|
||||
val key = File(directory, KEY_FILE)
|
||||
if (cert.isFile && key.isFile) {
|
||||
KadbCert.set(cert.readBytes(), key.readBytes())
|
||||
return
|
||||
}
|
||||
|
||||
val identity = KadbCert.get(notAfter = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(3650))
|
||||
KadbCert.set(identity.first, identity.second)
|
||||
writeAtomically(cert, identity.first)
|
||||
writeAtomically(key, identity.second)
|
||||
}
|
||||
|
||||
private fun writeAtomically(destination: File, contents: ByteArray) {
|
||||
val temporary = File(destination.parentFile, "${destination.name}.tmp")
|
||||
temporary.writeBytes(contents)
|
||||
check(temporary.renameTo(destination)) { "Could not save Wireless debugging identity" }
|
||||
}
|
||||
|
||||
suspend fun pair(context: Context, host: String, port: Int, pairingCode: String) {
|
||||
loadOrCreateIdentity(context.applicationContext)
|
||||
Kadb.pair(host, port, pairingCode, "DroidDeck")
|
||||
}
|
||||
|
||||
/** Finds this paired device's separate TLS connection port from Android's ADB mDNS record. */
|
||||
fun findConnectPort(context: Context, pairedHost: String, timeoutSeconds: Long = 12): Int? {
|
||||
val manager = context.applicationContext.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
val expectedAddress = runCatching { InetAddress.getByName(pairedHost).address }.getOrNull() ?: return null
|
||||
val resolvedPort = AtomicInteger(-1)
|
||||
val finished = AtomicBoolean(false)
|
||||
val latch = CountDownLatch(1)
|
||||
val queue = ArrayDeque<NsdServiceInfo>()
|
||||
val seen = mutableSetOf<String>()
|
||||
var resolving = false
|
||||
val lock = Any()
|
||||
|
||||
lateinit var resolveNext: () -> Unit
|
||||
resolveNext = {
|
||||
val next = synchronized(lock) {
|
||||
if (finished.get() || resolving || queue.isEmpty()) null
|
||||
else queue.removeFirst().also { resolving = true }
|
||||
}
|
||||
if (next != null) {
|
||||
val listener = object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||
synchronized(lock) { resolving = false }
|
||||
resolveNext()
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
val address = serviceInfo.host?.address
|
||||
if (address != null && address.contentEquals(expectedAddress) && finished.compareAndSet(false, true)) {
|
||||
resolvedPort.set(serviceInfo.port)
|
||||
latch.countDown()
|
||||
} else {
|
||||
synchronized(lock) { resolving = false }
|
||||
resolveNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
runCatching { manager.resolveService(next, listener) }.onFailure {
|
||||
synchronized(lock) { resolving = false }
|
||||
resolveNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val discoveryListener = object : NsdManager.DiscoveryListener {
|
||||
override fun onDiscoveryStarted(serviceType: String) = Unit
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
if (serviceInfo.serviceType?.contains("_adb-tls-connect._tcp") == true) {
|
||||
synchronized(lock) {
|
||||
if (seen.add(serviceInfo.serviceName)) queue.addLast(serviceInfo)
|
||||
}
|
||||
resolveNext()
|
||||
}
|
||||
}
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) = Unit
|
||||
override fun onDiscoveryStopped(serviceType: String) { latch.countDown() }
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { latch.countDown() }
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { latch.countDown() }
|
||||
}
|
||||
|
||||
try {
|
||||
manager.discoverServices("_adb-tls-connect._tcp.", NsdManager.PROTOCOL_DNS_SD, discoveryListener)
|
||||
latch.await(timeoutSeconds, TimeUnit.SECONDS)
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
} finally {
|
||||
runCatching { manager.stopServiceDiscovery(discoveryListener) }
|
||||
}
|
||||
return resolvedPort.get().takeIf { it in 1..65535 }
|
||||
}
|
||||
|
||||
fun disableChildProcessLimit(context: Context, host: String, port: Int) {
|
||||
loadOrCreateIdentity(context.applicationContext)
|
||||
Kadb.create(host, port).use { adb ->
|
||||
val change = adb.shell(PhantomProcessLimit.SHELL_COMMAND)
|
||||
check(change.exitCode == 0) { change.allOutput.ifBlank { "ADB command failed (${change.exitCode})" } }
|
||||
val result = adb.shell("settings get global settings_enable_monitor_phantom_procs")
|
||||
check(result.exitCode == 0 && result.output.trim() == "false") {
|
||||
"Android did not confirm the child-process limit was disabled: ${result.allOutput.trim()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,6 +213,9 @@ class FrontEndActions(
|
||||
val onCheckLatestBuild: () -> Unit = {},
|
||||
val onRefreshPhantomStatus: () -> Unit = {},
|
||||
val onOpenDeveloperOptions: () -> Unit = {},
|
||||
val onWirelessAdbPair: (String, Int, String, (String?) -> Unit) -> Unit = { _, _, _, done -> done("Wireless ADB is unavailable") },
|
||||
val onFindWirelessAdbPort: (String, (Int?) -> Unit) -> Unit = { _, done -> done(null) },
|
||||
val onWirelessAdbApply: (String, Int, (String?) -> Unit) -> Unit = { _, _, done -> done("Wireless ADB is unavailable") },
|
||||
val onCopyPhantomCommand: () -> Unit = {},
|
||||
val onDismissPhantomGate: () -> Unit = {},
|
||||
val controller: ControllerActions? = null,
|
||||
@@ -351,6 +354,7 @@ fun FrontEndScreen(s: FrontEndState, a: FrontEndActions, page: (@Composable () -
|
||||
private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Composable () -> Unit)?, frontFocus: FrontFocus) {
|
||||
var selected by rememberSaveable { mutableStateOf("steam") }
|
||||
var navOpen by rememberSaveable { mutableStateOf(false) }
|
||||
var showWirelessAdbFix by rememberSaveable { mutableStateOf(false) }
|
||||
var appToChooseDisplay by remember { mutableStateOf<HomeApp.LaunchableApp?>(null) }
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val ctx = LocalContext.current
|
||||
@@ -503,11 +507,21 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
|
||||
confirmButton = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
androidx.compose.material3.TextButton(onClick = a.onOpenDeveloperOptions) { Text("Developer options") }
|
||||
androidx.compose.material3.TextButton(onClick = { showWirelessAdbFix = true }) { Text("Fix over Wi-Fi") }
|
||||
androidx.compose.material3.TextButton(onClick = a.onCopyPhantomCommand) { Text("Copy ADB") }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showWirelessAdbFix) {
|
||||
WirelessAdbFixDialog(
|
||||
onDismiss = { showWirelessAdbFix = false },
|
||||
onOpenDeveloperOptions = a.onOpenDeveloperOptions,
|
||||
onPair = a.onWirelessAdbPair,
|
||||
onFindConnectPort = a.onFindWirelessAdbPort,
|
||||
onApply = a.onWirelessAdbApply,
|
||||
)
|
||||
}
|
||||
}
|
||||
BackHandler(enabled = navOpen) { navOpen = false }
|
||||
val hasBackTarget = (s.pageKey != null && page != null) ||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun WirelessAdbFixDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onOpenDeveloperOptions: () -> Unit,
|
||||
onPair: (String, Int, String, (String?) -> Unit) -> Unit,
|
||||
onFindConnectPort: (String, (Int?) -> Unit) -> Unit,
|
||||
onApply: (String, Int, (String?) -> Unit) -> Unit,
|
||||
) {
|
||||
var step by rememberSaveable { mutableStateOf(0) }
|
||||
var host by rememberSaveable { mutableStateOf("") }
|
||||
var pairingPort by rememberSaveable { mutableStateOf("") }
|
||||
var pairingCode by rememberSaveable { mutableStateOf("") }
|
||||
var connectPort by rememberSaveable { mutableStateOf("") }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var message by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!busy) onDismiss() },
|
||||
title = { Text("Fix over Wi-Fi") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
when (step) {
|
||||
0 -> {
|
||||
Text("Enable Developer options and Wireless debugging on this device, and connect to Wi-Fi. Keep DroidDeck visible beside the pairing-code screen, such as in split screen. Android may close the code if you leave that screen.")
|
||||
Text("Enter the IP address and pairing port shown with the code.", style = MaterialTheme.typography.bodySmall)
|
||||
OutlinedTextField(host, { host = it.trim() }, label = { Text("Device IP address") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(pairingPort, { pairingPort = it.filter(Char::isDigit).take(5) }, label = { Text("Pairing port") }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(pairingCode, { pairingCode = it.filter(Char::isDigit).take(6) }, label = { Text("Pairing code") }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
1 -> {
|
||||
Text("Paired. Return to the Wireless debugging screen and enter the current IP address and port shown there. This connect port is different from the pairing port.")
|
||||
OutlinedTextField(host, { host = it.trim() }, label = { Text("Device IP address") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(connectPort, { connectPort = it.filter(Char::isDigit).take(5) }, label = { Text("Wireless debugging port") }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
else -> Text(message ?: "Android confirmed the child-process limit is disabled. You can start Steam now.")
|
||||
}
|
||||
message?.let { Text(it, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall) }
|
||||
if (busy) Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
CircularProgressIndicator()
|
||||
Text(if (step == 0) "Pairing…" else "Applying and checking…")
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
when (step) {
|
||||
0 -> TextButton(enabled = !busy && host.isNotBlank() && validPort(pairingPort) && pairingCode.length >= 6, onClick = {
|
||||
busy = true
|
||||
message = null
|
||||
onPair(host, pairingPort.toInt(), pairingCode) { error ->
|
||||
busy = false
|
||||
pairingCode = ""
|
||||
if (error == null) { step = 1; message = null } else message = error
|
||||
if (error == null) {
|
||||
busy = true
|
||||
message = "Paired. Finding this device’s Wireless debugging port…"
|
||||
onFindConnectPort(host) { discoveredPort ->
|
||||
if (discoveredPort == null) {
|
||||
busy = false
|
||||
message = "Automatic discovery did not find the device. Enter the current Wireless debugging port below."
|
||||
} else {
|
||||
connectPort = discoveredPort.toString()
|
||||
message = "Device found. Applying the setting…"
|
||||
onApply(host, discoveredPort) { applyError ->
|
||||
busy = false
|
||||
if (applyError == null) { step = 2; message = null }
|
||||
else message = "Could not apply automatically. Check the port below and retry. $applyError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) { Text("Pair") }
|
||||
1 -> TextButton(enabled = !busy && validPort(connectPort), onClick = {
|
||||
busy = true
|
||||
message = null
|
||||
onApply(host, connectPort.toInt()) { error ->
|
||||
busy = false
|
||||
if (error == null) { step = 2; message = null } else message = error
|
||||
}
|
||||
}) { Text("Disable child-process limit") }
|
||||
else -> TextButton(onClick = onDismiss) { Text("Done") }
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Row {
|
||||
if (step < 2) TextButton(enabled = !busy, onClick = onOpenDeveloperOptions) { Text("Developer options") }
|
||||
TextButton(enabled = !busy, onClick = onDismiss) { Text(if (step == 2) "Close" else "Cancel") }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun validPort(value: String): Boolean = value.toIntOrNull()?.let { it in 1..65535 } == true
|
||||
+2
-2
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21'
|
||||
classpath 'org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.0.21'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0'
|
||||
classpath 'org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.1.0'
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user