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 = {}) } } }