@@ -32,18 +32,36 @@ class PcServiceConnectionController(
3232 private val _state = MutableStateFlow <PcServiceConnectionState >(PcServiceConnectionState .Disconnected )
3333 val state: StateFlow <PcServiceConnectionState > = _state
3434
35- suspend fun connectOrRequestAccess (onWaitingForApproval : (PcApprovalCodeState ) -> Unit = {}): PcServiceConnectResult {
36- (PcConnectionStateHolder .connectionState.value as ? PcConnectionState .Connected )?.let {
37- _state .value = PcServiceConnectionState .Connected (it.session, it.displayName)
38- return PcServiceConnectResult .Connected (it.session, it.displayName)
35+ /* *
36+ * Discovers Switchify PCs on the local network.
37+ *
38+ * Waits up to [DISCOVERY_TIMEOUT_MS] for the first PC to resolve, then keeps a short
39+ * settle window open so slower PCs can join the list before it is returned. This avoids
40+ * deciding on a single PC just because it won the mDNS resolve race.
41+ */
42+ suspend fun discoverPcs (): List <DiscoveredPc > {
43+ discovery.startDiscovery()
44+ try {
45+ var current = withTimeoutOrNull(DISCOVERY_TIMEOUT_MS ) {
46+ discovery.pcs.first { pcs -> pcs.isNotEmpty() }
47+ } ? : return emptyList()
48+ while (true ) {
49+ val grown = withTimeoutOrNull(SETTLE_WINDOW_MS ) {
50+ discovery.pcs.first { pcs -> pcs.size > current.size }
51+ } ? : break
52+ current = grown
53+ }
54+ return current
55+ } finally {
56+ discovery.stopDiscovery()
3957 }
58+ }
59+
60+ suspend fun connectOrRequestAccess (onWaitingForApproval : (PcApprovalCodeState ) -> Unit = {}): PcServiceConnectResult {
61+ existingConnection()?.let { return it }
4062
4163 _state .value = PcServiceConnectionState .Connecting
42- discovery.startDiscovery()
43- val discovered = withTimeoutOrNull(8_000 ) {
44- discovery.pcs.first { pcs -> pcs.isNotEmpty() }
45- }.orEmpty()
46- discovery.stopDiscovery()
64+ val discovered = discoverPcs()
4765
4866 for (pc in discovered) {
4967 tokenStore.getToken(pc.desktopId)?.let { token ->
@@ -54,31 +72,21 @@ class PcServiceConnectionController(
5472 }
5573 }
5674
75+ var lastFailure: PcServiceConnectResult .Failed ? = null
5776 for (pc in discovered) {
58- val requestNonce = requestNonceProvider()
59- val verificationCode = createPairingVerificationCode(
60- desktopId = pc.desktopId,
61- deviceId = identityRepository.getDeviceId(),
62- requestNonce = requestNonce
63- )
64- onWaitingForApproval(PcApprovalCodeState (pc.displayName, verificationCode))
65- when (val result = connector.requestApproval(pc, requestNonce)) {
66- is PcPairingResult .Paired -> {
67- tokenStore.saveToken(result.desktopId, result.token, result.websocketUrl, pc.displayName)
68- when (val ping = connectWithToken(pc, result.token)) {
69- is PcServiceConnectResult .Connected -> return ping
70- is PcServiceConnectResult .Failed -> return ping
77+ when (val result = pairAndConnect(pc, onWaitingForApproval)) {
78+ is PcServiceConnectResult .Connected -> return result
79+ is PcServiceConnectResult .Failed -> {
80+ if (isUserDecision(result.reason)) {
81+ _state .value = PcServiceConnectionState .Failed (result.message)
82+ return result
7183 }
72- }
73- is PcPairingResult .Failed -> {
74- val failure = PcServiceConnectResult .Failed (result.reason, result.message)
75- _state .value = PcServiceConnectionState .Failed (result.message)
76- return failure
84+ lastFailure = result
7785 }
7886 }
7987 }
8088
81- val failure = if (discovered.isEmpty()) {
89+ val failure = lastFailure ? : if (discovered.isEmpty()) {
8290 PcServiceConnectResult .Failed (PcErrorReason .NoPcFound , " No Switchify PC found." )
8391 } else {
8492 PcServiceConnectResult .Failed (PcErrorReason .Failed , " Could not connect to PC." )
@@ -87,6 +95,34 @@ class PcServiceConnectionController(
8795 return failure
8896 }
8997
98+ /* *
99+ * Connects to a specific, user-selected PC. Tries a saved token first; if the token is
100+ * missing or expired, falls through to a fresh pairing request against that PC.
101+ */
102+ suspend fun connectTo (
103+ pc : DiscoveredPc ,
104+ onWaitingForApproval : (PcApprovalCodeState ) -> Unit = {}
105+ ): PcServiceConnectResult {
106+ existingConnection()?.takeIf { it.session.desktopId == pc.desktopId }?.let { return it }
107+
108+ _state .value = PcServiceConnectionState .Connecting
109+ tokenStore.getToken(pc.desktopId)?.let { token ->
110+ when (val result = connectWithToken(pc, token)) {
111+ is PcServiceConnectResult .Connected -> return result
112+ is PcServiceConnectResult .Failed -> if (result.reason != PcErrorReason .AuthExpired ) {
113+ _state .value = PcServiceConnectionState .Failed (result.message)
114+ return result
115+ }
116+ }
117+ }
118+
119+ val result = pairAndConnect(pc, onWaitingForApproval)
120+ if (result is PcServiceConnectResult .Failed ) {
121+ _state .value = PcServiceConnectionState .Failed (result.message)
122+ }
123+ return result
124+ }
125+
90126 suspend fun sendCommand (command : PcControlCommand ): PcCommandResult {
91127 val connected = PcConnectionStateHolder .connectionState.value as ? PcConnectionState .Connected
92128 ? : return PcCommandResult .AuthFailed ()
@@ -104,6 +140,36 @@ class PcServiceConnectionController(
104140 connector.close()
105141 }
106142
143+ private fun existingConnection (): PcServiceConnectResult .Connected ? {
144+ val connected = PcConnectionStateHolder .connectionState.value as ? PcConnectionState .Connected ? : return null
145+ _state .value = PcServiceConnectionState .Connected (connected.session, connected.displayName)
146+ return PcServiceConnectResult .Connected (connected.session, connected.displayName)
147+ }
148+
149+ private suspend fun pairAndConnect (
150+ pc : DiscoveredPc ,
151+ onWaitingForApproval : (PcApprovalCodeState ) -> Unit
152+ ): PcServiceConnectResult {
153+ val requestNonce = requestNonceProvider()
154+ val verificationCode = createPairingVerificationCode(
155+ desktopId = pc.desktopId,
156+ deviceId = identityRepository.getDeviceId(),
157+ requestNonce = requestNonce
158+ )
159+ onWaitingForApproval(PcApprovalCodeState (pc.displayName, verificationCode))
160+ return when (val result = connector.requestApproval(pc, requestNonce)) {
161+ is PcPairingResult .Paired -> {
162+ tokenStore.saveToken(result.desktopId, result.token, result.websocketUrl, pc.displayName)
163+ connectWithToken(pc, result.token)
164+ }
165+ is PcPairingResult .Failed -> PcServiceConnectResult .Failed (result.reason, result.message)
166+ }
167+ }
168+
169+ private fun isUserDecision (reason : PcErrorReason ): Boolean {
170+ return reason == PcErrorReason .PairingRejected || reason == PcErrorReason .PairingRequestExpired
171+ }
172+
107173 private suspend fun connectWithToken (pc : DiscoveredPc , token : String ): PcServiceConnectResult {
108174 return when (val result = connector.authenticatedPing(pc, token)) {
109175 is PcPingResult .Connected -> {
@@ -125,5 +191,7 @@ class PcServiceConnectionController(
125191
126192 companion object {
127193 private const val EXPIRED_MESSAGE = " Connection expired. Request access again."
194+ private const val DISCOVERY_TIMEOUT_MS = 8_000L
195+ private const val SETTLE_WINDOW_MS = 1_500L
128196 }
129197}
0 commit comments