mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
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:
1 parent
067dabe81c
commit
f07dd5eb97
12 files changed
+443
-23
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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 = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user