From f07dd5eb979470ad2a0572a0143decda7dc975c3 Mon Sep 17 00:00:00 2001
From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>
Date: Tue, 27 Oct 2026 12:33:10 -0400
Subject: [PATCH] feat(setup): guided Wireless debugging onboarding for the
child-process limit (#1682)
* feat(setup): guide wireless pairing from a picture-in-picture window
* feat(setup): share one child-process fix flow between Setup and Play Steam
* fix(setup): read the child-process limit from the settings provider
* fix(setup): re-check the child-process limit while Setup is open
* fix(setup): skip a dead saved pairing and quiet the pairing guide
* fix(setup): handle repeat guide launches and start pairing after the display choice
---
app/src/main/AndroidManifest.xml | 11 +
.../droiddeck/launcher/FrontEndActivity.kt | 5 +
.../launcher/PairingGuideActivity.kt | 328 ++++++++++++++++++
.../droiddeck/launcher/core/GlobalSettings.kt | 24 ++
.../launcher/core/PhantomProcessLimit.kt | 4 +-
.../core/WirelessAdbPairingService.kt | 6 +-
.../droiddeck/launcher/ui/FrontEndContent.kt | 8 +-
.../droiddeck/launcher/ui/FrontEndScreen.kt | 46 ++-
.../droiddeck/launcher/ui/FrontEndSetup.kt | 17 +-
.../launcher/ui/PhantomProcessGatePage.kt | 8 +-
app/src/main/res/values/strings.xml | 7 +
.../launcher/ui/GlobalGlassesSettingsTest.kt | 2 +-
12 files changed, 443 insertions(+), 23 deletions(-)
create mode 100644 app/src/main/java/com/droiddeck/launcher/PairingGuideActivity.kt
create mode 100644 app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 861260492..0f9a8e9c1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -81,6 +81,17 @@
+
+
PairingGuideActivity.start(this, displayId) },
onOpenNotificationSettings = {
startActivity(Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, packageName)
diff --git a/app/src/main/java/com/droiddeck/launcher/PairingGuideActivity.kt b/app/src/main/java/com/droiddeck/launcher/PairingGuideActivity.kt
new file mode 100644
index 000000000..44fce026a
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/PairingGuideActivity.kt
@@ -0,0 +1,328 @@
+package com.droiddeck.launcher
+
+import android.app.ActivityOptions
+import android.app.AppOpsManager
+import android.app.PictureInPictureParams
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Bundle
+import android.os.Process
+import android.provider.Settings
+import android.util.Rational
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.setContent
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.droiddeck.launcher.core.AppLanguage
+import com.droiddeck.launcher.core.GlobalSettings
+import com.droiddeck.launcher.core.WirelessAdbPairingService
+import com.droiddeck.launcher.core.WirelessAdbPairingService.Stage
+import com.droiddeck.launcher.ui.DroidDeckTheme
+import com.droiddeck.launcher.ui.LocalPalette
+import com.droiddeck.launcher.ui.PrimaryButton
+import com.droiddeck.launcher.ui.SecondaryButton
+import kotlinx.coroutines.delay
+
+class PairingGuideActivity : ComponentActivity() {
+ enum class Step { BUILD_NUMBER, WIRELESS_DEBUGGING, PAIR, CODE, WORKING, DONE, FAILED }
+
+ private var settingsDisplay: Int? = null
+ private var inPip by mutableStateOf(false)
+ private var launchPending = false
+
+ override fun attachBaseContext(newBase: Context) = super.attachBaseContext(AppLanguage.wrap(newBase))
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ settingsDisplay = intent.getIntExtra(EXTRA_DISPLAY, -1).takeIf { it >= 0 }
+ setContent { DroidDeckTheme { Guide() } }
+ launchPending = savedInstanceState == null
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ setIntent(intent)
+ settingsDisplay = intent.getIntExtra(EXTRA_DISPLAY, -1).takeIf { it >= 0 }
+ launchPending = true
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (!launchPending) return
+ if (inPip || settingsDisplay != null) {
+ launchSettings()
+ return
+ }
+ enterPip()
+ if (!inPip) launchSettings()
+ }
+
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+ inPip = isInPictureInPictureMode
+ if (isInPictureInPictureMode && launchPending) launchSettings()
+ }
+
+ private fun launchSettings() {
+ launchPending = false
+ openSettings(stepFor(this, WirelessAdbPairingService.stage.value))
+ }
+
+ private fun enterPip() {
+ inPip = runCatching {
+ enterPictureInPictureMode(PictureInPictureParams.Builder().setAspectRatio(Rational(4, 3)).build())
+ }.getOrDefault(false)
+ }
+
+ private fun openSettings(step: Step) {
+ val intent = if (step == Step.BUILD_NUMBER) {
+ Intent(Settings.ACTION_DEVICE_INFO_SETTINGS).putExtra(HIGHLIGHT_KEY, "build_number")
+ } else {
+ Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).putExtra(HIGHLIGHT_KEY, "toggle_adb_wireless")
+ }.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ val display = settingsDisplay
+ runCatching {
+ if (display == null) startActivity(intent)
+ else startActivity(intent, ActivityOptions.makeBasic().setLaunchDisplayId(display).toBundle())
+ }.onFailure {
+ runCatching { startActivity(Intent(Settings.ACTION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) }
+ }
+ }
+
+ private fun returnToApp() {
+ startActivity(Intent(this, FrontEndActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT))
+ finish()
+ }
+
+ @Composable
+ private fun Guide() {
+ val stage by WirelessAdbPairingService.stage.collectAsState()
+ val settingsState by produceState(SettingsState.read(this)) {
+ while (true) {
+ value = SettingsState.read(this@PairingGuideActivity)
+ delay(500)
+ }
+ }
+ val step = stepFor(settingsState, stage)
+ var previous by remember { mutableStateOf(step) }
+ LaunchedEffect(step) {
+ if (previous == Step.BUILD_NUMBER && step == Step.WIRELESS_DEBUGGING) openSettings(step)
+ previous = step
+ when (step) {
+ Step.DONE -> { delay(1_500); returnToApp() }
+ Step.FAILED -> { delay(3_000); returnToApp() }
+ else -> Unit
+ }
+ }
+ LaunchedEffect(stage) { if (stage == Stage.Idle) finish() }
+ BackHandler(enabled = !inPip && settingsDisplay == null) { enterPip() }
+
+ Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center) {
+ if (inPip) CompactStep(step, stage)
+ else FullGuide(step, stage)
+ }
+ }
+
+ @Composable
+ private fun CompactStep(step: Step, stage: Stage) {
+ Column(Modifier.fillMaxSize().padding(10.dp), verticalArrangement = Arrangement.SpaceBetween) {
+ Text(
+ instruction(step, stage), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onBackground, maxLines = 3, overflow = TextOverflow.Ellipsis,
+ )
+ StepVisual(step, compact = true)
+ StepDots(step)
+ }
+ }
+
+ @Composable
+ private fun FullGuide(step: Step, stage: Stage) {
+ Column(
+ Modifier.widthIn(max = 520.dp).fillMaxWidth().padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Text(stringResource(R.string.gate_title), style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground)
+ Text(instruction(step, stage), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground)
+ StepVisual(step, compact = false)
+ StepDots(step)
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ if (step == Step.DONE || step == Step.FAILED) {
+ PrimaryButton(stringResource(R.string.common_back), onClick = ::returnToApp)
+ } else {
+ PrimaryButton(stringResource(R.string.gate_open_dev), onClick = {
+ if (settingsDisplay == null) enterPip()
+ openSettings(step)
+ })
+ SecondaryButton(stringResource(R.string.common_cancel), onClick = {
+ WirelessAdbPairingService.cancel(this@PairingGuideActivity)
+ returnToApp()
+ })
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun instruction(step: Step, stage: Stage): String = when (step) {
+ Step.BUILD_NUMBER -> stringResource(R.string.pairguide_build_number)
+ Step.WIRELESS_DEBUGGING -> stringResource(R.string.pairguide_wireless)
+ Step.PAIR -> stringResource(R.string.pairguide_pair)
+ Step.CODE -> (stage as? Stage.CodeNeeded)?.error ?: stringResource(R.string.gate_step3)
+ Step.WORKING -> (stage as? Stage.Working)?.step ?: stringResource(R.string.adbpair_title_working)
+ Step.DONE -> stringResource(R.string.adbpair_title_done)
+ Step.FAILED -> (stage as? Stage.Failed)?.error ?: stringResource(R.string.adbpair_title_failed)
+ }
+
+ @Composable
+ private fun StepVisual(step: Step, compact: Boolean) {
+ val colors = MaterialTheme.colorScheme
+ val accent = LocalPalette.current.signal
+ val pulse = rememberInfiniteTransition(label = "guidePulse")
+ val beat by pulse.animateFloat(0f, 1f, infiniteRepeatable(tween(900, easing = LinearEasing), RepeatMode.Restart), label = "beat")
+ val label = when (step) {
+ Step.BUILD_NUMBER -> stringResource(R.string.pairguide_row_build_number)
+ Step.WIRELESS_DEBUGGING -> stringResource(R.string.pairguide_row_wireless)
+ Step.PAIR -> stringResource(R.string.pairguide_row_pair)
+ Step.CODE -> stringResource(R.string.adbpair_enter_code)
+ Step.WORKING, Step.DONE, Step.FAILED -> return
+ }
+ val rowHeight = if (compact) 34.dp else 56.dp
+ Row(
+ Modifier.fillMaxWidth().height(rowHeight).background(colors.surfaceVariant, RoundedCornerShape(10.dp))
+ .border(1.dp, accent.copy(alpha = 0.35f), RoundedCornerShape(10.dp)).padding(horizontal = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(label, style = if (compact) MaterialTheme.typography.labelMedium else MaterialTheme.typography.bodyLarge,
+ color = colors.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
+ when (step) {
+ Step.WIRELESS_DEBUGGING -> Toggle(on = beat > 0.5f, accent = accent, compact = compact)
+ else -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
+ if (step == Step.BUILD_NUMBER) Text("×7", style = MaterialTheme.typography.labelLarge, color = accent)
+ Box(contentAlignment = Alignment.Center) {
+ Box(Modifier.size(if (compact) 18.dp else 26.dp).scale(0.6f + beat * 0.8f).alpha(1f - beat)
+ .background(accent, CircleShape))
+ Box(Modifier.size(if (compact) 8.dp else 12.dp).background(accent, CircleShape))
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun Toggle(on: Boolean, accent: Color, compact: Boolean) {
+ val width = if (compact) 26.dp else 40.dp
+ val knob = if (compact) 12.dp else 18.dp
+ Box(
+ Modifier.width(width).height(knob + 4.dp)
+ .background(if (on) accent.copy(alpha = 0.6f) else MaterialTheme.colorScheme.outline, CircleShape).padding(2.dp),
+ contentAlignment = if (on) Alignment.CenterEnd else Alignment.CenterStart,
+ ) { Box(Modifier.size(knob).background(Color.White, CircleShape)) }
+ }
+
+ @Composable
+ private fun StepDots(step: Step) {
+ val accent = LocalPalette.current.signal
+ val shown = listOf(Step.BUILD_NUMBER, Step.WIRELESS_DEBUGGING, Step.PAIR, Step.CODE)
+ val at = shown.indexOf(step).takeIf { it >= 0 } ?: shown.size
+ Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically) {
+ shown.forEachIndexed { index, _ ->
+ Box(Modifier.size(if (index == at) 8.dp else 6.dp)
+ .background(if (index <= at) accent else MaterialTheme.colorScheme.outline, CircleShape))
+ }
+ Spacer(Modifier.width(2.dp))
+ }
+ }
+
+ private data class SettingsState(val developerOptions: Boolean, val wirelessDebugging: Boolean) {
+ companion object {
+ fun read(context: Context) = SettingsState(
+ GlobalSettings.developerOptionsOn(context),
+ GlobalSettings.wirelessDebuggingOn(context),
+ )
+ }
+ }
+
+ companion object {
+ private const val EXTRA_DISPLAY = "settingsDisplay"
+ private const val HIGHLIGHT_KEY = ":settings:fragment_args_key"
+
+ private fun stepFor(context: Context, stage: Stage) = stepFor(SettingsState.read(context), stage)
+
+ private fun stepFor(settings: SettingsState, stage: Stage): Step = when (stage) {
+ is Stage.Working -> Step.WORKING
+ Stage.Done -> Step.DONE
+ is Stage.Failed -> Step.FAILED
+ is Stage.CodeNeeded -> Step.CODE
+ else -> when {
+ !settings.developerOptions -> Step.BUILD_NUMBER
+ !settings.wirelessDebugging -> Step.WIRELESS_DEBUGGING
+ else -> Step.PAIR
+ }
+ }
+
+ fun canFloat(context: Context): Boolean {
+ if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) return false
+ val ops = context.getSystemService(AppOpsManager::class.java) ?: return true
+ val mode = if (Build.VERSION.SDK_INT >= 29) {
+ ops.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), context.packageName)
+ } else {
+ @Suppress("DEPRECATION")
+ ops.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), context.packageName)
+ }
+ return mode == AppOpsManager.MODE_ALLOWED
+ }
+
+ fun start(context: Context, settingsDisplay: Int?): Boolean {
+ if (settingsDisplay == null && !canFloat(context)) return false
+ context.startActivity(Intent(context, PairingGuideActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .apply { if (settingsDisplay != null) putExtra(EXTRA_DISPLAY, settingsDisplay) })
+ return true
+ }
+ }
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt b/app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt
new file mode 100644
index 000000000..8cbf9bf39
--- /dev/null
+++ b/app/src/main/java/com/droiddeck/launcher/core/GlobalSettings.kt
@@ -0,0 +1,24 @@
+package com.droiddeck.launcher.core
+
+import android.content.Context
+import android.net.Uri
+import android.provider.Settings
+
+object GlobalSettings {
+ // Settings.Global caches values in-process and misses deletions made over adb; ask the provider directly.
+ fun read(context: Context, name: String): String? {
+ val uri = Uri.withAppendedPath(Settings.Global.CONTENT_URI, name)
+ context.contentResolver.query(uri, arrayOf(Settings.NameValueTable.VALUE), null, null, null)?.use { cursor ->
+ return if (cursor.moveToFirst()) cursor.getString(0) else null
+ }
+ return Settings.Global.getString(context.contentResolver, name)
+ }
+
+ fun isOn(context: Context, name: String): Boolean =
+ runCatching { read(context, name)?.trim() == "1" }.getOrDefault(false)
+
+ fun developerOptionsOn(context: Context): Boolean = isOn(context, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED)
+
+ // Settings.Global.ADB_WIFI_ENABLED is hidden; the key is stable since Android 11.
+ fun wirelessDebuggingOn(context: Context): Boolean = isOn(context, "adb_wifi_enabled")
+}
diff --git a/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt b/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt
index 6ad5f89b6..6e67ff495 100644
--- a/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt
+++ b/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt
@@ -60,14 +60,14 @@ object PhantomProcessLimit {
if (usesDeviceConfig(sdk)) {
val off = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(PREF_ANDROID_12_OFF, false)
val global = try {
- Settings.Global.getString(context.contentResolver, SETTING)
+ GlobalSettings.read(context, SETTING)
} catch (_: Exception) {
null
}
return android12Status(off, global, systemProperty(OVERRIDE_PROPERTY))
}
val global = try {
- Settings.Global.getString(context.contentResolver, SETTING)
+ GlobalSettings.read(context, SETTING)
} catch (_: Exception) {
return PhantomProcessStatus.UNREADABLE
}
diff --git a/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbPairingService.kt b/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbPairingService.kt
index c924f8532..2d6adb902 100644
--- a/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbPairingService.kt
+++ b/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbPairingService.kt
@@ -184,12 +184,15 @@ class WirelessAdbPairingService : Service() {
setSound(null, null)
enableVibration(false)
})
+ manager?.createNotificationChannel(NotificationChannel(PROGRESS_CHANNEL_ID, getString(R.string.adbpair_progress_channel_name),
+ NotificationManager.IMPORTANCE_LOW).apply { setShowBadge(false) })
val open = PendingIntent.getActivity(this, 0,
Intent(this, FrontEndActivity::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)
+ val quiet = stage == Stage.Waiting || stage == Stage.Idle || stage is Stage.Working
+ val builder = Notification.Builder(this, if (quiet) PROGRESS_CHANNEL_ID else CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_session)
.setContentIntent(open)
.setShowWhen(false)
@@ -239,6 +242,7 @@ class WirelessAdbPairingService : Service() {
companion object {
private const val CHANNEL_ID = "wireless-adb-pairing"
+ private const val PROGRESS_CHANNEL_ID = "wireless-adb-pairing-progress"
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"
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt
index b70f9bb5e..419329016 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt
@@ -105,7 +105,7 @@ import java.io.File
internal fun Pane(
s: FrontEndState, selected: String, a: FrontEndActions, page: (@Composable () -> Unit)?, modifier: Modifier,
onSelect: (String) -> Unit, onAndroidAppClick: (HomeApp.LaunchableApp) -> Unit,
- onOpenDeveloperOptions: () -> Unit, onRequestWirelessAdb: (Boolean) -> Unit,
+ onOpenDeveloperOptions: () -> Unit, onRequestWirelessAdb: (Boolean) -> Unit, onPairWirelessDebugging: () -> Unit,
) {
BoxWithConstraints(modifier = modifier) {
CompositionLocalProvider(LocalNarrowPane provides (maxWidth < NarrowPaneWidth)) {
@@ -162,7 +162,7 @@ internal fun Pane(
label = "pane",
) { key ->
val drawn = (if (page != null && key == s.pageKey) page else pages[key])
- ?: { Content(s, if (key == "games") gameShown[0] else key, a, Modifier.fillMaxSize(), onSelect, onAndroidAppClick, onOpenDeveloperOptions, onRequestWirelessAdb) }
+ ?: { Content(s, if (key == "games") gameShown[0] else key, a, Modifier.fillMaxSize(), onSelect, onAndroidAppClick, onOpenDeveloperOptions, onRequestWirelessAdb, onPairWirelessDebugging) }
val hopSink by androidx.compose.animation.core.animateFloatAsState(
if (Hops.sinking == key) 1f else 0f, Motion.tw(260), label = "hopSink",
)
@@ -223,7 +223,7 @@ private fun romFor(s: FrontEndState, selected: String): Pair Unit, onAndroidAppClick: (HomeApp.LaunchableApp) -> Unit,
- onOpenDeveloperOptions: () -> Unit, onRequestWirelessAdb: (Boolean) -> Unit,
+ onOpenDeveloperOptions: () -> Unit, onRequestWirelessAdb: (Boolean) -> Unit, onPairWirelessDebugging: () -> Unit,
) {
val colors = MaterialTheme.colorScheme
val detailPosterWidth = if (LocalConfiguration.current.screenHeightDp < 600) 72.dp else 120.dp
@@ -233,7 +233,7 @@ private fun Content(
val padV = if (narrow) 12.dp else 18.dp
if (selected == "setup") {
Column(modifier = modifier.padding(horizontal = padH, vertical = padV)) {
- SetupPanel(s, a, onOpenDeveloperOptions, onRequestWirelessAdb)
+ SetupPanel(s, a, onOpenDeveloperOptions, onRequestWirelessAdb, onPairWirelessDebugging)
}
return
}
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
index e75994776..3223826f3 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt
@@ -255,6 +255,8 @@ class FrontEndActions(
val onCopyPhantomCommand: (Boolean) -> Unit = {},
val onDismissPhantomGate: () -> Unit = {},
val onStartWirelessAdbPairing: () -> Unit = {},
+ val onStartPairingGuide: (Int?) -> Boolean = { false },
+ val onShowPhantomGate: () -> Unit = {},
val onOpenNotificationSettings: () -> Unit = {},
val controller: ControllerActions? = null,
val updates: UpdatesActions = UpdatesActions(),
@@ -425,6 +427,7 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
}
var showWirelessAdbFix by rememberSaveable { mutableStateOf(false) }
var showDeveloperDisplayChoice by rememberSaveable { mutableStateOf(false) }
+ var developerChoiceGuided by rememberSaveable { mutableStateOf(false) }
var wirelessAdbDesiredEnabled by rememberSaveable { mutableStateOf(false) }
var appToChooseDisplay by remember { mutableStateOf(null) }
val colors = MaterialTheme.colorScheme
@@ -433,9 +436,36 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
val phantomGateVisible = s.showPhantomGate && PhantomProcessLimit.blocksSteam(s.phantomProcessStatus)
val processSettingsPageVisible = phantomGateVisible || showWirelessAdbFix || showDeveloperDisplayChoice
val requestDeveloperOptions = {
+ developerChoiceGuided = false
if (s.secondScreenDisplays.isEmpty()) a.onOpenDeveloperOptions(null)
else showDeveloperDisplayChoice = true
}
+ val openDeveloperOptionsOn: (Int?) -> Unit = { displayId ->
+ if (developerChoiceGuided) a.onStartWirelessAdbPairing()
+ if (!developerChoiceGuided || !a.onStartPairingGuide(displayId)) a.onOpenDeveloperOptions(displayId)
+ developerChoiceGuided = false
+ }
+ val startGuidedPairing = {
+ if (!com.droiddeck.launcher.core.WirelessAdbPairingService.notificationsEnabled(ctx)) {
+ wirelessAdbDesiredEnabled = false
+ showWirelessAdbFix = true
+ } else {
+ developerChoiceGuided = true
+ if (s.secondScreenDisplays.isEmpty()) openDeveloperOptionsOn(null)
+ else showDeveloperDisplayChoice = true
+ }
+ }
+ var applyingSavedPairing by remember { mutableStateOf(false) }
+ val fixPhantomLimit = {
+ if (!s.adbPaired || !com.droiddeck.launcher.core.GlobalSettings.wirelessDebuggingOn(ctx)) startGuidedPairing()
+ else if (!applyingSavedPairing) {
+ applyingSavedPairing = true
+ a.onSetPhantomProcessLimit(false) { error ->
+ applyingSavedPairing = false
+ if (error != null) startGuidedPairing()
+ }
+ }
+ }
val requestWirelessAdbFix: (Boolean) -> Unit = { enabled ->
wirelessAdbDesiredEnabled = enabled
showWirelessAdbFix = true
@@ -586,6 +616,7 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
},
onOpenDeveloperOptions = requestDeveloperOptions,
onRequestWirelessAdb = requestWirelessAdbFix,
+ onPairWirelessDebugging = startGuidedPairing,
)
}
@@ -619,10 +650,8 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
status = s.phantomProcessStatus,
onDismiss = a.onDismissPhantomGate,
onOpenDeveloperOptions = requestDeveloperOptions,
- onFixWithWirelessDebugging = {
- a.onStartWirelessAdbPairing()
- requestDeveloperOptions()
- },
+ applyingSavedPairing = applyingSavedPairing,
+ onFixWithWirelessDebugging = fixPhantomLimit,
onEnterAddressManually = { requestWirelessAdbFix(false) },
onCopyCommand = { a.onCopyPhantomCommand(false) },
onOpenNotificationSettings = a.onOpenNotificationSettings,
@@ -648,13 +677,16 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
},
onMainScreen = {
showDeveloperDisplayChoice = false
- a.onOpenDeveloperOptions(null)
+ openDeveloperOptionsOn(null)
},
onSecondaryScreen = { displayId ->
showDeveloperDisplayChoice = false
- a.onOpenDeveloperOptions(displayId)
+ openDeveloperOptionsOn(displayId)
+ },
+ onDismiss = {
+ showDeveloperDisplayChoice = false
+ developerChoiceGuided = false
},
- onDismiss = { showDeveloperDisplayChoice = false },
)
}
}
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt
index d5932762f..754094913 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt
@@ -141,6 +141,7 @@ internal fun SetupPanel(
a: FrontEndActions,
onOpenDeveloperOptions: () -> Unit,
onRequestWirelessAdb: (Boolean) -> Unit,
+ onPairWirelessDebugging: () -> Unit,
) {
val host = rememberMenuHost()
val ctx = LocalContext.current
@@ -203,6 +204,12 @@ internal fun SetupPanel(
tab = STORAGE_TAB
}
}
+ LaunchedEffect(Unit) {
+ while (true) {
+ kotlinx.coroutines.delay(2_000)
+ a.onRefreshPhantomStatus()
+ }
+ }
LaunchedEffect(tab) {
if (tabTurned && inputModeManager.inputMode == InputMode.Keyboard) {
androidx.compose.runtime.withFrameNanos { }
@@ -279,7 +286,7 @@ internal fun SetupPanel(
},
divider = divider && !showLimitDetails,
) {
- if (limitBlocks) PrimaryButton(if (showLimitDetails) stringResource(R.string.common_hide) else stringResource(R.string.setup_fix_it), compact = true) { showLimitDetails = !showLimitDetails }
+ if (limitBlocks) PrimaryButton(stringResource(R.string.setup_fix_it), compact = true, onClick = a.onShowPhantomGate)
else if (s.phantomProcessStatus != PhantomProcessStatus.NOT_APPLICABLE) {
SecondaryButton(if (showLimitDetails) stringResource(R.string.common_hide) else stringResource(R.string.setup_details), compact = true) { showLimitDetails = !showLimitDetails }
}
@@ -292,11 +299,7 @@ internal fun SetupPanel(
fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 4.dp),
)
Actions {
- if (limitBlocks) {
- PrimaryButton(stringResource(R.string.setup_dev_options), compact = true, onClick = onOpenDeveloperOptions)
- SecondaryButton(stringResource(R.string.setup_use_wireless), compact = true, enabled = !processLimitBusy) { setProcessLimit(false) }
- SecondaryButton(stringResource(R.string.setup_check_again), compact = true, onClick = a.onRefreshPhantomStatus)
- } else if (s.phantomProcessStatus == PhantomProcessStatus.DISABLED) {
+ if (s.phantomProcessStatus == PhantomProcessStatus.DISABLED) {
SecondaryButton(stringResource(R.string.setup_limit_turn_on), compact = true, enabled = !processLimitBusy) { setProcessLimit(true) }
}
}
@@ -509,7 +512,7 @@ internal fun SetupPanel(
)
if (!s.adbPaired) ActionRow(
stringResource(R.string.setup_adb_pair), stringResource(R.string.setup_adb_pair_hint),
- stringResource(R.string.setup_adb_pair_button), { onRequestWirelessAdb(false) },
+ stringResource(R.string.setup_adb_pair_button), onPairWirelessDebugging,
)
}
}
diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PhantomProcessGatePage.kt b/app/src/main/java/com/droiddeck/launcher/ui/PhantomProcessGatePage.kt
index 517309ec0..60b4223f0 100644
--- a/app/src/main/java/com/droiddeck/launcher/ui/PhantomProcessGatePage.kt
+++ b/app/src/main/java/com/droiddeck/launcher/ui/PhantomProcessGatePage.kt
@@ -45,6 +45,7 @@ import com.droiddeck.launcher.core.WirelessAdbPairingService.Stage
@Composable
fun PhantomProcessGatePage(
status: PhantomProcessStatus,
+ applyingSavedPairing: Boolean,
onDismiss: () -> Unit,
onOpenDeveloperOptions: () -> Unit,
onFixWithWirelessDebugging: () -> Unit,
@@ -77,7 +78,7 @@ fun PhantomProcessGatePage(
BoxWithConstraints(Modifier.fillMaxWidth()) {
val wide = maxWidth >= 620.dp
val fix: @Composable ColumnScope.() -> Unit = {
- FixGroup(hasToggle, stage, environment, compact, onOpenDeveloperOptions, onFixWithWirelessDebugging,
+ FixGroup(hasToggle, stage, environment, compact, applyingSavedPairing, onOpenDeveloperOptions, onFixWithWirelessDebugging,
onOpenNotificationSettings, onCancel = { WirelessAdbPairingService.cancel(context) })
}
val side: @Composable ColumnScope.() -> Unit = {
@@ -119,6 +120,7 @@ private fun FixGroup(
stage: Stage,
environment: GateEnvironment,
compact: Boolean,
+ applyingSavedPairing: Boolean,
onOpenDeveloperOptions: () -> Unit,
onFixWithWirelessDebugging: () -> Unit,
onOpenNotificationSettings: () -> Unit,
@@ -131,6 +133,10 @@ private fun FixGroup(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
when {
+ applyingSavedPairing -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp)
+ Body(stringResource(R.string.adbpair_applying))
+ }
pairing -> PairingProgress(stage, onCancel)
hasToggle -> {
Body(stringResource(R.string.gate_toggle_steps))
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fc6649cc8..955caad10 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -990,6 +990,12 @@
Open Settings → Developer options → Wireless debugging and turn it on
Tap “Pair device with pairing code”
Type the code into the DroidDeck notification
+ In About phone, tap Build number 7 times
+ Turn on Wireless debugging
+ Open Wireless debugging, then tap “Pair device with pairing code”
+ Build number
+ Wireless debugging
+ Pair device with pairing code
Waiting for the pairing pop-up…
Pop-up found. Enter its code in the notification.
Status
@@ -1849,6 +1855,7 @@
Applying the setting…
Wireless debugging command failed
Wireless debugging setup
+ Wireless debugging setup progress
Asks for the Wireless debugging pairing code while Settings is open
Pair Wireless debugging
Turn on Wireless debugging, then tap “Pair device with pairing code”.
diff --git a/app/src/test/java/com/droiddeck/launcher/ui/GlobalGlassesSettingsTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/GlobalGlassesSettingsTest.kt
index b64986a1e..62c90ce94 100644
--- a/app/src/test/java/com/droiddeck/launcher/ui/GlobalGlassesSettingsTest.kt
+++ b/app/src/test/java/com/droiddeck/launcher/ui/GlobalGlassesSettingsTest.kt
@@ -144,7 +144,7 @@ class GlobalGlassesSettingsTest {
onRoms = {}, onFiles = {}, onLogs = {}, onOffline = {},
onWirelessAdbPair = { _, _, _, _ -> }, onWirelessAdbApply = { _, _, _, _ -> },
onSetPhantomProcessLimit = { _, _ -> },
- ), onOpenDeveloperOptions = {}, onRequestWirelessAdb = {})
+ ), onOpenDeveloperOptions = {}, onRequestWirelessAdb = {}, onPairWirelessDebugging = {})
}
}
}