mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e403156b | ||
|
|
9b495a7162 | ||
|
|
416fd509e6 | ||
|
|
14e6a673b5 |
No files matched your search
@@ -130,6 +130,9 @@
|
||||
<service
|
||||
android:name=".runtime.RuntimeInstallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".core.WirelessAdbPairingService"
|
||||
android:exported="false" />
|
||||
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
||||
@@ -45,6 +45,7 @@ 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.core.WirelessAdbPairingService
|
||||
import com.droiddeck.launcher.ui.CoreRow
|
||||
import com.droiddeck.launcher.ui.PerformancePage
|
||||
import com.droiddeck.launcher.ui.ModeSettingsPage
|
||||
@@ -431,6 +432,12 @@ class MainActivity : ComponentActivity() {
|
||||
android.widget.Toast.makeText(this, "ADB command copied", android.widget.Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onDismissPhantomGate = { showPhantomGate = false },
|
||||
onStartWirelessAdbPairing = { WirelessAdbPairingService.start(this) },
|
||||
onOpenNotificationSettings = {
|
||||
startActivity(Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
.putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
},
|
||||
controller = ControllerActions(
|
||||
onOsc = { o -> SessionPrefs.setOscMode(this, o); oscMode = o },
|
||||
onTint = { t -> ControllerPrefs.setTint(this, t); refreshController() },
|
||||
@@ -1040,15 +1047,18 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private fun refreshPhantomStatus() {
|
||||
phantomProcessStatus = PhantomProcessLimit.read(contentResolver)
|
||||
phantomProcessStatus = PhantomProcessLimit.read(this)
|
||||
phantomWarning = if (PhantomProcessLimit.blocksSteam(phantomProcessStatus)) {
|
||||
"${PhantomProcessLimit.title(phantomProcessStatus)}. ${PhantomProcessLimit.instructions(phantomProcessStatus)}\n\n${PhantomProcessLimit.ADB_COMMAND}"
|
||||
"${PhantomProcessLimit.title(phantomProcessStatus)}. ${PhantomProcessLimit.instructions(phantomProcessStatus)}\n\n${PhantomProcessLimit.adbCommand()}"
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun openDeveloperOptions(displayId: Int?) {
|
||||
val highlight = if (PhantomProcessLimit.hasDeveloperToggle() &&
|
||||
WirelessAdbPairingService.stage.value == WirelessAdbPairingService.Stage.Idle) null else "toggle_adb_wireless"
|
||||
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.apply { if (highlight != null) putExtra(":settings:fragment_args_key", highlight) }
|
||||
try {
|
||||
if (displayId == null) startActivity(intent)
|
||||
else startActivity(intent, ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle())
|
||||
|
||||
@@ -162,7 +162,7 @@ object DeviceReport {
|
||||
})
|
||||
|
||||
h("Android process limits")
|
||||
k("Phantom proc monitor", PhantomProcessLimit.reportValue(PhantomProcessLimit.read(context.contentResolver)))
|
||||
k("Phantom proc monitor", PhantomProcessLimit.reportValue(PhantomProcessLimit.read(context)))
|
||||
|
||||
h("Device switch files in Download")
|
||||
for (name in listOf("droiddeck-env", "droiddeck-tu-debug", "droiddeck-driver",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
|
||||
@@ -14,17 +14,53 @@ 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 adbCommand(enabled: Boolean): String =
|
||||
"adb shell settings put global $SETTING ${if (enabled) "true" else "false"}"
|
||||
private const val MAX_PHANTOM = "2147483647"
|
||||
private const val PREFS = "phantom-process-limit"
|
||||
private const val PREF_ANDROID_12_OFF = "android12-off"
|
||||
|
||||
fun read(resolver: ContentResolver, sdk: Int = Build.VERSION.SDK_INT): PhantomProcessStatus {
|
||||
fun usesDeviceConfig(sdk: Int = Build.VERSION.SDK_INT): Boolean = sdk == Build.VERSION_CODES.S
|
||||
|
||||
fun shellCommands(enabled: Boolean, sdk: Int = Build.VERSION.SDK_INT): List<String> = when {
|
||||
usesDeviceConfig(sdk) && enabled -> listOf(
|
||||
"device_config delete activity_manager max_phantom_processes",
|
||||
"device_config set_sync_disabled_for_tests none",
|
||||
)
|
||||
usesDeviceConfig(sdk) -> listOf(
|
||||
"device_config set_sync_disabled_for_tests persistent",
|
||||
"device_config put activity_manager max_phantom_processes $MAX_PHANTOM",
|
||||
)
|
||||
else -> listOf("settings put global $SETTING ${if (enabled) "true" else "false"}")
|
||||
}
|
||||
|
||||
fun verifyCommand(sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
if (usesDeviceConfig(sdk)) "device_config get activity_manager max_phantom_processes"
|
||||
else "settings get global $SETTING"
|
||||
|
||||
fun verified(output: String, enabled: Boolean, sdk: Int = Build.VERSION.SDK_INT): Boolean {
|
||||
val value = output.trim()
|
||||
return if (usesDeviceConfig(sdk)) (value == MAX_PHANTOM) != enabled
|
||||
else value == if (enabled) "true" else "false"
|
||||
}
|
||||
|
||||
fun adbCommand(enabled: Boolean, sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
shellCommands(enabled, sdk).joinToString(" && ") { "adb shell $it" }
|
||||
|
||||
fun adbCommand(): String = adbCommand(false)
|
||||
|
||||
fun rememberAndroid12(context: Context, off: Boolean) {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(PREF_ANDROID_12_OFF, off).apply()
|
||||
}
|
||||
|
||||
fun read(context: Context, sdk: Int = Build.VERSION.SDK_INT): PhantomProcessStatus {
|
||||
if (sdk < Build.VERSION_CODES.S) return PhantomProcessStatus.NOT_APPLICABLE
|
||||
if (usesDeviceConfig(sdk)) {
|
||||
val off = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(PREF_ANDROID_12_OFF, false)
|
||||
return if (off) PhantomProcessStatus.DISABLED else PhantomProcessStatus.UNREADABLE
|
||||
}
|
||||
val global = try {
|
||||
Settings.Global.getString(resolver, SETTING)
|
||||
Settings.Global.getString(context.contentResolver, SETTING)
|
||||
} catch (_: Exception) {
|
||||
return PhantomProcessStatus.UNREADABLE
|
||||
}
|
||||
@@ -57,6 +93,8 @@ object PhantomProcessLimit {
|
||||
null
|
||||
}
|
||||
|
||||
fun hasDeveloperToggle(sdk: Int = Build.VERSION.SDK_INT): Boolean = sdk >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
||||
|
||||
fun blocksSteam(status: PhantomProcessStatus): Boolean =
|
||||
status != PhantomProcessStatus.NOT_APPLICABLE && status != PhantomProcessStatus.DISABLED
|
||||
|
||||
@@ -69,20 +107,20 @@ object PhantomProcessLimit {
|
||||
}
|
||||
|
||||
fun instructions(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED -> "Turn off “Restrict child processes” in Developer options. If you cannot find that option, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.UNSET -> "This setting is unset, so the ROM default applies and may enforce the limit. Set “Restrict child processes” to off in Developer options. If you cannot find that option, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.UNREADABLE -> "DroidDeck could not read the setting. Set “Restrict child processes” to off in Developer options. If you cannot find that option, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE -> fixSentence() + " If that is not possible, connect this device to a computer with ADB and run the command below. Return here and check again."
|
||||
PhantomProcessStatus.DISABLED -> "Steam sessions can start."
|
||||
PhantomProcessStatus.NOT_APPLICABLE -> "This Android version does not use this limit."
|
||||
}
|
||||
|
||||
fun gateInstructions(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.ENABLED -> "Turn off “Restrict child processes” in Developer options."
|
||||
PhantomProcessStatus.UNSET -> "The ROM default may enforce this limit. Explicitly turn off “Restrict child processes” in Developer options."
|
||||
PhantomProcessStatus.UNREADABLE -> "DroidDeck could not check this setting. Turn off “Restrict child processes” in Developer options."
|
||||
PhantomProcessStatus.ENABLED, PhantomProcessStatus.UNSET, PhantomProcessStatus.UNREADABLE -> fixSentence()
|
||||
else -> instructions(status)
|
||||
}
|
||||
|
||||
fun fixSentence(sdk: Int = Build.VERSION.SDK_INT): String =
|
||||
if (hasDeveloperToggle(sdk)) "Turn on “Disable child process restrictions” in Developer options, and keep Developer options on afterwards."
|
||||
else "This Android version has no switch for it. Use Wireless debugging to change it from this device."
|
||||
|
||||
fun reportValue(status: PhantomProcessStatus): String = when (status) {
|
||||
PhantomProcessStatus.DISABLED -> "disabled (good - the OS will not kill the session's children)"
|
||||
PhantomProcessStatus.ENABLED -> "ENABLED (the OS may kill the session with no log; turn off Restrict child processes)"
|
||||
|
||||
@@ -6,7 +6,9 @@ import android.net.nsd.NsdServiceInfo
|
||||
import com.flyfishxu.kadb.Kadb
|
||||
import com.flyfishxu.kadb.cert.KadbCert
|
||||
import java.io.File
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -19,6 +21,7 @@ object WirelessAdbFix {
|
||||
private const val KEY_FILE = "wireless-adb-key.pem"
|
||||
private const val HOST_FILE = "wireless-adb-host.txt"
|
||||
private const val CONNECTION_FILE = "wireless-adb-connection.txt"
|
||||
const val LOOPBACK = "127.0.0.1"
|
||||
|
||||
private data class SavedConnection(val host: String, val port: Int?)
|
||||
|
||||
@@ -76,9 +79,27 @@ object WirelessAdbFix {
|
||||
}
|
||||
|
||||
/** Finds this paired device's separate TLS connection port from Android's ADB mDNS record. */
|
||||
fun isLocalAddress(address: InetAddress): Boolean = runCatching {
|
||||
if (address.isLoopbackAddress) return true
|
||||
NetworkInterface.getNetworkInterfaces()?.toList().orEmpty().any { network ->
|
||||
network.inetAddresses.toList().any { it.address.contentEquals(address.address) }
|
||||
}
|
||||
}.getOrDefault(address is Inet4Address)
|
||||
|
||||
fun localConnectPort(context: Context): Int? =
|
||||
tlsPortProperty()
|
||||
?: findConnectPort(context, LOOPBACK)
|
||||
|
||||
private fun tlsPortProperty(): Int? = runCatching {
|
||||
Class.forName("android.os.SystemProperties").getMethod("get", String::class.java)
|
||||
.invoke(null, "service.adb.tls.port") as? String
|
||||
}.getOrNull()?.trim()?.toIntOrNull()?.takeIf { it in 1..65535 }
|
||||
|
||||
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 paired = runCatching { InetAddress.getByName(pairedHost) }.getOrNull() ?: return null
|
||||
val matches: (InetAddress) -> Boolean =
|
||||
if (paired.isLoopbackAddress) ::isLocalAddress else { address -> address.address.contentEquals(paired.address) }
|
||||
val resolvedPort = AtomicInteger(-1)
|
||||
val finished = AtomicBoolean(false)
|
||||
val latch = CountDownLatch(1)
|
||||
@@ -101,8 +122,8 @@ object WirelessAdbFix {
|
||||
}
|
||||
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
val address = serviceInfo.host?.address
|
||||
if (address != null && address.contentEquals(expectedAddress) && finished.compareAndSet(false, true)) {
|
||||
val address = serviceInfo.host
|
||||
if (address != null && matches(address) && finished.compareAndSet(false, true)) {
|
||||
resolvedPort.set(serviceInfo.port)
|
||||
latch.countDown()
|
||||
} else {
|
||||
@@ -148,14 +169,16 @@ object WirelessAdbFix {
|
||||
fun setChildProcessLimit(context: Context, host: String, port: Int, enabled: Boolean) {
|
||||
loadOrCreateIdentity(context.applicationContext)
|
||||
Kadb.create(host, port).use { adb ->
|
||||
val value = if (enabled) "true" else "false"
|
||||
val change = adb.shell("settings put global settings_enable_monitor_phantom_procs $value")
|
||||
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() == value) {
|
||||
PhantomProcessLimit.shellCommands(enabled).forEach { command ->
|
||||
val change = adb.shell(command)
|
||||
check(change.exitCode == 0) { change.allOutput.ifBlank { "ADB command failed (${change.exitCode})" } }
|
||||
}
|
||||
val result = adb.shell(PhantomProcessLimit.verifyCommand())
|
||||
check(result.exitCode == 0 && PhantomProcessLimit.verified(result.output, enabled)) {
|
||||
"Android did not confirm the child-process limit was changed: ${result.allOutput.trim()}"
|
||||
}
|
||||
}
|
||||
if (PhantomProcessLimit.usesDeviceConfig()) PhantomProcessLimit.rememberAndroid12(context, !enabled)
|
||||
saveConnection(context, host, port)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.RemoteInput
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import com.droiddeck.launcher.MainActivity
|
||||
import com.droiddeck.launcher.R
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class WirelessAdbPairingService : Service() {
|
||||
sealed interface Stage {
|
||||
data object Idle : Stage
|
||||
data object Waiting : Stage
|
||||
data class CodeNeeded(val error: String? = null) : Stage
|
||||
data class Working(val step: String) : Stage
|
||||
data object Done : Stage
|
||||
data class Failed(val error: String) : Stage
|
||||
}
|
||||
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private var nsd: NsdManager? = null
|
||||
private var discovery: NsdManager.DiscoveryListener? = null
|
||||
private var pairingHost: String? = null
|
||||
private var pairingPort: Int? = null
|
||||
@Volatile private var working = false
|
||||
private val timeout = Runnable { finish(Stage.Failed("Timed out waiting for the pairing pop-up. Try again from DroidDeck.")) }
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_CODE -> {
|
||||
val code = RemoteInput.getResultsFromIntent(intent)?.getCharSequence(KEY_CODE)
|
||||
?.filter(Char::isDigit)?.toString().orEmpty()
|
||||
onCode(code)
|
||||
}
|
||||
ACTION_CANCEL -> finish(Stage.Idle)
|
||||
else -> begin()
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun begin() {
|
||||
pairingHost = null
|
||||
pairingPort = null
|
||||
working = false
|
||||
publish(Stage.Waiting)
|
||||
startForeground(NOTIFICATION_ID, notification(Stage.Waiting))
|
||||
startDiscovery()
|
||||
main.removeCallbacks(timeout)
|
||||
main.postDelayed(timeout, TIMEOUT_MS)
|
||||
}
|
||||
|
||||
private fun startDiscovery() {
|
||||
stopDiscovery()
|
||||
val manager = getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
val listener = object : NsdManager.DiscoveryListener {
|
||||
override fun onDiscoveryStarted(serviceType: String) = Unit
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
if (serviceInfo.serviceType?.contains(PAIRING_TYPE) != true) return
|
||||
runCatching { manager.resolveService(serviceInfo, resolveListener()) }
|
||||
}
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo) = Unit
|
||||
override fun onDiscoveryStopped(serviceType: String) = Unit
|
||||
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||
main.post { finish(Stage.Failed("Could not look for the pairing pop-up ($errorCode). Check that Wi-Fi is on.")) }
|
||||
}
|
||||
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) = Unit
|
||||
}
|
||||
runCatching { manager.discoverServices("$PAIRING_TYPE.", NsdManager.PROTOCOL_DNS_SD, listener) }
|
||||
.onSuccess { nsd = manager; discovery = listener }
|
||||
.onFailure { finish(Stage.Failed("Could not look for the pairing pop-up: ${it.localizedMessage}")) }
|
||||
}
|
||||
|
||||
private fun resolveListener() = object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) = Unit
|
||||
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||
val address = serviceInfo.host ?: return
|
||||
if (!WirelessAdbFix.isLocalAddress(address)) return
|
||||
main.post {
|
||||
if (working || current.value == Stage.Done) return@post
|
||||
pairingHost = address.hostAddress
|
||||
pairingPort = serviceInfo.port
|
||||
if (current.value !is Stage.CodeNeeded) show(Stage.CodeNeeded())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCode(code: String) {
|
||||
val host = pairingHost
|
||||
val port = pairingPort
|
||||
if (working) return
|
||||
if (host == null || port == null) {
|
||||
show(Stage.Waiting)
|
||||
return
|
||||
}
|
||||
if (code.length != 6) {
|
||||
show(Stage.CodeNeeded("Enter all 6 digits of the code"))
|
||||
return
|
||||
}
|
||||
working = true
|
||||
show(Stage.Working("Pairing…"))
|
||||
Thread({
|
||||
val paired = runCatching { kotlinx.coroutines.runBlocking { WirelessAdbFix.pair(this@WirelessAdbPairingService, WirelessAdbFix.LOOPBACK, port, code) } }
|
||||
if (paired.isFailure) {
|
||||
main.post {
|
||||
working = false
|
||||
pairingPort = null
|
||||
show(Stage.CodeNeeded("That code didn't work. Open the pairing pop-up again and enter its new code."))
|
||||
}
|
||||
return@Thread
|
||||
}
|
||||
main.post {
|
||||
stopDiscovery()
|
||||
show(Stage.Working("Paired. Connecting…"))
|
||||
}
|
||||
val result = runCatching {
|
||||
val connectPort = WirelessAdbFix.localConnectPort(this)
|
||||
?: error("Paired, but the Wireless debugging port was not found. Keep Wireless debugging on and try again.")
|
||||
main.post { show(Stage.Working("Applying the setting…")) }
|
||||
WirelessAdbFix.setChildProcessLimit(this, WirelessAdbFix.LOOPBACK, connectPort, false)
|
||||
}
|
||||
main.post {
|
||||
working = false
|
||||
finish(result.exceptionOrNull()?.let { Stage.Failed(it.localizedMessage ?: "Wireless debugging command failed") } ?: Stage.Done)
|
||||
}
|
||||
}, "wireless-adb-pairing").start()
|
||||
}
|
||||
|
||||
private fun show(stage: Stage) {
|
||||
publish(stage)
|
||||
getSystemService(NotificationManager::class.java)?.notify(NOTIFICATION_ID, notification(stage))
|
||||
}
|
||||
|
||||
private fun finish(stage: Stage) {
|
||||
main.removeCallbacks(timeout)
|
||||
stopDiscovery()
|
||||
publish(stage)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
if (stage is Stage.Done || stage is Stage.Failed) {
|
||||
manager?.notify(NOTIFICATION_ID, notification(stage))
|
||||
stopForeground(STOP_FOREGROUND_DETACH)
|
||||
} else {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
manager?.cancel(NOTIFICATION_ID)
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun stopDiscovery() {
|
||||
val listener = discovery ?: return
|
||||
runCatching { nsd?.stopServiceDiscovery(listener) }
|
||||
discovery = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
main.removeCallbacks(timeout)
|
||||
stopDiscovery()
|
||||
if (current.value.let { it is Stage.Waiting || it is Stage.CodeNeeded || it is Stage.Working }) publish(Stage.Idle)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun notification(stage: Stage): Notification {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager?.createNotificationChannel(NotificationChannel(CHANNEL_ID, "Wireless debugging setup",
|
||||
NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
description = "Asks for the Wireless debugging pairing code while Settings is open"
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
})
|
||||
val open = PendingIntent.getActivity(this, 0,
|
||||
Intent(this, MainActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||
PendingIntent.FLAG_IMMUTABLE)
|
||||
val cancel = PendingIntent.getService(this, 1,
|
||||
Intent(this, WirelessAdbPairingService::class.java).setAction(ACTION_CANCEL), PendingIntent.FLAG_IMMUTABLE)
|
||||
val builder = Notification.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_session)
|
||||
.setContentIntent(open)
|
||||
.setShowWhen(false)
|
||||
.setCategory(Notification.CATEGORY_STATUS)
|
||||
when (stage) {
|
||||
Stage.Waiting, Stage.Idle -> builder
|
||||
.setContentTitle("Pair Wireless debugging")
|
||||
.setContentText("Turn on Wireless debugging, then tap “Pair device with pairing code”.")
|
||||
.setStyle(Notification.BigTextStyle().bigText(
|
||||
"Turn on Wireless debugging, then tap “Pair device with pairing code”. Stay in Settings: the code goes in this notification."))
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.addAction(Notification.Action.Builder(null, "Cancel", cancel).build())
|
||||
is Stage.CodeNeeded -> {
|
||||
val reply = PendingIntent.getService(this, 2,
|
||||
Intent(this, WirelessAdbPairingService::class.java).setAction(ACTION_CODE),
|
||||
if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
else PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val input = RemoteInput.Builder(KEY_CODE).setLabel("6-digit pairing code").build()
|
||||
val text = stage.error ?: "Tap Enter code and type the 6 digits shown in Settings."
|
||||
builder
|
||||
.setContentTitle(if (stage.error == null) "Enter the Wi-Fi pairing code" else "Try the code again")
|
||||
.setContentText(text)
|
||||
.setStyle(Notification.BigTextStyle().bigText(text))
|
||||
.setOngoing(true)
|
||||
.addAction(Notification.Action.Builder(null, "Enter code", reply).addRemoteInput(input).build())
|
||||
.addAction(Notification.Action.Builder(null, "Cancel", cancel).build())
|
||||
}
|
||||
is Stage.Working -> builder
|
||||
.setContentTitle("Setting up Steam")
|
||||
.setContentText(stage.step)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
Stage.Done -> builder
|
||||
.setContentTitle("Steam is ready")
|
||||
.setContentText("Tap to go back to DroidDeck. You can turn Wireless debugging off now.")
|
||||
.setStyle(Notification.BigTextStyle().bigText("Tap to go back to DroidDeck. You can turn Wireless debugging off now."))
|
||||
.setAutoCancel(true)
|
||||
is Stage.Failed -> builder
|
||||
.setContentTitle("Wireless debugging setup stopped")
|
||||
.setContentText(stage.error)
|
||||
.setStyle(Notification.BigTextStyle().bigText(stage.error))
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "wireless-adb-pairing"
|
||||
private const val NOTIFICATION_ID = 3
|
||||
private const val PAIRING_TYPE = "_adb-tls-pairing._tcp"
|
||||
private const val ACTION_CODE = "com.droiddeck.launcher.action.WIRELESS_ADB_CODE"
|
||||
private const val ACTION_CANCEL = "com.droiddeck.launcher.action.WIRELESS_ADB_CANCEL"
|
||||
private const val KEY_CODE = "code"
|
||||
private const val TIMEOUT_MS = 5 * 60_000L
|
||||
|
||||
private val current = MutableStateFlow<Stage>(Stage.Idle)
|
||||
val stage: StateFlow<Stage> = current
|
||||
|
||||
private fun publish(stage: Stage) { current.value = stage }
|
||||
|
||||
fun start(context: Context) {
|
||||
context.getSystemService(NotificationManager::class.java)?.cancel(NOTIFICATION_ID)
|
||||
publish(Stage.Waiting)
|
||||
val intent = Intent(context, WirelessAdbPairingService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= 26) context.startForegroundService(intent) else context.startService(intent)
|
||||
}
|
||||
|
||||
fun cancel(context: Context) {
|
||||
if (current.value.let { it is Stage.Done || it is Stage.Failed || it is Stage.Idle }) {
|
||||
publish(Stage.Idle)
|
||||
return
|
||||
}
|
||||
context.startService(Intent(context, WirelessAdbPairingService::class.java).setAction(ACTION_CANCEL))
|
||||
}
|
||||
|
||||
fun notificationsEnabled(context: Context): Boolean =
|
||||
context.getSystemService(NotificationManager::class.java)?.areNotificationsEnabled() != false
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,8 @@ class FrontEndActions(
|
||||
val onSetPhantomProcessLimit: (Boolean, (String?) -> Unit) -> Unit = { _, done -> done("Wireless debugging is unavailable") },
|
||||
val onCopyPhantomCommand: (Boolean) -> Unit = {},
|
||||
val onDismissPhantomGate: () -> Unit = {},
|
||||
val onStartWirelessAdbPairing: () -> Unit = {},
|
||||
val onOpenNotificationSettings: () -> Unit = {},
|
||||
val controller: ControllerActions? = null,
|
||||
)
|
||||
|
||||
@@ -469,7 +471,13 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
|
||||
status = s.phantomProcessStatus,
|
||||
onDismiss = a.onDismissPhantomGate,
|
||||
onOpenDeveloperOptions = requestDeveloperOptions,
|
||||
onSetUpWirelessAdb = { requestWirelessAdbFix(false) },
|
||||
onFixWithWirelessDebugging = {
|
||||
a.onStartWirelessAdbPairing()
|
||||
requestDeveloperOptions()
|
||||
},
|
||||
onEnterAddressManually = { requestWirelessAdbFix(false) },
|
||||
onCopyCommand = { a.onCopyPhantomCommand(false) },
|
||||
onOpenNotificationSettings = a.onOpenNotificationSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,96 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
import com.droiddeck.launcher.core.WirelessAdbPairingService
|
||||
import com.droiddeck.launcher.core.WirelessAdbPairingService.Stage
|
||||
|
||||
@Composable
|
||||
fun PhantomProcessGatePage(
|
||||
status: PhantomProcessStatus,
|
||||
onDismiss: () -> Unit,
|
||||
onOpenDeveloperOptions: () -> Unit,
|
||||
onSetUpWirelessAdb: () -> Unit,
|
||||
onFixWithWirelessDebugging: () -> Unit,
|
||||
onEnterAddressManually: () -> Unit,
|
||||
onCopyCommand: () -> Unit,
|
||||
onOpenNotificationSettings: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = onDismiss)
|
||||
val context = LocalContext.current
|
||||
val stage by WirelessAdbPairingService.stage.collectAsState()
|
||||
val environment by produceState(GateEnvironment.read(context)) {
|
||||
while (true) {
|
||||
value = GateEnvironment.read(context)
|
||||
kotlinx.coroutines.delay(2_000)
|
||||
}
|
||||
}
|
||||
val hasToggle = PhantomProcessLimit.hasDeveloperToggle()
|
||||
|
||||
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||
val compact = maxWidth < 620.dp && maxHeight < 500.dp
|
||||
SettingsPage(
|
||||
host = rememberMenuHost(),
|
||||
title = if (compact) "Steam paused" else "Steam cannot start yet",
|
||||
title = "One Android setting to change",
|
||||
eyebrow = "Steam",
|
||||
lede = if (compact) compactStatus(status) else "${PhantomProcessLimit.title(status)} · checking again every two seconds",
|
||||
lede = if (compact) null else "Android stops apps that start lots of background processes, and Steam starts dozens. " +
|
||||
"Until this is changed, games close with no error.",
|
||||
onBack = onDismiss,
|
||||
// Scrolls, so the larger text never pushes the ADB command off a short screen.
|
||||
scrollContent = true,
|
||||
compactLayout = compact,
|
||||
) {
|
||||
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||
val wide = maxWidth >= 620.dp
|
||||
Column(
|
||||
modifier = Modifier.widthIn(max = 900.dp).fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
when {
|
||||
compact -> CompactGateInstructions(status, onOpenDeveloperOptions, onSetUpWirelessAdb)
|
||||
wide -> Row(horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
GateInstructions(status, onOpenDeveloperOptions, onSetUpWirelessAdb)
|
||||
}
|
||||
Column(Modifier.weight(1f)) { AdbFallback() }
|
||||
}
|
||||
else -> {
|
||||
GateInstructions(status, onOpenDeveloperOptions, onSetUpWirelessAdb)
|
||||
AdbFallback()
|
||||
}
|
||||
val fix: @Composable ColumnScope.() -> Unit = {
|
||||
FixGroup(hasToggle, stage, environment, compact, onOpenDeveloperOptions, onFixWithWirelessDebugging,
|
||||
onOpenNotificationSettings, onCancel = { WirelessAdbPairingService.cancel(context) })
|
||||
}
|
||||
val side: @Composable ColumnScope.() -> Unit = {
|
||||
StatusGroup(status, compact)
|
||||
OtherWays(hasToggle, compact, onOpenDeveloperOptions, onFixWithWirelessDebugging, onEnterAddressManually, onCopyCommand)
|
||||
}
|
||||
if (wide) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.widthIn(max = 900.dp).fillMaxWidth()) {
|
||||
Column(Modifier.weight(1.2f), content = fix)
|
||||
Column(Modifier.weight(1f), content = side)
|
||||
}
|
||||
} else {
|
||||
Column(Modifier.widthIn(max = 600.dp).fillMaxWidth()) {
|
||||
fix()
|
||||
side()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,97 +98,168 @@ fun PhantomProcessGatePage(
|
||||
}
|
||||
}
|
||||
|
||||
private fun compactStatus(status: PhantomProcessStatus): String {
|
||||
val state = when (status) {
|
||||
PhantomProcessStatus.ENABLED -> "Child-process limit is on"
|
||||
PhantomProcessStatus.UNSET -> "Child-process limit is unset"
|
||||
PhantomProcessStatus.UNREADABLE -> "Child-process limit is unreadable"
|
||||
else -> PhantomProcessLimit.title(status)
|
||||
private data class GateEnvironment(val onWifi: Boolean, val notifications: Boolean) {
|
||||
companion object {
|
||||
fun read(context: Context): GateEnvironment {
|
||||
val connectivity = context.getSystemService(ConnectivityManager::class.java)
|
||||
val wifi = runCatching {
|
||||
connectivity?.getNetworkCapabilities(connectivity.activeNetwork)
|
||||
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
}.getOrDefault(true)
|
||||
return GateEnvironment(wifi, WirelessAdbPairingService.notificationsEnabled(context))
|
||||
}
|
||||
}
|
||||
return "$state · retrying every 2 sec"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompactGateInstructions(
|
||||
status: PhantomProcessStatus,
|
||||
private fun FixGroup(
|
||||
hasToggle: Boolean,
|
||||
stage: Stage,
|
||||
environment: GateEnvironment,
|
||||
compact: Boolean,
|
||||
onOpenDeveloperOptions: () -> Unit,
|
||||
onSetUpWirelessAdb: () -> Unit,
|
||||
onFixWithWirelessDebugging: () -> Unit,
|
||||
onOpenNotificationSettings: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
SettingsGroup("1 · Developer options", compact = true) {
|
||||
val pairing = stage is Stage.Waiting || stage is Stage.CodeNeeded || stage is Stage.Working
|
||||
SettingsGroup(if (hasToggle) "Developer options" else "Fix it for me", compact = compact) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 7.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(if (compact) 10.dp else 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
PhantomProcessLimit.gateInstructions(status),
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
PrimaryButton("Open Developer options", compact = true, onClick = onOpenDeveloperOptions)
|
||||
SecondaryButton("Use Wireless debugging", compact = true, onClick = onSetUpWirelessAdb)
|
||||
when {
|
||||
pairing -> PairingProgress(stage, onCancel)
|
||||
hasToggle -> {
|
||||
Body("Turn on “Disable child process restrictions”, then come back. Keep Developer options on afterwards: turning them off turns the switch back off.")
|
||||
PrimaryButton("Open Developer options", compact = compact, onClick = onOpenDeveloperOptions)
|
||||
}
|
||||
else -> {
|
||||
Body("This Android version has no switch for it, so DroidDeck changes it over Wireless debugging on this device. About a minute, no computer needed.")
|
||||
(stage as? Stage.Failed)?.let { Body(it.error, error = true) }
|
||||
PrimaryButton(
|
||||
if (stage is Stage.Failed) "Try again" else "Fix it for me",
|
||||
compact = compact,
|
||||
enabled = environment.onWifi && environment.notifications,
|
||||
onClick = onFixWithWirelessDebugging,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!hasToggle && !pairing) {
|
||||
if (!environment.onWifi) {
|
||||
Body("Wireless debugging needs Wi-Fi. Connect to any network first; it doesn't need internet.", warn = true)
|
||||
}
|
||||
if (!environment.notifications) {
|
||||
Body("The pairing code is entered in a notification. Allow notifications for DroidDeck, or use Other ways.", warn = true)
|
||||
SecondaryButton("Allow notifications", compact = compact, onClick = onOpenNotificationSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsGroup("3 · Computer ADB last resort", compact = true) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
Text(
|
||||
"If the setting is missing and Wireless debugging is unavailable, run this from a computer:",
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
PhantomProcessLimit.ADB_COMMAND,
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontFamily = FontFamily.Monospace, fontSize = 11.sp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PairingProgress(stage: Stage, onCancel: () -> Unit) {
|
||||
val steps = listOf(
|
||||
"Open Settings → Developer options → Wireless debugging and turn it on",
|
||||
"Tap “Pair device with pairing code”",
|
||||
"Type the code into the DroidDeck notification",
|
||||
)
|
||||
val active = when (stage) {
|
||||
Stage.Waiting -> 1
|
||||
is Stage.CodeNeeded -> 2
|
||||
else -> 3
|
||||
}
|
||||
steps.forEachIndexed { index, step ->
|
||||
val done = index < active
|
||||
val current = index == active
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(if (done) "✓" else "${index + 1}", fontSize = 13.sp, fontWeight = FontWeight.SemiBold,
|
||||
color = if (done) LocalPalette.current.good else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(step, style = MaterialTheme.typography.bodySmall,
|
||||
color = if (current) MaterialTheme.colorScheme.onBackground else MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
Text(
|
||||
when (stage) {
|
||||
Stage.Waiting -> "Waiting for the pairing pop-up…"
|
||||
is Stage.CodeNeeded -> stage.error ?: "Pop-up found. Enter its code in the notification."
|
||||
is Stage.Working -> stage.step
|
||||
else -> ""
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (stage is Stage.CodeNeeded && stage.error != null) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SecondaryButton("Cancel", compact = true, onClick = onCancel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusGroup(status: PhantomProcessStatus, compact: Boolean) {
|
||||
SettingsGroup("Status", compact = compact) {
|
||||
Column(Modifier.fillMaxWidth().padding(if (compact) 10.dp else 14.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(PhantomProcessLimit.title(status), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground)
|
||||
Body("Checking every 2 seconds. This page closes when it's off.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GateInstructions(
|
||||
status: PhantomProcessStatus,
|
||||
private fun OtherWays(
|
||||
hasToggle: Boolean,
|
||||
compact: Boolean,
|
||||
onOpenDeveloperOptions: () -> Unit,
|
||||
onSetUpWirelessAdb: () -> Unit,
|
||||
onFixWithWirelessDebugging: () -> Unit,
|
||||
onEnterAddressManually: () -> Unit,
|
||||
onCopyCommand: () -> Unit,
|
||||
) {
|
||||
SettingsGroup("1 · Developer options first") {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
var open by rememberSaveable { mutableStateOf(false) }
|
||||
SettingsGroup("Other ways", compact = compact) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().clickable { open = !open }.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Body(if (hasToggle) "Wireless debugging, manual address, computer ADB" else "Manual address, computer ADB, Developer options", modifier = Modifier.weight(1f))
|
||||
Text(if (open) "▴" else "▾", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
if (open) Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 14.dp, end = 14.dp, bottom = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
if (hasToggle) {
|
||||
SecondaryButton("Use Wireless debugging instead", compact = true, onClick = onFixWithWirelessDebugging)
|
||||
} else {
|
||||
Body("Some ROMs add the switch anyway. Look for “child process” in Developer options.")
|
||||
SecondaryButton("Open Developer options", compact = true, onClick = onOpenDeveloperOptions)
|
||||
}
|
||||
Body("If the notification doesn't work, enter the pop-up's address and code yourself.")
|
||||
SecondaryButton("Enter address manually", compact = true, onClick = onEnterAddressManually)
|
||||
Body("From a computer with ADB:")
|
||||
Text(
|
||||
PhantomProcessLimit.gateInstructions(status),
|
||||
PhantomProcessLimit.adbCommand(),
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace, fontSize = 11.sp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
PrimaryButton("Open Developer options", onClick = onOpenDeveloperOptions)
|
||||
SecondaryButton("Use Wireless debugging", onClick = onSetUpWirelessAdb)
|
||||
SecondaryButton("Copy command", compact = true, onClick = onCopyCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdbFallback() {
|
||||
SettingsGroup("3 · Computer ADB last resort") {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
"Use this only if Developer options has no setting and Wireless debugging is unavailable. Connect this device to a computer with ADB and run:",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
Text(
|
||||
PhantomProcessLimit.ADB_COMMAND,
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun Body(text: String, modifier: Modifier = Modifier, error: Boolean = false, warn: Boolean = false) {
|
||||
Text(
|
||||
text,
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = when {
|
||||
error -> MaterialTheme.colorScheme.error
|
||||
warn -> androidx.compose.ui.graphics.Color(0xFFFFB86B)
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<!--
|
||||
Status-bar icon: Android renders it as a single-colour silhouette, so this is the launcher
|
||||
controller reduced to one path, with the d-pad and the buttons punched out (evenOdd) rather
|
||||
than drawn in a second colour that would never survive.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp" android:height="24dp"
|
||||
android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:fillColor="#FFFFFFFF" android:fillType="evenOdd"
|
||||
android:pathData="M40,36 H68 C79,36 87,44.5 87,55.5 v4 c0,6.6 -5.4,12 -12,12 c-4.5,0 -8.6,-2.5 -10.6,-6.5 L62,60 H46 l-2.4,5 C41.6,69 37.5,71.5 33,71.5 c-6.6,0 -12,-5.4 -12,-12 v-4 C21,44.5 29,36 40,36 Z M33,47.5 h4.5 v-4.5 h4.5 v4.5 h4.5 v4.5 h-4.5 v4.5 h-4.5 v-4.5 h-4.5 z M71,43 a2.5,2.5 0 1,0 0.01,0 z M77,47.5 a2.5,2.5 0 1,0 0.01,0 z M65,47.5 a2.5,2.5 0 1,0 0.01,0 z M71,52 a2.5,2.5 0 1,0 0.01,0 z M54,44 a6,6 0 1,0 0.01,0 z" />
|
||||
android:viewportWidth="239.92" android:viewportHeight="239.92">
|
||||
<group android:translateX="55.98" android:translateY="8.1">
|
||||
<path android:fillColor="#FFFFFFFF" android:fillType="evenOdd"
|
||||
android:pathData="M-31.98,0 H63.97 C125.81,0 175.93,50.08 175.93,111.86 S125.81,223.72 63.97,223.72 H-31.98 Z M-7.83,111.86 a71.81,71.81 0 1,0 143.62,0 a71.81,71.81 0 1,0 -143.62,0 Z M8,111.86 a55.98,55.98 0 1,0 111.96,0 a55.98,55.98 0 1,0 -111.96,0 Z M56.055,0 h15.83 v40.49 h-15.83 Z M56.055,183.23 h15.83 v40.49 h-15.83 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PhantomProcessLimitTest {
|
||||
@@ -26,4 +28,33 @@ class PhantomProcessLimitTest {
|
||||
assertEquals(PhantomProcessStatus.UNSET, PhantomProcessLimit.status(null, null))
|
||||
assertEquals(PhantomProcessStatus.UNSET, PhantomProcessLimit.status("", ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun android12UsesDeviceConfig() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
"device_config set_sync_disabled_for_tests persistent",
|
||||
"device_config put activity_manager max_phantom_processes 2147483647",
|
||||
),
|
||||
PhantomProcessLimit.shellCommands(enabled = false, sdk = 31),
|
||||
)
|
||||
assertTrue(PhantomProcessLimit.verified("2147483647\n", enabled = false, sdk = 31))
|
||||
assertFalse(PhantomProcessLimit.verified("null", enabled = false, sdk = 31))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun android12LAndLaterUseTheFlag() {
|
||||
assertEquals(
|
||||
listOf("settings put global settings_enable_monitor_phantom_procs false"),
|
||||
PhantomProcessLimit.shellCommands(enabled = false, sdk = 33),
|
||||
)
|
||||
assertTrue(PhantomProcessLimit.verified("false", enabled = false, sdk = 34))
|
||||
assertFalse(PhantomProcessLimit.verified("null", enabled = false, sdk = 33))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun developerToggleStartsAtAndroid14() {
|
||||
assertFalse(PhantomProcessLimit.hasDeveloperToggle(33))
|
||||
assertTrue(PhantomProcessLimit.hasDeveloperToggle(34))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user