|
|
|
@ -126,12 +126,22 @@ fun CallScreen( |
|
|
|
val video by CallController.video.collectAsState() |
|
|
|
val video by CallController.video.collectAsState() |
|
|
|
val videoActive by CallController.videoActive.collectAsState() |
|
|
|
val videoActive by CallController.videoActive.collectAsState() |
|
|
|
val remote = video?.display?.collectAsState()?.value |
|
|
|
val remote = video?.display?.collectAsState()?.value |
|
|
|
|
|
|
|
val local = video?.preview?.collectAsState()?.value |
|
|
|
val receiving = phase == CallPhase.ACTIVE && remote != null |
|
|
|
val receiving = phase == CallPhase.ACTIVE && remote != null |
|
|
|
var controlsVisible by rememberSaveable { mutableStateOf(true) } |
|
|
|
val sending = phase == CallPhase.ACTIVE && local != null |
|
|
|
var settingsVisible by rememberSaveable { mutableStateOf(false) } |
|
|
|
var selectedVideo by rememberSaveable(st?.callId) { mutableStateOf<CallVideoSource?>(CallVideoSource.REMOTE) } |
|
|
|
LaunchedEffect(receiving) { controlsVisible = !receiving } |
|
|
|
val expandedVideo = selectedVideo?.takeIf { if (it == CallVideoSource.REMOTE) receiving else sending } |
|
|
|
LaunchedEffect(receiving, controlsVisible) { |
|
|
|
val viewingVideo = expandedVideo != null |
|
|
|
if (receiving && controlsVisible) { delay(5000); controlsVisible = false } |
|
|
|
val hasVideo = receiving || sending |
|
|
|
|
|
|
|
val controlInsets = WindowInsets.systemBarsIgnoringVisibility.union(WindowInsets.displayCutout) |
|
|
|
|
|
|
|
var controlsVisible by rememberSaveable(st?.callId) { mutableStateOf(true) } |
|
|
|
|
|
|
|
var settingsVisible by rememberSaveable(st?.callId) { mutableStateOf(false) } |
|
|
|
|
|
|
|
LaunchedEffect(expandedVideo) { |
|
|
|
|
|
|
|
controlsVisible = true |
|
|
|
|
|
|
|
LogManager.addLog("INFO", "call", "video presentation=${expandedVideo ?: "controls"} remote=$receiving local=$sending") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
LaunchedEffect(viewingVideo, controlsVisible, settingsVisible) { |
|
|
|
|
|
|
|
if (viewingVideo && controlsVisible && !settingsVisible) { delay(5000); controlsVisible = false } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val locked = phase == CallPhase.ACTIVE && !videoActive && route == AudioRoute.EARPIECE && proximityNear == true |
|
|
|
val locked = phase == CallPhase.ACTIVE && !videoActive && route == AudioRoute.EARPIECE && proximityNear == true |
|
|
|
@ -149,7 +159,7 @@ fun CallScreen( |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// Ориентация принадлежит экрану звонка; краткое переподключение Surface не должно поворачивать его обратно. |
|
|
|
// Ориентация принадлежит экрану звонка; краткое переподключение Surface не должно поворачивать его обратно. |
|
|
|
DisposableEffect(video, phase == CallPhase.ACTIVE) { |
|
|
|
DisposableEffect(video, phase == CallPhase.ACTIVE, selectedVideo) { |
|
|
|
val activity = context.findActivity(); val previous = activity?.requestedOrientation |
|
|
|
val activity = context.findActivity(); val previous = activity?.requestedOrientation |
|
|
|
onDispose { |
|
|
|
onDispose { |
|
|
|
if (activity != null && previous != null && activity.requestedOrientation != previous) { |
|
|
|
if (activity != null && previous != null && activity.requestedOrientation != previous) { |
|
|
|
@ -158,18 +168,19 @@ fun CallScreen( |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
LaunchedEffect(video, phase, remote?.width, remote?.height, remote?.rotation) { |
|
|
|
val rotated = remote?.rotation?.rem(180) != 0 |
|
|
|
val format = remote |
|
|
|
val videoWidth = if (expandedVideo == CallVideoSource.LOCAL) local?.width |
|
|
|
|
|
|
|
else if (rotated) remote?.height else remote?.width |
|
|
|
|
|
|
|
val videoHeight = if (expandedVideo == CallVideoSource.LOCAL) local?.height |
|
|
|
|
|
|
|
else if (rotated) remote?.width else remote?.height |
|
|
|
|
|
|
|
LaunchedEffect(video, phase, expandedVideo, videoWidth, videoHeight) { |
|
|
|
val activity = context.findActivity() |
|
|
|
val activity = context.findActivity() |
|
|
|
if (phase == CallPhase.ACTIVE && format != null && activity != null) { |
|
|
|
if (viewingVideo && videoWidth != null && videoHeight != null && activity != null) { |
|
|
|
val rotated = format.rotation % 180 != 0 |
|
|
|
val orientation = if (videoWidth > videoHeight) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE |
|
|
|
val width = if (rotated) format.height else format.width |
|
|
|
|
|
|
|
val height = if (rotated) format.width else format.height |
|
|
|
|
|
|
|
val orientation = if (width > height) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE |
|
|
|
|
|
|
|
else ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT |
|
|
|
else ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT |
|
|
|
if (activity.requestedOrientation != orientation) { |
|
|
|
if (activity.requestedOrientation != orientation) { |
|
|
|
activity.requestedOrientation = orientation |
|
|
|
activity.requestedOrientation = orientation |
|
|
|
LogManager.addLog("INFO", "call", "video screen orientation=$orientation display=${width}x$height rotation=${format.rotation}") |
|
|
|
LogManager.addLog("INFO", "call", "video screen orientation=$orientation source=$expandedVideo display=${videoWidth}x$videoHeight") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -184,9 +195,10 @@ fun CallScreen( |
|
|
|
|
|
|
|
|
|
|
|
/* Пока телефон у уха (proximity) — глотаем Back, чтобы не свернуть звонок. */ |
|
|
|
/* Пока телефон у уха (proximity) — глотаем Back, чтобы не свернуть звонок. */ |
|
|
|
BackHandler(enabled = locked) {} |
|
|
|
BackHandler(enabled = locked) {} |
|
|
|
|
|
|
|
BackHandler(enabled = viewingVideo && !locked && !settingsVisible) { selectedVideo = null } |
|
|
|
|
|
|
|
|
|
|
|
/* При блокировке скрываем системные панели (иначе статус-бар светится поверх оверлея). */ |
|
|
|
/* При блокировке скрываем системные панели (иначе статус-бар светится поверх оверлея). */ |
|
|
|
val hideBars = locked || (receiving && !controlsVisible) |
|
|
|
val hideBars = locked || (viewingVideo && !controlsVisible) |
|
|
|
DisposableEffect(hideBars) { |
|
|
|
DisposableEffect(hideBars) { |
|
|
|
val act = context.findActivity() |
|
|
|
val act = context.findActivity() |
|
|
|
val controller = act?.let { WindowCompat.getInsetsController(it.window, it.window.decorView) } |
|
|
|
val controller = act?.let { WindowCompat.getInsetsController(it.window, it.window.decorView) } |
|
|
|
@ -208,16 +220,19 @@ fun CallScreen( |
|
|
|
contentAlignment = Alignment.Center |
|
|
|
contentAlignment = Alignment.Center |
|
|
|
) { |
|
|
|
) { |
|
|
|
if (phase == CallPhase.ACTIVE) video?.let { |
|
|
|
if (phase == CallPhase.ACTIVE) video?.let { |
|
|
|
CallVideoPlayback(it, onTap = { controlsVisible = !controlsVisible }, modifier = Modifier.matchParentSize()) |
|
|
|
CallVideoStage(it, expandedVideo, onSelect = { source -> selectedVideo = source }, |
|
|
|
|
|
|
|
onTap = { controlsVisible = !controlsVisible }, modifier = Modifier.matchParentSize()) |
|
|
|
} |
|
|
|
} |
|
|
|
if (!receiving || controlsVisible) { |
|
|
|
if (!viewingVideo || controlsVisible) { |
|
|
|
Column( |
|
|
|
if (!viewingVideo) Column( |
|
|
|
modifier = Modifier.fillMaxWidth().align(if (receiving) Alignment.BottomCenter else Alignment.Center) |
|
|
|
modifier = Modifier.fillMaxWidth().align(Alignment.Center) |
|
|
|
.background(Color(0xFF0F1B22)).statusBarsPadding().navigationBarsPadding() |
|
|
|
.windowInsetsPadding(controlInsets) |
|
|
|
.verticalScroll(rememberScrollState()).padding(top = if (receiving) 12.dp else 56.dp, bottom = 24.dp), |
|
|
|
.padding(top = if (hasVideo) 224.dp else 56.dp) |
|
|
|
|
|
|
|
.background(Color(0xFF0F1B22)).verticalScroll(rememberScrollState()) |
|
|
|
|
|
|
|
.padding(bottom = 24.dp), |
|
|
|
horizontalAlignment = Alignment.CenterHorizontally |
|
|
|
horizontalAlignment = Alignment.CenterHorizontally |
|
|
|
) { |
|
|
|
) { |
|
|
|
if (!receiving) { |
|
|
|
if (!viewingVideo) { |
|
|
|
Box( |
|
|
|
Box( |
|
|
|
modifier = Modifier.size(96.dp).background(Color(0xFF1F6FEB), CircleShape), |
|
|
|
modifier = Modifier.size(96.dp).background(Color(0xFF1F6FEB), CircleShape), |
|
|
|
contentAlignment = Alignment.Center |
|
|
|
contentAlignment = Alignment.Center |
|
|
|
@ -250,7 +265,6 @@ fun CallScreen( |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
Spacer(Modifier.height(40.dp)) |
|
|
|
Spacer(Modifier.height(40.dp)) |
|
|
|
if (phase == CallPhase.ACTIVE) video?.let { CallVideoPreview(it) } |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (phase == CallPhase.ENDED) { |
|
|
|
if (phase == CallPhase.ENDED) { |
|
|
|
@ -266,7 +280,7 @@ fun CallScreen( |
|
|
|
contentDescription = stringResource(R.string.earpiece), |
|
|
|
contentDescription = stringResource(R.string.earpiece), |
|
|
|
label = stringResource(R.string.earpiece), |
|
|
|
label = stringResource(R.string.earpiece), |
|
|
|
active = route == AudioRoute.EARPIECE, |
|
|
|
active = route == AudioRoute.EARPIECE, |
|
|
|
showLabel = !receiving, |
|
|
|
showLabel = !viewingVideo, |
|
|
|
onClick = { viewModel.setAudioRoute(AudioRoute.EARPIECE) } |
|
|
|
onClick = { viewModel.setAudioRoute(AudioRoute.EARPIECE) } |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -275,7 +289,7 @@ fun CallScreen( |
|
|
|
contentDescription = stringResource(R.string.speakerphone), |
|
|
|
contentDescription = stringResource(R.string.speakerphone), |
|
|
|
label = stringResource(R.string.speaker_short), |
|
|
|
label = stringResource(R.string.speaker_short), |
|
|
|
active = route == AudioRoute.SPEAKER, |
|
|
|
active = route == AudioRoute.SPEAKER, |
|
|
|
showLabel = !receiving, |
|
|
|
showLabel = !viewingVideo, |
|
|
|
onClick = { viewModel.setAudioRoute(AudioRoute.SPEAKER) } |
|
|
|
onClick = { viewModel.setAudioRoute(AudioRoute.SPEAKER) } |
|
|
|
) |
|
|
|
) |
|
|
|
if (headsetAvailable) { |
|
|
|
if (headsetAvailable) { |
|
|
|
@ -284,7 +298,7 @@ fun CallScreen( |
|
|
|
contentDescription = stringResource(R.string.headset), |
|
|
|
contentDescription = stringResource(R.string.headset), |
|
|
|
label = stringResource(R.string.headset), |
|
|
|
label = stringResource(R.string.headset), |
|
|
|
active = route == AudioRoute.HEADSET, |
|
|
|
active = route == AudioRoute.HEADSET, |
|
|
|
showLabel = !receiving, |
|
|
|
showLabel = !viewingVideo, |
|
|
|
onClick = { |
|
|
|
onClick = { |
|
|
|
if (hasBtPermission) { |
|
|
|
if (hasBtPermission) { |
|
|
|
viewModel.setAudioRoute(AudioRoute.HEADSET) |
|
|
|
viewModel.setAudioRoute(AudioRoute.HEADSET) |
|
|
|
@ -305,7 +319,7 @@ fun CallScreen( |
|
|
|
contentDescription = stringResource(if (muted) R.string.microphone_enable else R.string.microphone_disable), |
|
|
|
contentDescription = stringResource(if (muted) R.string.microphone_enable else R.string.microphone_disable), |
|
|
|
label = stringResource(if (muted) R.string.microphone_on_short else R.string.microphone_mute_short), |
|
|
|
label = stringResource(if (muted) R.string.microphone_on_short else R.string.microphone_mute_short), |
|
|
|
active = muted, |
|
|
|
active = muted, |
|
|
|
showLabel = !receiving, |
|
|
|
showLabel = !viewingVideo, |
|
|
|
activeColor = Color(0xFFB32632), |
|
|
|
activeColor = Color(0xFFB32632), |
|
|
|
onClick = { viewModel.toggleMute() } |
|
|
|
onClick = { viewModel.toggleMute() } |
|
|
|
) |
|
|
|
) |
|
|
|
@ -319,7 +333,7 @@ fun CallScreen( |
|
|
|
Icon(Icons.Filled.CallEnd, stringResource(R.string.hang_up), tint = Color.White, modifier = Modifier.size(36.dp)) |
|
|
|
Icon(Icons.Filled.CallEnd, stringResource(R.string.hang_up), tint = Color.White, modifier = Modifier.size(36.dp)) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (!receiving) { |
|
|
|
if (!viewingVideo) { |
|
|
|
Spacer(Modifier.height(6.dp)) |
|
|
|
Spacer(Modifier.height(6.dp)) |
|
|
|
Text(stringResource(R.string.hang_up), color = Color(0xFFAEBCC8), fontSize = 12.sp) |
|
|
|
Text(stringResource(R.string.hang_up), color = Color(0xFFAEBCC8), fontSize = 12.sp) |
|
|
|
} |
|
|
|
} |
|
|
|
@ -329,22 +343,20 @@ fun CallScreen( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
IconButton( |
|
|
|
IconButton( |
|
|
|
onClick = onMinimize, |
|
|
|
onClick = { if (viewingVideo) selectedVideo = null else onMinimize() }, |
|
|
|
modifier = Modifier |
|
|
|
modifier = Modifier |
|
|
|
.align(Alignment.TopStart) |
|
|
|
.align(Alignment.TopStart) |
|
|
|
.statusBarsPadding() |
|
|
|
.windowInsetsPadding(controlInsets) |
|
|
|
.padding(8.dp) |
|
|
|
.padding(8.dp) |
|
|
|
) { |
|
|
|
) { |
|
|
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.minimize_call), tint = Color.White, modifier = Modifier.size(24.dp)) |
|
|
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(if (viewingVideo) R.string.call_controls else R.string.minimize_call), |
|
|
|
|
|
|
|
tint = Color.White, modifier = Modifier.size(24.dp)) |
|
|
|
} |
|
|
|
} |
|
|
|
if (phase == CallPhase.ACTIVE) IconButton(onClick = { settingsVisible = true }, |
|
|
|
if (phase == CallPhase.ACTIVE) IconButton(onClick = { selectedVideo = null; settingsVisible = true }, |
|
|
|
modifier = Modifier.align(Alignment.TopEnd).statusBarsPadding().padding(8.dp)) { |
|
|
|
modifier = Modifier.align(Alignment.TopEnd).windowInsetsPadding(controlInsets).padding(8.dp)) { |
|
|
|
Icon(Icons.Filled.Settings, stringResource(R.string.settings), tint = Color.White) |
|
|
|
Icon(Icons.Filled.Settings, stringResource(R.string.settings), tint = Color.White) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (receiving) video?.let { |
|
|
|
|
|
|
|
CallVideoPreview(it, Modifier.align(Alignment.TopEnd).statusBarsPadding().padding(top = 64.dp, end = 12.dp)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (settingsVisible && phase == CallPhase.ACTIVE && !locked) AlertDialog( |
|
|
|
if (settingsVisible && phase == CallPhase.ACTIVE && !locked) AlertDialog( |
|
|
|
onDismissRequest = { settingsVisible = false }, title = { Text(stringResource(R.string.call_settings)) }, |
|
|
|
onDismissRequest = { settingsVisible = false }, title = { Text(stringResource(R.string.call_settings)) }, |
|
|
|
confirmButton = { TextButton(onClick = { settingsVisible = false }) { Text(stringResource(R.string.close)) } }, |
|
|
|
confirmButton = { TextButton(onClick = { settingsVisible = false }) { Text(stringResource(R.string.close)) } }, |
|
|
|
|