android, desktop: calls switching from audio to video and back (#4814)

* android, desktop: calls switching from audio to video and back

* refactor

* working all 4 streams with mute handling differently

* changes

* changes

* wrong file

* changes

* padding

* android camera service type

* icons, sizes, clickable

* refactor

* Revert "android camera service type"

This reverts commit 9878ff38e9.

* late init camera permissions

* enabling camera sooner than call establishes (not fully done)

* changes

* alpha

* fixes for Safari

* enhancements

* fix Safari sound

* padding between buttons on desktop

* android default values for padding

* changes

* calls without encryption are supported and flipping camera on some devices works

* unused param

* logs

* background color

* play local video in Safari

* no line height

* removed one listener from per frame processing

* enhancements

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-09-27 02:18:05 +07:00
committed by GitHub
parent 4a39b481b1
commit 95c1d8d798
30 changed files with 2319 additions and 538 deletions
@@ -2,6 +2,7 @@ package chat.simplex.app
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@@ -83,7 +84,7 @@ class CallService: Service() {
generalGetString(MR.strings.notification_preview_somebody)
else
call?.contact?.profile?.displayName ?: ""
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
val text = generalGetString(if (call?.hasVideo == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
val image = call?.contact?.image
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
BitmapFactory.decodeResource(resources, R.drawable.icon)
@@ -105,7 +106,7 @@ class CallService: Service() {
0
}
} else if (Build.VERSION.SDK_INT >= 30) {
if (call.supportsVideo()) {
if (call.hasVideo && ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
@@ -116,7 +116,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
private fun hasGrantedPermissions(): Boolean {
val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
val grantedCamera = !callSupportsVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
val grantedCamera = !callHasVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
return grantedAudio && grantedCamera
}
@@ -124,7 +124,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
} else if (!hasGrantedPermissions() && !callSupportsVideo()) {
} else if (!hasGrantedPermissions() && !callHasVideo()) {
val call = m.activeCall.value
if (call != null) {
withBGApi { chatModel.callManager.endCall(call) }
@@ -142,7 +142,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// On Android 12+ PiP is enabled automatically when a user hides the app
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callHasVideo() && platform.androidPictureInPictureAllowed()) {
enterPictureInPictureMode()
}
}
@@ -198,7 +198,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
fun getKeyguardManager(context: Context): KeyguardManager =
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
private fun callHasVideo() = m.activeCall.value?.hasVideo == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
@Composable
fun CallActivityView() {
@@ -212,7 +212,7 @@ fun CallActivityView() {
.collect { collapsed ->
when {
collapsed -> {
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
if (!platform.androidPictureInPictureAllowed() || !callHasVideo()) {
activity.moveTaskToBack(true)
activity.startActivity(Intent(activity, MainActivity::class.java))
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
@@ -221,7 +221,7 @@ fun CallActivityView() {
activity.enterPictureInPictureMode()
}
}
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
callHasVideo() && !platform.androidPictureInPictureAllowed() -> {
// PiP disabled by user
platform.androidStartCallActivity(false)
}
@@ -242,28 +242,43 @@ fun CallActivityView() {
Box(Modifier.background(Color.Black)) {
if (call != null) {
val permissionsState = rememberMultiplePermissionsState(
permissions = if (callSupportsVideo()) {
permissions = if (callHasVideo()) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
if (permissionsState.allPermissionsGranted) {
// callState == connected is needed in a situation when a peer enabled camera in audio call while a user didn't grant camera permission yet,
// so no need to hide active call view in this case
if (permissionsState.allPermissionsGranted || call.callState == CallState.Connected) {
ActiveCallView()
LaunchedEffect(Unit) {
activity.startServiceAndBind()
}
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
}
if ((!permissionsState.allPermissionsGranted && call.callState != CallState.Connected) || call.wantsToEnableCamera) {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callHasVideo() || call.wantsToEnableCamera) {
withBGApi { chatModel.callManager.endCall(call) }
}
val cameraAndMicPermissions = rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
DisposableEffect(cameraAndMicPermissions.allPermissionsGranted) {
onDispose {
if (call.wantsToEnableCamera && cameraAndMicPermissions.allPermissionsGranted) {
val activeCall = chatModel.activeCall.value
if (activeCall != null && activeCall.contact.apiId == call.contact.apiId) {
chatModel.activeCall.value = activeCall.copy(wantsToEnableCamera = false)
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = true))
}
}
}
}
}
val view = LocalView.current
if (callSupportsVideo()) {
if (callHasVideo()) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
scope.launch {
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
activity.setPipParams(callHasVideo(), viewRatio = Rational(view.width, view.height))
activity.trackPipAnimationHintView(view)
}
}