android, desktop: landscape calls on Android and better local camera ratio management (#5124)

* android, desktop: landscape calls on Android and better local camera ratio management

The main thing is that now when exiting from CallActivity while in call
audio devices are not reset to default. It allows to have landscape mode
enabled

* styles

* fix changing calls
This commit is contained in:
Stanislav Dmitrenko
2024-12-08 00:09:00 +07:00
committed by GitHub
parent 7d6c7c58d7
commit 307211a47f
14 changed files with 283 additions and 137 deletions
@@ -71,8 +71,12 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
}
override fun stop() {
am.unregisterAudioDeviceCallback(audioCallback)
am.removeOnCommunicationDeviceChangedListener(listener)
try {
am.unregisterAudioDeviceCallback(audioCallback)
am.removeOnCommunicationDeviceChangedListener(listener)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
}
}
override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) {
@@ -6,12 +6,12 @@ import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.media.*
import android.os.Build
import android.os.PowerManager
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
import android.os.PowerManager.WakeLock
import android.view.View
import android.view.ViewGroup
import android.webkit.*
@@ -23,7 +23,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -47,7 +46,6 @@ import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource
@@ -58,6 +56,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.datetime.Clock
import kotlinx.serialization.encodeToString
import java.io.Closeable
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
@SuppressLint("StaticFieldLeak")
@@ -72,49 +71,62 @@ fun activeCallDestroyWebView() = withApi {
Log.d(TAG, "CallView: webview was destroyed")
}
@SuppressLint("SourceLockedOrientationActivity")
@Composable
actual fun ActiveCallView() {
val call = remember { chatModel.activeCall }.value
val scope = rememberCoroutineScope()
val proximityLock = remember {
class ActiveCallState: Closeable {
val proximityLock: WakeLock? = screenOffWakeLock()
var wasConnected = false
val callAudioDeviceManager = CallAudioDeviceManagerInterface.new()
private var closed = false
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
callAudioDeviceManager.start()
}
}
override fun close() {
if (closed) return
closed = true
CallSoundsPlayer.stop()
if (wasConnected) {
CallSoundsPlayer.vibrate()
}
callAudioDeviceManager.stop()
dropAudioManagerOverrides()
if (proximityLock?.isHeld == true) {
proximityLock.release()
}
}
private fun screenOffWakeLock(): WakeLock? {
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
return if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
} else {
null
}
}
val wasConnected = rememberSaveable { mutableStateOf(false) }
}
@SuppressLint("SourceLockedOrientationActivity")
@Composable
actual fun ActiveCallView() {
val call = remember { chatModel.activeCall }.value
val callState = call?.androidCallState as ActiveCallState?
val scope = rememberCoroutineScope()
LaunchedEffect(call) {
if (call?.callState == CallState.Connected && !wasConnected.value) {
if (call?.callState == CallState.Connected && callState != null && !callState.wasConnected) {
CallSoundsPlayer.vibrate(2)
wasConnected.value = true
callState.wasConnected = true
}
}
val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() }
DisposableEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
callAudioDeviceManager.start()
}
onDispose {
CallSoundsPlayer.stop()
if (wasConnected.value) {
CallSoundsPlayer.vibrate()
}
callAudioDeviceManager.stop()
dropAudioManagerOverrides()
if (proximityLock?.isHeld == true) {
proximityLock.release()
}
}
}
LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) {
LaunchedEffect(callState, chatModel.activeCallViewIsCollapsed.value) {
callState ?: return@LaunchedEffect
if (chatModel.activeCallViewIsCollapsed.value) {
if (proximityLock?.isHeld == true) proximityLock.release()
if (callState.proximityLock?.isHeld == true) callState.proximityLock.release()
} else {
delay(1000)
if (proximityLock?.isHeld == false) proximityLock.acquire()
if (callState.proximityLock?.isHeld == false) callState.proximityLock.acquire()
}
}
Box(Modifier.fillMaxSize()) {
@@ -122,6 +134,7 @@ actual fun ActiveCallView() {
Log.d(TAG, "received from WebRTCView: $apiMsg")
val call = chatModel.activeCall.value
if (call != null) {
val callState = call.androidCallState as ActiveCallState
Log.d(TAG, "has active call $call")
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
@@ -131,9 +144,9 @@ actual fun ActiveCallView() {
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, 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()
callState.callAudioDeviceManager.start()
} else {
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
CallSoundsPlayer.startConnectingCallSound(scope)
activeCallWaitDeliveryReceipt(scope)
@@ -143,9 +156,9 @@ actual fun ActiveCallView() {
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()
callState.callAudioDeviceManager.start()
} else {
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
}
is WCallResponse.Answer -> withBGApi {
@@ -228,14 +241,14 @@ actual fun ActiveCallView() {
!chatModel.activeCallViewIsCollapsed.value -> true
else -> false
}
if (call != null && showOverlay) {
ActiveCallOverlay(call, chatModel, callAudioDeviceManager)
if (call != null && showOverlay && callState != null) {
ActiveCallOverlay(call, chatModel, callState.callAudioDeviceManager)
}
}
KeyChangeEffect(call?.localMediaSources?.hasVideo) {
if (call != null && call.hasVideo && callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
KeyChangeEffect(callState, call?.localMediaSources?.hasVideo) {
if (call != null && call.hasVideo && callState != null && callState.callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
// enabling speaker on user action (peer action ignored) and not disabling it again
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
}
val context = LocalContext.current
@@ -243,16 +256,12 @@ actual fun ActiveCallView() {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
val prevVolumeControlStream = activity.volumeControlStream
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
// Lock orientation to portrait in order to have good experience with calls
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
chatModel.activeCallViewIsVisible.value = true
// After the first call, End command gets added to the list which prevents making another calls
chatModel.callCommand.removeAll { it is WCallCommand.End }
keepScreenOn(true)
onDispose {
activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
chatModel.activeCallViewIsVisible.value = false
chatModel.callCommand.clear()
keepScreenOn(false)
@@ -264,8 +273,8 @@ actual fun ActiveCallView() {
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) {
ActiveCallOverlayLayout(
call = call,
devices = remember { callAudioDeviceManager.devices }.value,
currentDevice = remember { callAudioDeviceManager.currentDevice },
devices = remember(callAudioDeviceManager) { callAudioDeviceManager.devices }.value,
currentDevice = remember(callAudioDeviceManager) { callAudioDeviceManager.currentDevice },
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
@@ -832,7 +841,8 @@ fun PreviewActiveCallOverlayVideo() {
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
)
),
androidCallState = {}
),
devices = emptyList(),
currentDevice = remember { mutableStateOf(null) },
@@ -841,7 +851,7 @@ fun PreviewActiveCallOverlayVideo() {
selectDevice = {},
toggleVideo = {},
toggleSound = {},
flipCamera = {}
flipCamera = {},
)
}
}
@@ -862,7 +872,8 @@ fun PreviewActiveCallOverlayAudio() {
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
)
),
androidCallState = {}
),
devices = emptyList(),
currentDevice = remember { mutableStateOf(null) },