diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index 5a4ae01098..c90946c95b 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -67,10 +67,13 @@ actual fun getKeyboardState(): State { return keyboardState } -actual fun hideKeyboard(view: Any?) { +actual fun hideKeyboard(view: Any?, clearFocus: Boolean) { // LALAL // LocalSoftwareKeyboardController.current?.hide() if (view is View) { + if (clearFocus) { + view.clearFocus() + } (androidAppContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt index 9c6f76461d..d1376196a9 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt @@ -64,6 +64,7 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { } override fun start() { + am.mode = AudioManager.MODE_IN_COMMUNICATION am.registerAudioDeviceCallback(audioCallback, null) am.addOnCommunicationDeviceChangedListener(Executors.newSingleThreadExecutor(), listener) } @@ -75,7 +76,6 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean) { Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker") - am.mode = AudioManager.MODE_IN_COMMUNICATION val commDevice = am.communicationDevice if (keepAnyNonEarpiece && commDevice != null && commDevice.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { // some external device or speaker selected already, no need to change it @@ -98,7 +98,6 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { } override fun selectDevice(id: Int) { - am.mode = AudioManager.MODE_IN_COMMUNICATION val device = devices.value.lastOrNull { it.id == id } if (device != null && am.communicationDevice?.id != id ) { am.setCommunicationDevice(device) @@ -115,40 +114,32 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface { override fun onAudioDevicesAdded(addedDevices: Array) { Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") super.onAudioDevicesAdded(addedDevices) - val wasSize = devices.value.size - devices.value += addedDevices.filter { it.hasSupportedType() } - val addedCount = devices.value.size - wasSize - //if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) { - // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 - selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false) - //} + devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired() + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false) } override fun onAudioDevicesRemoved(removedDevices: Array) { Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") super.onAudioDevicesRemoved(removedDevices) - val wasSize = devices.value.size - devices.value = devices.value.filterNot { removedDevices.any { rm -> rm.id == it.id } } - //val removedCount = wasSize - devices.value.size - //if (devices.value.count { it.hasSupportedType() } == 2 && chatModel.activeCall.value?.callState == CallState.Connected) { - // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 - selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, true) - //} + devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired() + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, true) } } override fun start() { + am.mode = AudioManager.MODE_IN_COMMUNICATION am.registerAudioDeviceCallback(audioCallback, null) } override fun stop() { am.unregisterAudioDeviceCallback(audioCallback) + am.stopBluetoothSco() } override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyNonEarpiece: Boolean) { Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker") val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE - val externalDevice = devices.value.lastOrNull { it.hasSupportedType() && it.isSource && it.isSink && it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE } + val externalDevice = devices.value.lastOrNull { it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE } if (externalDevice != null) { selectDevice(externalDevice.id) } else { @@ -166,7 +157,7 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface { val isExternalDevice = device != null && device.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER && device.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE if (isExternalDevice) { am.isSpeakerphoneOn = false - if (device?.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) { + if (device?.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || device?.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) { am.isWiredHeadsetOn = true am.stopBluetoothSco() am.isBluetoothScoOn = false @@ -194,11 +185,18 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface { AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> true AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> true - AudioDeviceInfo.TYPE_BLE_HEADSET -> true - AudioDeviceInfo.TYPE_BLE_SPEAKER -> true AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> true else -> false } + + private fun List.excludeSameType(): List = + groupBy { it.type }.flatMap { devices -> listOf(devices.value.minByOrNull { it.id }!!) } + + // Earpiece will not work if there is a wired connection + private fun List.excludeEarpieceIfWired(): List = + if (any { it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES }) + filter { it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE } + else this } val AudioDeviceInfo.icon: ImageResource @@ -225,11 +223,15 @@ val AudioDeviceInfo.name: StringResource? AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLE_HEADSET, - AudioDeviceInfo.TYPE_BLE_SPEAKER -> null // Use product name instead + AudioDeviceInfo.TYPE_BLE_SPEAKER -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Use product name instead + null + } else { + MR.strings.audio_device_bluetooth + } AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> MR.strings.audio_device_wired_headphones AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_USB_DEVICE -> null // Use product name instead else -> null // Use product name instead } - diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index a93f79f7bc..382d87dccb 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -87,7 +87,9 @@ actual fun ActiveCallView() { } val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() } DisposableEffect(Unit) { - callAudioDeviceManager.start() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + callAudioDeviceManager.start() + } onDispose { CallSoundsPlayer.stop() if (wasConnected.value) { @@ -120,13 +122,24 @@ actual fun ActiveCallView() { val callType = CallType(call.localMedia, r.capabilities) chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType) updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) } - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // Starting is delayed to make Android <= 11 working good with Bluetooth + callAudioDeviceManager.start() + } else { + callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) + } CallSoundsPlayer.startConnectingCallSound(scope) activeCallWaitDeliveryReceipt(scope) } is WCallResponse.Offer -> withBGApi { chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities) updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // Starting is delayed to make Android <= 11 working good with Bluetooth + callAudioDeviceManager.start() + } else { + callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) + } } is WCallResponse.Answer -> withBGApi { chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates) @@ -141,7 +154,6 @@ actual fun ActiveCallView() { val callStatus = json.decodeFromString("\"${r.state.connectionState}\"") if (callStatus == WebRTCCallStatus.Connected) { updateActiveCall(call) { it.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) } - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) } withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) } } catch (e: Throwable) { @@ -149,9 +161,6 @@ actual fun ActiveCallView() { } is WCallResponse.Connected -> { updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) } - scope.launch { - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.soundSpeaker, true) - } } is WCallResponse.End -> { withBGApi { chatModel.callManager.endCall(call) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt index 28ac357c2d..af202fcabd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt @@ -13,7 +13,7 @@ expect fun LocalMultiplatformView(): Any? @Composable expect fun getKeyboardState(): State -expect fun hideKeyboard(view: Any?) +expect fun hideKeyboard(view: Any?, clearFocus: Boolean = false) expect fun androidIsFinishingMainActivity(): Boolean diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt index 2b2f006d25..9b25e9b5e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt @@ -135,7 +135,9 @@ fun PasscodeView( if (!buttonsEnabled.value) { ProgressIndicator() } + val view = LocalMultiplatformView() LaunchedEffect(Unit) { + hideKeyboard(view, true) focusRequester.requestFocus() // Disallow to steal a focus by clicking on buttons or using Tab focusRequester.captureFocus() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index 3f63e72a69..0ea2d25c4f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -521,6 +521,37 @@ private val versionDescriptions: List = listOf( ) ) ), + VersionDescription( + version = "v5.7", + features = listOf( + FeatureDescription( + icon = MR.images.ic_vpn_key_filled, + titleId = MR.strings.v5_6_quantum_resistant_encryption, + descrId = MR.strings.v5_7_quantum_resistant_encryption_descr + ), + FeatureDescription( + icon = MR.images.ic_forward, + titleId = MR.strings.v5_7_forward, + descrId = MR.strings.v5_7_forward_descr + ), + FeatureDescription( + icon = MR.images.ic_music_note, + titleId = MR.strings.v5_7_call_sounds, + descrId = MR.strings.v5_7_call_sounds_descr + ), + FeatureDescription( + icon = MR.images.ic_wifi_tethering, + titleId = MR.strings.v5_7_network, + descrId = MR.strings.v5_7_network_descr + ), + FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v5_7_new_interface_languages, + descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, + link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" + ) + ) + ), ) private val lastVersion = versionDescriptions.last().version diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index c93e7e282a..c55888d8a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -212,7 +212,7 @@ object AppearanceScope { "it" to "Italiano", "iw" to "עִברִית", "ja" to "日本語", - "lt" to "Lietuvių kalba", + "lt" to "Lietuvių", "nl" to "Nederlands", "pl" to "Polski", "pt-BR" to "Português, Brasil", diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d30db06f9f..f3a5c138d5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -839,6 +839,7 @@ Earpiece Speaker Headphones + Bluetooth The next generation of private messaging @@ -1764,6 +1765,14 @@ Use the app while in the call. Safer groups Admins can block a member for all. + Will be enabled in direct chats! + Forward and save messages + Message source remains private. + In-call sounds + When connecting audio and video calls. + Network management + More reliable network connection. + Lithuanian UI seconds diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg new file mode 100644 index 0000000000..766dc76be8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt index b5a0e2e008..e49dc6f2b1 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt @@ -16,7 +16,7 @@ actual fun LocalMultiplatformView(): Any? = null @Composable actual fun getKeyboardState(): State = remember { mutableStateOf(KeyboardState.Opened) } -actual fun hideKeyboard(view: Any?) {} +actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {} actual fun androidIsFinishingMainActivity(): Boolean = false diff --git a/cabal.project b/cabal.project index 572c22c5fd..6e8ae3302e 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: fe28e02be779fb0357a7f650bd327a75f4aaab95 + tag: daa866f3331cf882f1030ae636f9e27a2498ca8b source-repository-package type: git diff --git a/package.yaml b/package.yaml index 84a0eff967..c899587e16 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.7.0.2 +version: 5.7.0.3 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index c4e857d97e..4d29184ac0 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."fe28e02be779fb0357a7f650bd327a75f4aaab95" = "1hwh0kx6ljskjx7svxpwmga79dfa2vz9qq0mwbqr6kkb9kf6qdd3"; + "https://github.com/simplex-chat/simplexmq.git"."daa866f3331cf882f1030ae636f9e27a2498ca8b" = "1bfzw8q8jgblpqh0vjvhs3ddyvgxrdpv18gy84yr6kmvssmdgs76"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 67dfb8a046..5af23471a8 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.7.0.2 +version: 5.7.0.3 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat