diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 1a7120c8b1..c9520f5c69 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -70,6 +70,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState import io.getstream.video.android.BuildConfig import io.getstream.video.android.R +import io.getstream.video.android.compose.pip.rememberIsInPipMode import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.base.StreamBadgeBox import io.getstream.video.android.compose.ui.components.base.StreamDialogPositiveNegative @@ -411,27 +412,32 @@ fun CallScreen( }, ) - if (participantsSize.size == 1 && !chatState.isVisible && orientation == Configuration.ORIENTATION_PORTRAIT) { - val context = LocalContext.current - val clipboardManager = remember(context) { - context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - } - val env = AppConfig.currentEnvironment.collectAsStateWithLifecycle() - Popup( - alignment = Alignment.BottomCenter, - offset = IntOffset( - 0, - -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx() - .toInt(), - ), + val isPictureInPictureMode = rememberIsInPipMode() + if (!isPictureInPictureMode) { + if (participantsSize.size == 1 && + !chatState.isVisible && + orientation == Configuration.ORIENTATION_PORTRAIT ) { - ShareCallWithOthers( - modifier = Modifier.fillMaxWidth(), - call = call, - clipboardManager = clipboardManager, - env = env, - context = context, - ) + val clipboardManager = remember(context) { + context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + } + val env = AppConfig.currentEnvironment.collectAsStateWithLifecycle() + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset( + 0, + -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx() + .toInt(), + ), + ) { + ShareCallWithOthers( + modifier = Modifier.fillMaxWidth(), + call = call, + clipboardManager = clipboardManager, + env = env, + context = context, + ) + } } } @@ -440,7 +446,13 @@ fun CallScreen( } if (showingLandscapeControls && orientation == Configuration.ORIENTATION_LANDSCAPE) { - LandscapeControls(call) { + LandscapeControls(call, onChat = { + showingLandscapeControls = false + scope.launch { chatState.show() } + }, onSettings = { + showingLandscapeControls = false + isShowingSettingMenu = true + }) { showingLandscapeControls = !showingLandscapeControls } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt index 2fdf65f63f..72d5b5b7ed 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt @@ -21,11 +21,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CallEnd +import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -38,6 +41,7 @@ import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.call.controls.actions.ChatDialogAction import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction @@ -48,7 +52,12 @@ import io.getstream.video.android.mock.previewCall import io.getstream.video.android.tooling.extensions.toPx @Composable -fun LandscapeControls(call: Call, onDismiss: () -> Unit) { +fun LandscapeControls( + call: Call, + onChat: () -> Unit, + onSettings: () -> Unit, + onDismiss: () -> Unit, +) { val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle() val isMicrophoneEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle() val toggleCamera = { @@ -76,6 +85,8 @@ fun LandscapeControls(call: Call, onDismiss: () -> Unit) { camera = toggleCamera, mic = toggleMicrophone, onClick = onClick, + onChat = onChat, + onSettings = onSettings, ) { onDismiss() } @@ -87,6 +98,8 @@ fun LandscapeControlsContent( isCameraEnabled: Boolean, isMicEnabled: Boolean, call: Call, + onChat: () -> Unit, + onSettings: () -> Unit, camera: () -> Unit, mic: () -> Unit, onClick: () -> Unit, @@ -107,21 +120,36 @@ fun LandscapeControlsContent( ReactionsMenu(call = call, reactionMapper = ReactionMapper.defaultReactionMapper()) { onDismiss() } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - ToggleCameraAction(isCameraEnabled = isCameraEnabled) { - camera() - } - ToggleMicrophoneAction(isMicrophoneEnabled = isMicEnabled) { - mic() - } - FlipCameraAction { - call.camera.flip() - } + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ToggleCameraAction(isCameraEnabled = isCameraEnabled) { + camera() + } + ToggleMicrophoneAction(isMicrophoneEnabled = isMicEnabled) { + mic() + } + FlipCameraAction { + call.camera.flip() + } + ChatDialogAction( + messageCount = 0, + onCallAction = { onChat() }, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + StreamButton( + modifier = Modifier.fillMaxWidth(), + style = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(), + icon = Icons.Default.Settings, + text = "Settings", + onClick = onSettings, + ) StreamButton( + modifier = Modifier.fillMaxWidth(), style = VideoTheme.styles.buttonStyles.alertButtonStyle(), icon = Icons.Default.CallEnd, text = "Leave call", @@ -144,6 +172,8 @@ fun LandscapeControlsPreview() { {}, {}, {}, + {}, + {}, ) { } } diff --git a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api index cf9a49199b..5370b2c7b7 100644 --- a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api +++ b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api @@ -42,6 +42,11 @@ public abstract interface class io/getstream/video/android/compose/permission/Vi public abstract fun launchPermissionRequest ()V } +public final class io/getstream/video/android/compose/pip/PictureInPictureKt { + public static final fun isInPictureInPictureMode (Landroid/content/Context;)Z + public static final fun rememberIsInPipMode (Landroidx/compose/runtime/Composer;I)Z +} + public abstract interface class io/getstream/video/android/compose/state/ui/internal/CallParticipantInfoMode { } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt index 1aca609532..c6146dfa51 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt @@ -24,6 +24,16 @@ import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.os.Build import android.util.Rational +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.util.Consumer import io.getstream.video.android.core.Call @Suppress("DEPRECATION") @@ -58,12 +68,48 @@ internal fun enterPictureInPicture(context: Context, call: Call) { } } -internal val Context.isInPictureInPictureMode: Boolean +/** + * Remember if the current activity is in Picture-in-Picture mode. + * To be used in compose to decide weather the current mode is PiP or not. + */ +@Composable +public fun rememberIsInPipMode(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val activity = LocalContext.current.findComponentActivity() + var pipMode by remember { mutableStateOf(activity?.isInPictureInPictureMode) } + DisposableEffect(activity) { + val observer = Consumer { info -> + pipMode = info.isInPictureInPictureMode + } + activity?.addOnPictureInPictureModeChangedListener( + observer, + ) + onDispose { activity?.removeOnPictureInPictureModeChangedListener(observer) } + } + return pipMode ?: false + } else { + return false + } +} + +/** + * Used in other parts of the app to check if the current context is in Picture-in-Picture mode. + */ +public val Context.isInPictureInPictureMode: Boolean get() { val currentActivity = findActivity() return currentActivity?.isInPictureInPictureMode == true } +internal fun Context.findComponentActivity(): ComponentActivity? { + var context = this + while (context is ContextWrapper) { + if (context is ComponentActivity) return context + context = context.baseContext + } + return null +} + internal fun Context.findActivity(): Activity? { var context = this while (context is ContextWrapper) { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt index 486765419f..9ce3728fbe 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt @@ -44,7 +44,7 @@ import io.getstream.video.android.compose.lifecycle.MediaPiPLifecycle import io.getstream.video.android.compose.permission.VideoPermissionsState import io.getstream.video.android.compose.permission.rememberMicrophonePermissionState import io.getstream.video.android.compose.pip.enterPictureInPicture -import io.getstream.video.android.compose.pip.isInPictureInPictureMode +import io.getstream.video.android.compose.pip.rememberIsInPipMode import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState @@ -122,7 +122,7 @@ public fun AudioRoomContent( ) { val context = LocalContext.current val orientation = LocalConfiguration.current.orientation - val isInPictureInPicture = context.isInPictureInPictureMode + val isInPictureInPicture = rememberIsInPipMode() DefaultPermissionHandler(videoPermission = permissions) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt index 8a2f8e8983..b54f34d2a7 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt @@ -55,7 +55,7 @@ import io.getstream.video.android.compose.lifecycle.MediaPiPLifecycle import io.getstream.video.android.compose.permission.VideoPermissionsState import io.getstream.video.android.compose.permission.rememberCallPermissionsState import io.getstream.video.android.compose.pip.enterPictureInPicture -import io.getstream.video.android.compose.pip.isInPictureInPictureMode +import io.getstream.video.android.compose.pip.rememberIsInPipMode import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.call.CallAppBar import io.getstream.video.android.compose.ui.components.call.activecall.internal.DefaultPermissionHandler @@ -151,7 +151,7 @@ public fun CallContent( ) { val context = LocalContext.current val orientation = LocalConfiguration.current.orientation - val isInPictureInPicture = context.isInPictureInPictureMode + val isInPictureInPicture = rememberIsInPipMode() DefaultPermissionHandler(videoPermission = permissions) diff --git a/stream-video-android-ui-core/api/stream-video-android-ui-core.api b/stream-video-android-ui-core/api/stream-video-android-ui-core.api index 3c7208df4a..37b85ba400 100644 --- a/stream-video-android-ui-core/api/stream-video-android-ui-core.api +++ b/stream-video-android-ui-core/api/stream-video-android-ui-core.api @@ -80,15 +80,17 @@ public final class io/getstream/video/android/ui/common/StreamCallActivity$Compa public final class io/getstream/video/android/ui/common/StreamCallActivityConfiguration { public fun ()V - public fun (ZZZLandroid/os/Bundle;)V - public synthetic fun (ZZZLandroid/os/Bundle;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZZLandroid/os/Bundle;)V + public synthetic fun (ZZZZLandroid/os/Bundle;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Z public final fun component2 ()Z public final fun component3 ()Z - public final fun component4 ()Landroid/os/Bundle; - public final fun copy (ZZZLandroid/os/Bundle;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; - public static synthetic fun copy$default (Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;ZZZLandroid/os/Bundle;ILjava/lang/Object;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public final fun component4 ()Z + public final fun component5 ()Landroid/os/Bundle; + public final fun copy (ZZZZLandroid/os/Bundle;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public static synthetic fun copy$default (Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;ZZZZLandroid/os/Bundle;ILjava/lang/Object;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; public fun equals (Ljava/lang/Object;)Z + public final fun getCanKeepScreenOn ()Z public final fun getCanSkiPermissionRationale ()Z public final fun getCloseScreenOnCallEnded ()Z public final fun getCloseScreenOnError ()Z diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt index b6dda4d512..f3c7ecbc22 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt @@ -24,6 +24,7 @@ import android.os.Build import android.os.Bundle import android.os.PersistableBundle import android.util.Rational +import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.annotation.CallSuper import androidx.lifecycle.lifecycleScope @@ -71,7 +72,7 @@ public abstract class StreamCallActivity : ComponentActivity() { private const val EXTRA_MEMBERS_ARRAY: String = "members_extra" // Extra default values - private const val DEFAULT_LEAVE_WHEN_LAST: Boolean = true + private const val DEFAULT_LEAVE_WHEN_LAST: Boolean = false private val defaultExtraMembers = emptyList() private val logger by taggedLogger("DefaultCallActivity") @@ -90,7 +91,7 @@ public abstract class StreamCallActivity : ComponentActivity() { context: Context, cid: StreamCallId, members: List = defaultExtraMembers, - leaveWhenLastInCall: Boolean = DEFAULT_LEAVE_WHEN_LAST, + leaveWhenLastInCall: Boolean = true, action: String? = null, clazz: Class, configuration: StreamCallActivityConfiguration = StreamCallActivityConfiguration(), @@ -178,7 +179,6 @@ public abstract class StreamCallActivity : ComponentActivity() { // Platform restriction public final override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - onPreCreate(savedInstanceState, null) logger.d { "Entered [onCreate(Bundle?)" } initializeCallOrFail( savedInstanceState, @@ -344,7 +344,9 @@ public abstract class StreamCallActivity : ComponentActivity() { * @param call */ public open fun onResume(call: Call) { - // No - op + if (configuration.canKeepScreenOn) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } logger.d { "DefaultCallActivity - Resumed (call -> $call)" } } @@ -354,7 +356,13 @@ public abstract class StreamCallActivity : ComponentActivity() { * @param call the call */ public open fun onPause(call: Call) { - if (isVideoCall(call) && !isInPictureInPictureMode) { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + // Default PiP behavior + if (isConnected(call) && + !isChangingConfigurations && + isVideoCall(call) && + !isInPictureInPictureMode + ) { enterPictureInPicture() } logger.d { "DefaultCallActivity - Paused (call -> $call)" } @@ -383,7 +391,7 @@ public abstract class StreamCallActivity : ComponentActivity() { * @param call the call */ public open fun onStop(call: Call) { - // Extension point only. + // No-op logger.d { "Default activity - stopped (call -> $call)" } } @@ -738,6 +746,7 @@ public abstract class StreamCallActivity : ComponentActivity() { onSuccessFinish.invoke(call) } } + is RealtimeConnection.Failed -> { lifecycleScope.launch { val conn = state as? RealtimeConnection.Failed @@ -746,6 +755,7 @@ public abstract class StreamCallActivity : ComponentActivity() { onErrorFinish.invoke(throwable) } } + else -> { // No-op } @@ -846,6 +856,14 @@ public abstract class StreamCallActivity : ComponentActivity() { } } + private fun isConnected(call: Call): Boolean = + when (call.state.connection.value) { + RealtimeConnection.Disconnected -> false + RealtimeConnection.PreJoin -> false + is RealtimeConnection.Failed -> false + else -> true + } + private suspend fun Result.onOutcome( call: Call, onSuccess: (suspend (Call) -> Unit)? = null, diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt index 206b7ba62f..05473f74bd 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt @@ -22,6 +22,7 @@ internal object StreamCallActivityConfigStrings { const val EXTRA_STREAM_CONFIG = "stream-activity-config" const val EXTRA_CLOSE_ON_ERROR = "close-on-error" const val EXTRA_CLOSE_ON_ENDED = "close-on-ended" + const val EXTRA_KEEP_SCREEN_ON = "keep-screen-on" const val EXTRA_CAN_SKIP_RATIONALE = "skip-rationale-allowed" const val EXTRA_CUSTOM = "custom-fields" } @@ -36,6 +37,8 @@ public data class StreamCallActivityConfiguration( val closeScreenOnCallEnded: Boolean = true, /** When set to false, the activity will simply ignore the `showRationale` from the system and show the rationale screen anyway. */ val canSkiPermissionRationale: Boolean = true, + /** When set to true, the activity will keep the screen on. */ + val canKeepScreenOn: Boolean = true, /** * Custom configuration extension for any extending classes. * Can be used same as normal extras. @@ -53,10 +56,12 @@ public fun Bundle.extractStreamActivityConfig(): StreamCallActivityConfiguration getBoolean(StreamCallActivityConfigStrings.EXTRA_CLOSE_ON_ENDED, true) val canSkipPermissionRationale = getBoolean(StreamCallActivityConfigStrings.EXTRA_CAN_SKIP_RATIONALE, true) + val canKeepScreenOn = getBoolean(StreamCallActivityConfigStrings.EXTRA_KEEP_SCREEN_ON, true) val custom = getBundle(StreamCallActivityConfigStrings.EXTRA_CUSTOM) return StreamCallActivityConfiguration( closeScreenOnError = closeScreenOnError, closeScreenOnCallEnded = closeScreenOnCallEnded, + canKeepScreenOn = canKeepScreenOn, canSkiPermissionRationale = canSkipPermissionRationale, custom = custom, ) @@ -73,6 +78,7 @@ public fun StreamCallActivityConfiguration.toBundle(): Bundle { StreamCallActivityConfigStrings.EXTRA_CAN_SKIP_RATIONALE, canSkiPermissionRationale, ) + bundle.putBoolean(StreamCallActivityConfigStrings.EXTRA_KEEP_SCREEN_ON, canKeepScreenOn) bundle.putBundle(StreamCallActivityConfigStrings.EXTRA_CUSTOM, custom) return bundle }