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
This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-27 12:33:10 -04:00
1 parent 067dabe81c
commit f07dd5eb97
12 files changed
+443 -23

No files matched your search

+11
View File
@@ -81,6 +81,17 @@
</intent-filter>
</activity>
<activity
android:name=".PairingGuideActivity"
android:exported="false"
android:launchMode="singleTask"
android:taskAffinity="com.droiddeck.launcher.pairingguide"
android:excludeFromRecents="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|density|keyboard|keyboardHidden|navigation"
android:theme="@style/AppTheme" />
<activity
android:name=".FrontEndActivity"
android:exported="true"
@@ -1274,7 +1274,12 @@ class FrontEndActivity : ComponentActivity() {
android.widget.Toast.makeText(this, R.string.main_adb_copied, android.widget.Toast.LENGTH_SHORT).show()
},
onDismissPhantomGate = { showPhantomGate = false },
onShowPhantomGate = {
refreshPhantomStatus()
showPhantomGate = true
},
onStartWirelessAdbPairing = { WirelessAdbPairingService.start(this) },
onStartPairingGuide = { displayId -> PairingGuideActivity.start(this, displayId) },
onOpenNotificationSettings = {
startActivity(Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, packageName)
@@ -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
}
}
}
@@ -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")
}
@@ -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
}
@@ -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"
@@ -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<Library.Emulator, L
private fun Content(
s: FrontEndState, selected: String, a: FrontEndActions, modifier: Modifier,
onSelect: (String) -> 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
}
@@ -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<HomeApp.LaunchableApp?>(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 },
)
}
}
@@ -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,
)
}
}
@@ -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))
+7
View File
@@ -990,6 +990,12 @@
<string name="gate_step1">Open Settings → Developer options → Wireless debugging and turn it on</string>
<string name="gate_step2">Tap “Pair device with pairing code”</string>
<string name="gate_step3">Type the code into the DroidDeck notification</string>
<string name="pairguide_build_number">In About phone, tap Build number 7 times</string>
<string name="pairguide_wireless">Turn on Wireless debugging</string>
<string name="pairguide_pair">Open Wireless debugging, then tap “Pair device with pairing code”</string>
<string name="pairguide_row_build_number">Build number</string>
<string name="pairguide_row_wireless">Wireless debugging</string>
<string name="pairguide_row_pair">Pair device with pairing code</string>
<string name="gate_waiting">Waiting for the pairing pop-up…</string>
<string name="gate_code_needed">Pop-up found. Enter its code in the notification.</string>
<string name="gate_status">Status</string>
@@ -1849,6 +1855,7 @@
<string name="adbpair_applying">Applying the setting…</string>
<string name="adbpair_command_failed">Wireless debugging command failed</string>
<string name="adbpair_channel_name">Wireless debugging setup</string>
<string name="adbpair_progress_channel_name">Wireless debugging setup progress</string>
<string name="adbpair_channel_description">Asks for the Wireless debugging pairing code while Settings is open</string>
<string name="adbpair_title_pair">Pair Wireless debugging</string>
<string name="adbpair_waiting_text">Turn on Wireless debugging, then tap “Pair device with pairing code”.</string>
@@ -144,7 +144,7 @@ class GlobalGlassesSettingsTest {
onRoms = {}, onFiles = {}, onLogs = {}, onOffline = {},
onWirelessAdbPair = { _, _, _, _ -> }, onWirelessAdbApply = { _, _, _, _ -> },
onSetPhantomProcessLimit = { _, _ -> },
), onOpenDeveloperOptions = {}, onRequestWirelessAdb = {})
), onOpenDeveloperOptions = {}, onRequestWirelessAdb = {}, onPairWirelessDebugging = {})
}
}
}