android, desktop: connect remote desktop via multicast (#3442)

* android, desktop: connect remote desktop via multicast

* changes

* string

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2023-11-24 05:00:11 +08:00 committed by GitHub
parent f7903c5c83
commit b2dbb558f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 200 additions and 36 deletions

View File

@ -2921,7 +2921,7 @@ enum class NotificationPreviewMode {
} }
data class RemoteCtrlSession( data class RemoteCtrlSession(
val ctrlAppInfo: CtrlAppInfo, val ctrlAppInfo: CtrlAppInfo?,
val appVersion: String, val appVersion: String,
val sessionState: UIRemoteCtrlSessionState val sessionState: UIRemoteCtrlSessionState
) { ) {
@ -2939,6 +2939,7 @@ data class RemoteCtrlSession(
@Serializable @Serializable
sealed class RemoteCtrlSessionState { sealed class RemoteCtrlSessionState {
@Serializable @SerialName("starting") object Starting: RemoteCtrlSessionState() @Serializable @SerialName("starting") object Starting: RemoteCtrlSessionState()
@Serializable @SerialName("searching") object Searching: RemoteCtrlSessionState()
@Serializable @SerialName("connecting") object Connecting: RemoteCtrlSessionState() @Serializable @SerialName("connecting") object Connecting: RemoteCtrlSessionState()
@Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val sessionCode: String): RemoteCtrlSessionState() @Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val sessionCode: String): RemoteCtrlSessionState()
@Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteCtrlSessionState() @Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteCtrlSessionState()
@ -2946,6 +2947,8 @@ sealed class RemoteCtrlSessionState {
sealed class UIRemoteCtrlSessionState { sealed class UIRemoteCtrlSessionState {
@Serializable @SerialName("starting") object Starting: UIRemoteCtrlSessionState() @Serializable @SerialName("starting") object Starting: UIRemoteCtrlSessionState()
@Serializable @SerialName("searching") object Searching: UIRemoteCtrlSessionState()
@Serializable @SerialName("found") data class Found(val remoteCtrl: RemoteCtrlInfo, val compatible: Boolean): UIRemoteCtrlSessionState()
@Serializable @SerialName("connecting") data class Connecting(val remoteCtrl_: RemoteCtrlInfo? = null): UIRemoteCtrlSessionState() @Serializable @SerialName("connecting") data class Connecting(val remoteCtrl_: RemoteCtrlInfo? = null): UIRemoteCtrlSessionState()
@Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val remoteCtrl_: RemoteCtrlInfo? = null, val sessionCode: String): UIRemoteCtrlSessionState() @Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val remoteCtrl_: RemoteCtrlInfo? = null, val sessionCode: String): UIRemoteCtrlSessionState()
@Serializable @SerialName("connected") data class Connected(val remoteCtrl: RemoteCtrlInfo, val sessionCode: String): UIRemoteCtrlSessionState() @Serializable @SerialName("connected") data class Connected(val remoteCtrl: RemoteCtrlInfo, val sessionCode: String): UIRemoteCtrlSessionState()

View File

@ -170,6 +170,7 @@ class AppPreferences {
val confirmRemoteSessions = mkBoolPreference(SHARED_PREFS_CONFIRM_REMOTE_SESSIONS, false) val confirmRemoteSessions = mkBoolPreference(SHARED_PREFS_CONFIRM_REMOTE_SESSIONS, false)
val connectRemoteViaMulticast = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST, false) val connectRemoteViaMulticast = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST, false)
val connectRemoteViaMulticastAuto = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO, true)
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true) val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
private fun mkIntPreference(prefName: String, default: Int) = private fun mkIntPreference(prefName: String, default: Int) =
@ -314,6 +315,7 @@ class AppPreferences {
private const val SHARED_PREFS_DEVICE_NAME_FOR_REMOTE_ACCESS = "DeviceNameForRemoteAccess" private const val SHARED_PREFS_DEVICE_NAME_FOR_REMOTE_ACCESS = "DeviceNameForRemoteAccess"
private const val SHARED_PREFS_CONFIRM_REMOTE_SESSIONS = "ConfirmRemoteSessions" private const val SHARED_PREFS_CONFIRM_REMOTE_SESSIONS = "ConfirmRemoteSessions"
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST = "ConnectRemoteViaMulticast" private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST = "ConnectRemoteViaMulticast"
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast" private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
} }
} }
@ -1432,14 +1434,25 @@ object ChatController {
suspend fun connectRemoteCtrl(desktopAddress: String): Pair<SomeRemoteCtrl?, CR.ChatCmdError?> { suspend fun connectRemoteCtrl(desktopAddress: String): Pair<SomeRemoteCtrl?, CR.ChatCmdError?> {
val r = sendCmd(null, CC.ConnectRemoteCtrl(desktopAddress)) val r = sendCmd(null, CC.ConnectRemoteCtrl(desktopAddress))
if (r is CR.RemoteCtrlConnecting) return SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null return if (r is CR.RemoteCtrlConnecting) SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null
else if (r is CR.ChatCmdError) return null to r else if (r is CR.ChatCmdError) null to r
else throw Exception("connectRemoteCtrl error: ${r.responseType} ${r.details}") else {
apiErrorAlert("connectRemoteCtrl", generalGetString(MR.strings.error_alert_title), r)
null to null
}
} }
suspend fun findKnownRemoteCtrl(): Boolean = sendCommandOkResp(null, CC.FindKnownRemoteCtrl()) suspend fun findKnownRemoteCtrl(): Boolean = sendCommandOkResp(null, CC.FindKnownRemoteCtrl())
suspend fun confirmRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(null, CC.ConfirmRemoteCtrl(rcId)) suspend fun confirmRemoteCtrl(rcId: Long): Pair<SomeRemoteCtrl?, CR.ChatCmdError?> {
val r = sendCmd(null, CC.ConfirmRemoteCtrl(remoteCtrlId = rcId))
return if (r is CR.RemoteCtrlConnecting) SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null
else if (r is CR.ChatCmdError) null to r
else {
apiErrorAlert("confirmRemoteCtrl", generalGetString(MR.strings.error_alert_title), r)
null to null
}
}
suspend fun verifyRemoteCtrlSession(sessionCode: String): RemoteCtrlInfo? { suspend fun verifyRemoteCtrlSession(sessionCode: String): RemoteCtrlInfo? {
val r = sendCmd(null, CC.VerifyRemoteCtrlSession(sessionCode)) val r = sendCmd(null, CC.VerifyRemoteCtrlSession(sessionCode))
@ -1857,8 +1870,15 @@ object ChatController {
} }
} }
is CR.RemoteCtrlFound -> { is CR.RemoteCtrlFound -> {
// TODO multicast val sess = chatModel.remoteCtrlSession.value
Log.d(TAG, "RemoteCtrlFound: ${r.remoteCtrl}") if (sess != null && sess.sessionState is UIRemoteCtrlSessionState.Searching) {
val state = UIRemoteCtrlSessionState.Found(remoteCtrl = r.remoteCtrl, compatible = r.compatible)
chatModel.remoteCtrlSession.value = RemoteCtrlSession(
ctrlAppInfo = r.ctrlAppInfo_,
appVersion = r.appVersion,
sessionState = state
)
}
} }
is CR.RemoteCtrlSessionCode -> { is CR.RemoteCtrlSessionCode -> {
val state = UIRemoteCtrlSessionState.PendingConfirmation(remoteCtrl_ = r.remoteCtrl_, sessionCode = r.sessionCode) val state = UIRemoteCtrlSessionState.PendingConfirmation(remoteCtrl_ = r.remoteCtrl_, sessionCode = r.sessionCode)
@ -1870,7 +1890,13 @@ object ChatController {
chatModel.remoteCtrlSession.value = chatModel.remoteCtrlSession.value?.copy(sessionState = state) chatModel.remoteCtrlSession.value = chatModel.remoteCtrlSession.value?.copy(sessionState = state)
} }
is CR.RemoteCtrlStopped -> { is CR.RemoteCtrlStopped -> {
switchToLocalSession() val sess = chatModel.remoteCtrlSession.value
if (sess != null) {
chatModel.remoteCtrlSession.value = null
if (sess.sessionState is UIRemoteCtrlSessionState.Connected) {
switchToLocalSession()
}
}
} }
else -> else ->
Log.d(TAG , "unsupported event: ${r.responseType}") Log.d(TAG , "unsupported event: ${r.responseType}")
@ -3782,7 +3808,7 @@ sealed class CR {
@Serializable @SerialName("remoteFileStored") class RemoteFileStored(val remoteHostId: Long, val remoteFileSource: CryptoFile): CR() @Serializable @SerialName("remoteFileStored") class RemoteFileStored(val remoteHostId: Long, val remoteFileSource: CryptoFile): CR()
// remote events (mobile) // remote events (mobile)
@Serializable @SerialName("remoteCtrlList") class RemoteCtrlList(val remoteCtrls: List<RemoteCtrlInfo>): CR() @Serializable @SerialName("remoteCtrlList") class RemoteCtrlList(val remoteCtrls: List<RemoteCtrlInfo>): CR()
@Serializable @SerialName("remoteCtrlFound") class RemoteCtrlFound(val remoteCtrl: RemoteCtrlInfo): CR() @Serializable @SerialName("remoteCtrlFound") class RemoteCtrlFound(val remoteCtrl: RemoteCtrlInfo, val ctrlAppInfo_: CtrlAppInfo?, val appVersion: String, val compatible: Boolean): CR()
@Serializable @SerialName("remoteCtrlConnecting") class RemoteCtrlConnecting(val remoteCtrl_: RemoteCtrlInfo?, val ctrlAppInfo: CtrlAppInfo, val appVersion: String): CR() @Serializable @SerialName("remoteCtrlConnecting") class RemoteCtrlConnecting(val remoteCtrl_: RemoteCtrlInfo?, val ctrlAppInfo: CtrlAppInfo, val appVersion: String): CR()
@Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): CR() @Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): CR()
@Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR() @Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR()
@ -4080,7 +4106,11 @@ sealed class CR {
is RemoteHostStopped -> "remote host ID: $remoteHostId_" is RemoteHostStopped -> "remote host ID: $remoteHostId_"
is RemoteFileStored -> "remote host ID: $remoteHostId\nremoteFileSource:\n" + json.encodeToString(remoteFileSource) is RemoteFileStored -> "remote host ID: $remoteHostId\nremoteFileSource:\n" + json.encodeToString(remoteFileSource)
is RemoteCtrlList -> json.encodeToString(remoteCtrls) is RemoteCtrlList -> json.encodeToString(remoteCtrls)
is RemoteCtrlFound -> json.encodeToString(remoteCtrl) is RemoteCtrlFound -> "remote ctrl: " + json.encodeToString(remoteCtrl) +
"\nctrlAppInfo: " +
(if (ctrlAppInfo_ == null) "null" else json.encodeToString(ctrlAppInfo_)) +
"\nappVersion: $appVersion" +
"\ncompatible: $compatible"
is RemoteCtrlConnecting -> is RemoteCtrlConnecting ->
"remote ctrl: " + "remote ctrl: " +
(if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) + (if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) +

View File

@ -125,7 +125,7 @@ fun DefaultConfigurableTextField(
keyboardType: KeyboardType = KeyboardType.Text, keyboardType: KeyboardType = KeyboardType.Text,
dependsOn: State<Any?>? = null, dependsOn: State<Any?>? = null,
) { ) {
var valid by remember { mutableStateOf(validKey(state.value.text)) } var valid by remember { mutableStateOf(isValid(state.value.text)) }
var showKey by remember { mutableStateOf(false) } var showKey by remember { mutableStateOf(false) }
val icon = if (valid) { val icon = if (valid) {
if (showKey) painterResource(MR.images.ic_visibility_off_filled) else painterResource(MR.images.ic_visibility_filled) if (showKey) painterResource(MR.images.ic_visibility_off_filled) else painterResource(MR.images.ic_visibility_filled)

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@ -37,6 +38,7 @@ import chat.simplex.common.views.newchat.QRCodeScanner
import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@ -69,15 +71,21 @@ fun ConnectDesktopView(close: () -> Unit) {
@Composable @Composable
private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) { private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) {
val showConnectScreen = remember { mutableStateOf(true) }
val sessionAddress = remember { mutableStateOf("") } val sessionAddress = remember { mutableStateOf("") }
val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() } val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() }
val session = remember { chatModel.remoteCtrlSession }.value val session = remember { chatModel.remoteCtrlSession }.value
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) { ) {
if (session != null) { val discovery = if (session == null) null else session.sessionState is UIRemoteCtrlSessionState.Searching
if (discovery == true || (discovery == null && !showConnectScreen.value)) {
SearchingDesktop(deviceName, remoteCtrls)
} else if (session != null) {
when (session.sessionState) { when (session.sessionState) {
is UIRemoteCtrlSessionState.Starting -> ConnectingDesktop(session, null) is UIRemoteCtrlSessionState.Starting -> ConnectingDesktop(session, null)
is UIRemoteCtrlSessionState.Searching -> SearchingDesktop(deviceName, remoteCtrls)
is UIRemoteCtrlSessionState.Found -> FoundDesktop(session, session.sessionState.remoteCtrl, session.sessionState.compatible, remember { controller.appPrefs.connectRemoteViaMulticastAuto.state }, deviceName, remoteCtrls, sessionAddress)
is UIRemoteCtrlSessionState.Connecting -> ConnectingDesktop(session, session.sessionState.remoteCtrl_) is UIRemoteCtrlSessionState.Connecting -> ConnectingDesktop(session, session.sessionState.remoteCtrl_)
is UIRemoteCtrlSessionState.PendingConfirmation -> { is UIRemoteCtrlSessionState.PendingConfirmation -> {
if (controller.appPrefs.confirmRemoteSessions.get() || session.sessionState.remoteCtrl_ == null) { if (controller.appPrefs.confirmRemoteSessions.get() || session.sessionState.remoteCtrl_ == null) {
@ -97,11 +105,21 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) {
} }
SectionBottomSpacer() SectionBottomSpacer()
} }
DisposableEffect(Unit) { LaunchedEffect(Unit) {
setDeviceName(deviceName) setDeviceName(deviceName)
updateRemoteCtrls(remoteCtrls) updateRemoteCtrls(remoteCtrls)
val useMulticast = useMulticast(remoteCtrls)
showConnectScreen.value = !useMulticast
if (chatModel.remoteCtrlSession.value != null) {
disconnectDesktop()
} else if (useMulticast) {
findKnownDesktop(showConnectScreen)
}
}
DisposableEffect(Unit) {
onDispose { onDispose {
if (chatModel.remoteCtrlSession.value != null) { if (chatModel.remoteCtrlSession.value != null) {
showConnectScreen.value = false
disconnectDesktop() disconnectDesktop()
} }
} }
@ -146,7 +164,75 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
SectionSpacer() SectionSpacer()
SectionView { SectionView {
DisconnectButton(::disconnectDesktop) DisconnectButton(onClick = ::disconnectDesktop)
}
}
@Composable
private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
SectionView(stringResource(MR.strings.this_device_name).uppercase()) {
DevicesView(deviceName, remoteCtrls) {
if (it != "") {
setDeviceName(it)
controller.appPrefs.deviceNameForRemoteAccess.set(it)
}
}
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.waiting_for_desktop), fontStyle = FontStyle.Italic)
}
SectionSpacer()
DisconnectButton(stringResource(MR.strings.scan_QR_code).replace('\n', ' '), MR.images.ic_qr_code, ::disconnectDesktop)
}
@Composable
private fun FoundDesktop(
session: RemoteCtrlSession,
rc: RemoteCtrlInfo,
compatible: Boolean,
connectRemoteViaMulticastAuto: State<Boolean>,
deviceName: String,
remoteCtrls: SnapshotStateList<RemoteCtrlInfo>,
sessionAddress: MutableState<String>,
) {
AppBarTitle(stringResource(MR.strings.found_desktop))
SectionView(stringResource(MR.strings.this_device_name).uppercase()) {
DevicesView(deviceName, remoteCtrls) {
if (it != "") {
setDeviceName(it)
controller.appPrefs.deviceNameForRemoteAccess.set(it)
}
}
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
if (!compatible) {
Text(stringResource(MR.strings.not_compatible), color = MaterialTheme.colors.error)
}
}
SectionSpacer()
if (compatible) {
SectionItemView({ confirmKnownDesktop(sessionAddress, rc) }) {
Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.connect_button), tint = MaterialTheme.colors.secondary)
TextIconSpaced(false)
Text(generalGetString(MR.strings.connect_button))
}
}
if (!compatible || !connectRemoteViaMulticastAuto.value) {
DisconnectButton(stringResource(MR.strings.cancel_verb), onClick = ::disconnectDesktop)
}
if (compatible && connectRemoteViaMulticastAuto.value) {
LaunchedEffect(Unit) {
confirmKnownDesktop(sessionAddress, rc)
}
} }
} }
@ -174,7 +260,7 @@ private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessC
} }
SectionView { SectionView {
DisconnectButton(::disconnectDesktop) DisconnectButton(onClick = ::disconnectDesktop)
} }
} }
@ -182,7 +268,7 @@ private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessC
private fun CtrlDeviceNameText(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) { private fun CtrlDeviceNameText(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
val newDesktop = annotatedStringResource(MR.strings.new_desktop) val newDesktop = annotatedStringResource(MR.strings.new_desktop)
val text = remember(rc) { val text = remember(rc) {
var t = AnnotatedString(rc?.deviceViewName ?: session.ctrlAppInfo.deviceName) var t = AnnotatedString(rc?.deviceViewName ?: session.ctrlAppInfo?.deviceName ?: "")
if (rc == null) { if (rc == null) {
t = t + AnnotatedString(" ") + newDesktop t = t + AnnotatedString(" ") + newDesktop
} }
@ -195,7 +281,7 @@ private fun CtrlDeviceNameText(session: RemoteCtrlSession, rc: RemoteCtrlInfo?)
private fun CtrlDeviceVersionText(session: RemoteCtrlSession) { private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
val thisDeviceVersion = annotatedStringResource(MR.strings.this_device_version, session.appVersion) val thisDeviceVersion = annotatedStringResource(MR.strings.this_device_version, session.appVersion)
val text = remember(session) { val text = remember(session) {
val v = AnnotatedString(session.ctrlAppInfo.appVersionRange.maxVersion) val v = AnnotatedString(session.ctrlAppInfo?.appVersionRange?.maxVersion ?: "")
var t = AnnotatedString("v$v") var t = AnnotatedString("v$v")
if (v.text != session.appVersion) { if (v.text != session.appVersion) {
t = t + AnnotatedString(" ") + thisDeviceVersion t = t + AnnotatedString(" ") + thisDeviceVersion
@ -243,7 +329,8 @@ private fun SessionCodeText(code: String) {
private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, updateDeviceName: (String) -> Unit) { private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, updateDeviceName: (String) -> Unit) {
DeviceNameField(deviceName) { updateDeviceName(it) } DeviceNameField(deviceName) { updateDeviceName(it) }
if (remoteCtrls.isNotEmpty()) { if (remoteCtrls.isNotEmpty()) {
SectionItemView({ ModalManager.start.showModal { LinkedDesktopsView(remoteCtrls) } }) { SectionItemView({ ModalManager.start.showModal { LinkedDesktopsView(remoteCtrls) }
}) {
Text(generalGetString(MR.strings.linked_desktops)) Text(generalGetString(MR.strings.linked_desktops))
} }
} }
@ -336,8 +423,13 @@ private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
PreferenceToggle(stringResource(MR.strings.verify_connections), remember { controller.appPrefs.confirmRemoteSessions.state }.value) { PreferenceToggle(stringResource(MR.strings.verify_connections), remember { controller.appPrefs.confirmRemoteSessions.state }.value) {
controller.appPrefs.confirmRemoteSessions.set(it) controller.appPrefs.confirmRemoteSessions.set(it)
} }
PreferenceToggle(stringResource(MR.strings.discover_on_network), remember { controller.appPrefs.connectRemoteViaMulticast.state }.value && false) { PreferenceToggle(stringResource(MR.strings.discover_on_network), remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) {
controller.appPrefs.confirmRemoteSessions.set(it) controller.appPrefs.connectRemoteViaMulticast.set(it)
}
if (remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) {
PreferenceToggle(stringResource(MR.strings.multicast_connect_automatically), remember { controller.appPrefs.connectRemoteViaMulticastAuto.state }.value) {
controller.appPrefs.connectRemoteViaMulticastAuto.set(it)
}
} }
} }
SectionBottomSpacer() SectionBottomSpacer()
@ -355,13 +447,11 @@ private fun setDeviceName(name: String) {
} }
} }
private fun updateRemoteCtrls(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { private suspend fun updateRemoteCtrls(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
withBGApi { val res = controller.listRemoteCtrls()
val res = controller.listRemoteCtrls() if (res != null) {
if (res != null) { remoteCtrls.clear()
remoteCtrls.clear() remoteCtrls.addAll(res)
remoteCtrls.addAll(res)
}
} }
} }
@ -369,9 +459,34 @@ private fun processDesktopQRCode(sessionAddress: MutableState<String>, resp: Str
connectDesktopAddress(sessionAddress, resp) connectDesktopAddress(sessionAddress, resp)
} }
private fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String) { private fun findKnownDesktop(showConnectScreen: MutableState<Boolean>) {
withBGApi { withBGApi {
val res = controller.connectRemoteCtrl(desktopAddress = addr) if (controller.findKnownRemoteCtrl()) {
chatModel.remoteCtrlSession.value = RemoteCtrlSession(
ctrlAppInfo = null,
appVersion = "",
sessionState = UIRemoteCtrlSessionState.Searching
)
showConnectScreen.value = true
}
}
}
private fun confirmKnownDesktop(sessionAddress: MutableState<String>, rc: RemoteCtrlInfo) {
connectDesktop(sessionAddress) {
controller.confirmRemoteCtrl(rc.remoteCtrlId)
}
}
private fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String) {
connectDesktop(sessionAddress) {
controller.connectRemoteCtrl(addr)
}
}
private fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, CR.ChatCmdError?>) {
withBGApi {
val res = connect()
if (res.first != null) { if (res.first != null) {
val (rc_, ctrlAppInfo, v) = res.first!! val (rc_, ctrlAppInfo, v) = res.first!!
sessionAddress.value = "" sessionAddress.value = ""
@ -409,18 +524,25 @@ private fun verifyDesktopSessionCode(remoteCtrls: SnapshotStateList<RemoteCtrlIn
} }
@Composable @Composable
private fun DisconnectButton(onClick: () -> Unit) { private fun DisconnectButton(label: String = generalGetString(MR.strings.disconnect_remote_host), icon: ImageResource = MR.images.ic_close, onClick: () -> Unit) {
SectionItemView(onClick) { SectionItemView(onClick) {
Icon(painterResource(MR.images.ic_close), generalGetString(MR.strings.disconnect_remote_host), tint = MaterialTheme.colors.secondary) Icon(painterResource(icon), label, tint = MaterialTheme.colors.secondary)
TextIconSpaced(false) TextIconSpaced(false)
Text(generalGetString(MR.strings.disconnect_remote_host)) Text(label)
} }
} }
private fun useMulticast(remoteCtrls: List<RemoteCtrlInfo>): Boolean =
controller.appPrefs.connectRemoteViaMulticast.get() && remoteCtrls.isNotEmpty()
private fun disconnectDesktop(close: (() -> Unit)? = null) { private fun disconnectDesktop(close: (() -> Unit)? = null) {
withBGApi { withBGApi {
controller.stopRemoteCtrl() controller.stopRemoteCtrl()
switchToLocalSession() if (chatModel.remoteCtrlSession.value?.sessionState is UIRemoteCtrlSessionState.Connected) {
switchToLocalSession()
} else {
chatModel.remoteCtrlSession.value = null
}
close?.invoke() close?.invoke()
} }
} }

View File

@ -30,6 +30,7 @@ import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@ -90,6 +91,9 @@ fun ConnectMobileLayout(
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { controller.appPrefs.offerRemoteMulticast.state }.value) {
controller.appPrefs.offerRemoteMulticast.set(it)
}
SectionDividerSpaced(maxBottomPadding = false) SectionDividerSpaced(maxBottomPadding = false)
} }
SectionView(stringResource(MR.strings.devices).uppercase()) { SectionView(stringResource(MR.strings.devices).uppercase()) {
@ -266,7 +270,7 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
withBGApi { withBGApi {
val r = chatModel.controller.startRemoteHost(null) val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get())
if (r != null) { if (r != null) {
connecting.value = true connecting.value = true
invitation.value = r.second invitation.value = r.second
@ -309,7 +313,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
) )
var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) } var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val r = chatModel.controller.startRemoteHost(rh.remoteHostId) val r = chatModel.controller.startRemoteHost(rh.remoteHostId, controller.appPrefs.offerRemoteMulticast.get())
if (r != null) { if (r != null) {
val (rh_, inv) = r val (rh_, inv) = r
connecting.value = true connecting.value = true

View File

@ -1670,6 +1670,8 @@
<string name="desktop_connection_terminated">Connection terminated</string> <string name="desktop_connection_terminated">Connection terminated</string>
<string name="session_code">Session code</string> <string name="session_code">Session code</string>
<string name="connecting_to_desktop">Connecting to desktop</string> <string name="connecting_to_desktop">Connecting to desktop</string>
<string name="waiting_for_desktop">Waiting for desktop…</string>
<string name="found_desktop">Found desktop</string>
<string name="connect_to_desktop">Connect to desktop</string> <string name="connect_to_desktop">Connect to desktop</string>
<string name="connected_to_desktop">Connected to desktop</string> <string name="connected_to_desktop">Connected to desktop</string>
<string name="connected_desktop">Connected desktop</string> <string name="connected_desktop">Connected desktop</string>
@ -1681,9 +1683,12 @@
<string name="scan_qr_code_from_desktop">Scan QR code from desktop</string> <string name="scan_qr_code_from_desktop">Scan QR code from desktop</string>
<string name="desktop_address">Desktop address</string> <string name="desktop_address">Desktop address</string>
<string name="verify_connections">Verify connections</string> <string name="verify_connections">Verify connections</string>
<string name="discover_on_network">Discover on network</string> <string name="discover_on_network">Discover via local network</string>
<string name="multicast_discoverable_via_local_network">Discoverable via local network</string>
<string name="multicast_connect_automatically">Connect automatically</string>
<string name="paste_desktop_address">Paste desktop address</string> <string name="paste_desktop_address">Paste desktop address</string>
<string name="desktop_device">Desktop</string> <string name="desktop_device">Desktop</string>
<string name="not_compatible">Not compatible!</string>
<!-- Under development --> <!-- Under development -->
<string name="in_developing_title">Coming soon!</string> <string name="in_developing_title">Coming soon!</string>