From 2acb43637c09debc7e862557547ea94bb3abb8bc Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Wed, 24 Apr 2024 15:19:59 +0900 Subject: [PATCH 1/6] Add Android video samples on README file (#1079) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 722237e298..ded14e46eb 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ If you're interested in customizing the UI components for the Video SDK, check o You can find sample projects below that demonstrates use cases of Stream Video SDK for Android: - [Demo App](https://github.com/GetStream/stream-video-android/tree/develop/demo-app): Demo App demonstrates Stream Video SDK for Android with modern Android tech stacks, such as Compose, Hilt, and Coroutines. +- [Android Video Samples](https://github.com/GetStream/Android-Video-Samples): Provides a collection of samples that utilize modern Android tech stacks and Stream Video SDK for Kotlin and Compose. - [WhatsApp Clone Compose](https://github.com/getstream/whatsapp-clone-compose): WhatsApp clone project demonstrates modern Android development built with Jetpack Compose and Stream Chat/Video SDK for Compose. - [Twitch Clone Compose](https://github.com/skydoves/twitch-clone-compose): Twitch clone project demonstrates modern Android development built with Jetpack Compose and Stream Chat/Video SDK for Compose. - [Meeting Room Compose](https://github.com/GetStream/meeting-room-compose): A real-time meeting room app built with Jetpack Compose to demonstrate video communications. From 11d0c61bd3e02d7682caf116e4965a72fab33786 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:45:15 +0300 Subject: [PATCH 2/6] Handle incoming call accept in "same user on multiple devices & incoming call activity" scenario (#1077) * Add acceptedOnThisDevice flag and user it to leave call if needed --- .../src/main/kotlin/io/getstream/video/android/core/Call.kt | 2 ++ .../kotlin/io/getstream/video/android/core/CallState.kt | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index a3e148a546..ffaa2d20e6 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -1015,6 +1015,8 @@ public class Call( } suspend fun accept(): Result { + state.acceptedOnThisDevice = true + clientImpl.state.removeRingingCall() clientImpl.state.maybeStopForegroundService() return clientImpl.accept(type, id) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt index 48dbdc5afa..7a960389f6 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt @@ -555,6 +555,8 @@ public class CallState( private var autoJoiningCall: Job? = null private var ringingTimerJob: Job? = null + internal var acceptedOnThisDevice: Boolean = false + fun handleEvent(event: VideoEvent) { logger.d { "Updating call state with event ${event::class.java}" } when (event) { @@ -594,6 +596,10 @@ public class CallState( call.join() autoJoiningCall = null } + } else if (callRingState is RingingState.Incoming && event.user.id == client.userId) { + // Call accepted by me + this device is Incoming => I accepted on another device + // Then leave the call on this device + if (!acceptedOnThisDevice) call.leave() } } From 470c4fc8835484d71e7affed41e6cee757c62650 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Tue, 30 Apr 2024 11:52:24 +0200 Subject: [PATCH 3/6] Call activity replace in demo app (#1078) * Make use of the StreamCallActivity in the demo-app --- demo-app/src/main/AndroidManifest.xml | 42 +--- .../getstream/video/android/CallActivity.kt | 108 +++++++++ .../video/android/DeeplinkingActivity.kt | 32 ++- .../video/android/DirectCallActivity.kt | 209 ------------------ .../video/android/IncomingCallActivity.kt | 174 --------------- .../video/android/ui/DogfoodingNavHost.kt | 17 +- .../video/android/ui/call/CallActivity.kt | 168 -------------- .../video/android/ui/lobby/CallLobbyScreen.kt | 8 +- .../ui/outgoing/DirectCallJoinScreen.kt | 68 ++++-- .../video/android/util/ProgressBar.kt | 73 ++++++ 10 files changed, 273 insertions(+), 626 deletions(-) create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt delete mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt delete mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt delete mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt create mode 100644 demo-app/src/main/kotlin/io/getstream/video/android/util/ProgressBar.kt diff --git a/demo-app/src/main/AndroidManifest.xml b/demo-app/src/main/AndroidManifest.xml index 353250d768..e883c5eabd 100644 --- a/demo-app/src/main/AndroidManifest.xml +++ b/demo-app/src/main/AndroidManifest.xml @@ -51,33 +51,6 @@ - - - - - - - - - - - - - - - + android:showOnLockScreen="true" + android:showWhenLocked="true" + android:launchMode="singleTop" + android:exported="false"> + + + + + diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt new file mode 100644 index 0000000000..eb1e16ebc7 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/CallActivity.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.models.Filters +import io.getstream.chat.android.models.querysort.QuerySortByField +import io.getstream.result.onSuccessSuspend +import io.getstream.video.android.compose.ui.ComposeStreamCallActivity +import io.getstream.video.android.compose.ui.StreamCallActivityComposeDelegate +import io.getstream.video.android.core.Call +import io.getstream.video.android.ui.call.CallScreen +import io.getstream.video.android.ui.common.StreamActivityUiDelegate +import io.getstream.video.android.ui.common.StreamCallActivity +import io.getstream.video.android.ui.common.StreamCallActivityConfiguration +import io.getstream.video.android.ui.common.util.StreamCallActivityDelicateApi +import io.getstream.video.android.util.FullScreenCircleProgressBar + +@OptIn(StreamCallActivityDelicateApi::class) +class CallActivity : ComposeStreamCallActivity() { + + override val uiDelegate: StreamActivityUiDelegate = StreamDemoUiDelegate() + override val configuration: StreamCallActivityConfiguration = + StreamCallActivityConfiguration(closeScreenOnCallEnded = false) + + private class StreamDemoUiDelegate : StreamCallActivityComposeDelegate() { + + @Composable + override fun StreamCallActivity.LoadingContent(call: Call) { + // Use as loading screen.. so the layout is shown. + if (call.type == "default") { + VideoCallContent(call = call) + } else { + FullScreenCircleProgressBar(text = "Connecting...") + } + } + + @Composable + override fun StreamCallActivity.CallDisconnectedContent(call: Call) { + goBackToMainScreen() + } + + @Composable + override fun StreamCallActivity.VideoCallContent(call: Call) { + CallScreen( + call = call, + showDebugOptions = BuildConfig.DEBUG, + onCallDisconnected = { + leave(call) + goBackToMainScreen() + }, + onUserLeaveCall = { + leave(call) + goBackToMainScreen() + }, + ) + + // step 4 (optional) - chat integration + val user by ChatClient.instance().clientState.user.collectAsState(initial = null) + LaunchedEffect(key1 = user) { + if (user != null) { + val channel = ChatClient.instance().channel("videocall", call.id) + channel.queryMembers( + offset = 0, + limit = 10, + filter = Filters.neutral(), + sort = QuerySortByField(), + ).await().onSuccessSuspend { members -> + if (members.isNotEmpty()) { + channel.addMembers(listOf(user!!.id)).await() + } else { + channel.create(listOf(user!!.id), emptyMap()).await() + } + } + } + } + } + + private fun StreamCallActivity.goBackToMainScreen() { + if (!isFinishing) { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + startActivity(intent) + finish() + } + } + } +} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt index 7c0d1e5800..88147e1093 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/DeeplinkingActivity.kt @@ -40,7 +40,7 @@ import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.datastore.delegate.StreamUserDataStore import io.getstream.video.android.model.StreamCallId -import io.getstream.video.android.ui.call.CallActivity +import io.getstream.video.android.ui.common.StreamCallActivity import io.getstream.video.android.util.InitializedState import io.getstream.video.android.util.StreamVideoInitHelper import io.getstream.video.android.util.config.AppConfig @@ -110,16 +110,17 @@ class DeeplinkingActivity : ComponentActivity() { joinCall(data, callId) } else { // first ask for push notification permission - val manager = NotificationPermissionManager.createNotificationPermissionsManager( - application = app, - requestPermissionOnAppLaunch = { true }, - onPermissionStatus = { - // we don't care about the result for demo purposes - if (it != NotificationPermissionStatus.REQUESTED) { - joinCall(data, callId) - } - }, - ) + val manager = + NotificationPermissionManager.createNotificationPermissionsManager( + application = app, + requestPermissionOnAppLaunch = { true }, + onPermissionStatus = { + // we don't care about the result for demo purposes + if (it != NotificationPermissionStatus.REQUESTED) { + joinCall(data, callId) + } + }, + ) manager.start() } } else { @@ -179,13 +180,10 @@ class DeeplinkingActivity : ComponentActivity() { if (it == InitializedState.FINISHED || it == InitializedState.FAILED) { if (StreamVideo.isInstalled) { val callId = StreamCallId(type = "default", id = cid) - val intent = CallActivity.createIntent( + val intent = StreamCallActivity.callIntent( context = this@DeeplinkingActivity, - callId = callId, - disableMicOverride = intent.getBooleanExtra( - EXTRA_DISABLE_MIC_OVERRIDE, - false, - ), + cid = callId, + clazz = CallActivity::class.java, ).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt deleted file mode 100644 index 695fca3af3..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/DirectCallActivity.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-video-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.video.android - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.background -import androidx.compose.ui.Modifier -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import io.getstream.result.Result -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.ui.components.call.ringing.RingingCallContent -import io.getstream.video.android.core.Call -import io.getstream.video.android.core.StreamVideo -import io.getstream.video.android.core.call.state.AcceptCall -import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.core.call.state.CancelCall -import io.getstream.video.android.core.call.state.DeclineCall -import io.getstream.video.android.core.call.state.LeaveCall -import io.getstream.video.android.core.call.state.ToggleCamera -import io.getstream.video.android.core.call.state.ToggleMicrophone -import io.getstream.video.android.core.call.state.ToggleSpeakerphone -import io.getstream.video.android.datastore.delegate.StreamUserDataStore -import io.getstream.video.android.model.mapper.isValidCallId -import io.getstream.video.android.model.mapper.toTypeAndId -import io.getstream.video.android.ui.call.CallScreen -import io.getstream.video.android.util.StreamVideoInitHelper -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.openapitools.client.models.CallRejectedEvent -import java.util.UUID -import javax.inject.Inject - -@AndroidEntryPoint -class DirectCallActivity : ComponentActivity() { - - @Inject - lateinit var dataStore: StreamUserDataStore - private lateinit var call: Call - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - lifecycleScope.launch { - // Not necessary if you initialise the SDK in Application.onCreate() - StreamVideoInitHelper.loadSdk(dataStore = dataStore) - - // Create Call ID if it wasn't supplied by Intent - val callId: String = intent.getStringExtra(EXTRA_CID) - ?: "default:${UUID.randomUUID()}" - - val (type, id) = if (callId.isValidCallId()) { - callId.toTypeAndId() - } else { - "default" to callId - } - - // Create call object - call = StreamVideo.instance().call(type, id) - - // Get list of members - val members: List = intent.getStringArrayExtra(EXTRA_MEMBERS_ARRAY)?.asList() ?: emptyList() - - // You must add yourself as member too - val membersWithMe = members.toMutableList().apply { add(call.user.id) } - - // Ring the members - val result = call.create(ring = true, memberIds = membersWithMe) - - // Update the call - call.get() - - call.subscribe { - when (it) { - // Finish this activity if ever a call.reject is received - is CallRejectedEvent -> { - finish() - } - } - } - - if (result is Result.Failure) { - // Failed to recover the current state of the call - // TODO: Automaticly call this in the SDK? - Log.e("DirectCallActivity", "Call.create failed ${result.value}") - Toast.makeText( - this@DirectCallActivity, - "Failed get call status (${result.value.message})", - Toast.LENGTH_SHORT, - ).show() - finish() - } - - setContent { - VideoTheme { - val onCallAction: (CallAction) -> Unit = { callAction -> - when (callAction) { - is ToggleCamera -> call.camera.setEnabled(callAction.isEnabled) - is ToggleMicrophone -> call.microphone.setEnabled( - callAction.isEnabled, - ) - is ToggleSpeakerphone -> call.speaker.setEnabled(callAction.isEnabled) - is LeaveCall -> { - call.leave() - finish() - } - is DeclineCall -> { - reject(call) - } - is CancelCall -> { - reject(call) - } - is AcceptCall -> { - lifecycleScope.launch { - call.accept() - call.join() - } - } - - else -> Unit - } - } - - RingingCallContent( - modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), - call = call, - onBackPressed = { - reject(call) - }, - onAcceptedContent = { - CallScreen( - call = call, - showDebugOptions = BuildConfig.DEBUG, - onCallDisconnected = { - finish() - }, - onUserLeaveCall = { - call.leave() - finish() - }, - ) - }, - onNoAnswerContent = { - leave() - }, - onRejectedContent = { - reject(call) - }, - onCallAction = onCallAction, - ) - } - } - } - } - - private fun reject(call: Call) { - lifecycleScope.launch(Dispatchers.IO) { - call.reject() - withContext(Dispatchers.Main) { - finish() - } - } - } - private fun leave() { - lifecycleScope.launch(Dispatchers.IO) { - withContext(Dispatchers.Main) { - finish() - } - } - } - - companion object { - const val EXTRA_CID: String = "EXTRA_CID" - const val EXTRA_MEMBERS_ARRAY: String = "EXTRA_MEMBERS_ARRAY" - - @JvmStatic - fun createIntent( - context: Context, - callId: String? = null, - members: List, - ): Intent { - return Intent(context, DirectCallActivity::class.java).apply { - putExtra(EXTRA_CID, callId) - putExtra(EXTRA_MEMBERS_ARRAY, members.toTypedArray()) - } - } - } -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt deleted file mode 100644 index 417bfb1b37..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/IncomingCallActivity.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-video-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.video.android - -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.WindowManager -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.background -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import io.getstream.result.Result -import io.getstream.video.android.compose.theme.VideoTheme -import io.getstream.video.android.compose.ui.components.call.ringing.RingingCallContent -import io.getstream.video.android.core.StreamVideo -import io.getstream.video.android.core.call.state.AcceptCall -import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.core.call.state.DeclineCall -import io.getstream.video.android.core.call.state.FlipCamera -import io.getstream.video.android.core.call.state.LeaveCall -import io.getstream.video.android.core.call.state.ToggleCamera -import io.getstream.video.android.core.call.state.ToggleMicrophone -import io.getstream.video.android.core.call.state.ToggleSpeakerphone -import io.getstream.video.android.core.notifications.NotificationHandler -import io.getstream.video.android.datastore.delegate.StreamUserDataStore -import io.getstream.video.android.model.streamCallId -import io.getstream.video.android.ui.call.CallScreen -import io.getstream.video.android.util.StreamVideoInitHelper -import kotlinx.coroutines.launch -import javax.inject.Inject - -@AndroidEntryPoint -class IncomingCallActivity : ComponentActivity() { - - @Inject - lateinit var dataStore: StreamUserDataStore - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // release the lock, turn on screen, and keep the device awake. - showWhenLockedAndTurnScreenOn() - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - - val callId = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)!! - - lifecycleScope.launch { - // Not necessary if you initialise the SDK in Application.onCreate() - StreamVideoInitHelper.loadSdk(dataStore = dataStore) - val call = StreamVideo.instance().call(callId.type, callId.id) - - // Update the call state. This activity could have been started from a push notification. - // Doing a call.get() will also internally update the Call state object with the latest - // state from the backend. - val result = call.get() - - if (result is Result.Failure) { - // Failed to recover the current state of the call - // TODO: Automaticly call this in the SDK? - Log.e("IncomingCallActivity", "Call.join failed ${result.value}") - Toast.makeText( - this@IncomingCallActivity, - "Failed get call status (${result.value.message})", - Toast.LENGTH_SHORT, - ).show() - finish() - } - - // We also check if savedInstanceState is null to prevent duplicate calls when activity - // is recreated (e.g. when entering PiP mode) - if (NotificationHandler.ACTION_ACCEPT_CALL == intent.action && savedInstanceState == null) { - call.accept() - call.join() - } - - setContent { - VideoTheme { - val onCallAction: (CallAction) -> Unit = { callAction -> - when (callAction) { - is ToggleCamera -> call.camera.setEnabled(callAction.isEnabled) - is ToggleMicrophone -> call.microphone.setEnabled(callAction.isEnabled) - is ToggleSpeakerphone -> call.speaker.setEnabled(callAction.isEnabled) - is FlipCamera -> call.camera.flip() - is LeaveCall -> { - call.leave() - finish() - } - is DeclineCall -> { - lifecycleScope.launch { - call.reject() - call.leave() - finish() - } - } - is AcceptCall -> { - lifecycleScope.launch { - call.accept() - call.join() - } - } - else -> Unit - } - } - RingingCallContent( - modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), - call = call, - onBackPressed = { - call.leave() - finish() - }, - onAcceptedContent = { - CallScreen( - call = call, - showDebugOptions = BuildConfig.DEBUG, - onCallDisconnected = { - finish() - }, - onUserLeaveCall = { - call.leave() - finish() - }, - ) - }, - onNoAnswerContent = { - LaunchedEffect(key1 = call) { - call.leave() - finish() - } - }, - onRejectedContent = { - LaunchedEffect(key1 = call) { - call.reject() - finish() - } - }, - onCallAction = onCallAction, - ) - } - } - } - } - - private fun showWhenLockedAndTurnScreenOn() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(true) - setTurnScreenOn(true) - } else { - @Suppress("DEPRECATION") - window.addFlags( - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON, - ) - } - } -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt index 26fc22eaa0..dd82634bd2 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt @@ -26,7 +26,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import io.getstream.video.android.DirectCallActivity +import io.getstream.video.android.CallActivity +import io.getstream.video.android.core.notifications.NotificationHandler +import io.getstream.video.android.ui.common.StreamCallActivity import io.getstream.video.android.ui.join.CallJoinScreen import io.getstream.video.android.ui.join.barcode.BarcodeScanner import io.getstream.video.android.ui.lobby.CallLobbyScreen @@ -46,7 +48,8 @@ fun AppNavHost( ) { composable(AppScreens.Login.route) { backStackEntry -> LoginScreen( - autoLogIn = backStackEntry.arguments?.getString("auto_log_in")?.let { it.toBoolean() } ?: true, + autoLogIn = backStackEntry.arguments?.getString("auto_log_in") + ?.let { it.toBoolean() } ?: true, navigateToCallJoin = { navController.navigate(AppScreens.CallJoin.route) { popUpTo(AppScreens.Login.route) { inclusive = true } @@ -85,11 +88,14 @@ fun AppNavHost( composable(AppScreens.DirectCallJoin.route) { val context = LocalContext.current DirectCallJoinScreen( - navigateToDirectCall = { members -> + navigateToDirectCall = { cid, members -> context.startActivity( - DirectCallActivity.createIntent( - context, + StreamCallActivity.callIntent( + action = NotificationHandler.ACTION_OUTGOING_CALL, + context = context, + cid = cid, members = members.split(","), + clazz = CallActivity::class.java, ), ) }, @@ -109,6 +115,7 @@ enum class AppScreens(val route: String) { CallLobby("call_lobby/{cid}"), DirectCallJoin("direct_call_join"), BarcodeScanning("barcode_scanning"), ; + fun routeWithArg(argValue: Any): String = when (this) { Login -> this.route.replace("{auto_log_in}", argValue.toString()) CallLobby -> this.route.replace("{cid}", argValue.toString()) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt deleted file mode 100644 index de4e534b9b..0000000000 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallActivity.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-video-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.getstream.video.android.ui.call - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.view.WindowManager -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.lifecycle.lifecycleScope -import io.getstream.chat.android.client.ChatClient -import io.getstream.chat.android.models.Filters -import io.getstream.chat.android.models.querysort.QuerySortByField -import io.getstream.result.Result -import io.getstream.result.onSuccessSuspend -import io.getstream.video.android.MainActivity -import io.getstream.video.android.core.StreamVideo -import io.getstream.video.android.core.notifications.NotificationHandler -import io.getstream.video.android.model.StreamCallId -import io.getstream.video.android.model.streamCallId -import kotlinx.coroutines.launch - -class CallActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - - // step 1 - get the StreamVideo instance and create a call - val streamVideo = StreamVideo.instance() - val cid = intent.streamCallId(EXTRA_CID) - ?: throw IllegalArgumentException("call type and id is invalid!") - - // optional - check for already active call that can be utilized - // This step is optional and can be skipped - // val cal = streamVideo.call(type = cid.type, id = cid.id) - val activeCall = streamVideo.state.activeCall.value - val call = if (activeCall != null) { - if (activeCall.id != cid.id) { - Log.w("CallActivity", "A call with id: ${cid.cid} existed. Leaving.") - // If the call id is different leave the previous call - activeCall.leave() - // Return a new call - streamVideo.call(type = cid.type, id = cid.id) - } else { - // Call ID is the same, use the active call - activeCall - } - } else { - // There is no active call, create new call - streamVideo.call(type = cid.type, id = cid.id) - } - - // optional - call settings. We disable the mic if coming from QR code demo - if (intent.getBooleanExtra(EXTRA_DISABLE_MIC_BOOLEAN, false)) { - call.microphone.disable(true) - } - - // step 2 - join a call - lifecycleScope.launch { - // If the call is new, join the call - if (activeCall != call) { - val result = call.join(create = true) - - // Unable to join. Device is offline or other usually connection issue. - if (result is Result.Failure) { - Log.e("CallActivity", "Call.join failed ${result.value}") - Toast.makeText( - this@CallActivity, - "Failed to join call (${result.value.message})", - Toast.LENGTH_SHORT, - ).show() - finish() - } - } - } - - // step 3 - build a call screen - setContent { - CallScreen( - call = call, - showDebugOptions = io.getstream.video.android.BuildConfig.DEBUG, - onCallDisconnected = { - // call state changed to disconnected - we can leave the screen - goBackToMainScreen() - }, - onUserLeaveCall = { - call.leave() - // we don't need to wait for the call state to change to disconnected, we can - // leave immediately - goBackToMainScreen() - }, - ) - - // step 4 (optional) - chat integration - val user by ChatClient.instance().clientState.user.collectAsState(initial = null) - LaunchedEffect(key1 = user) { - if (user != null) { - val channel = ChatClient.instance().channel("videocall", cid.id) - channel.queryMembers( - offset = 0, - limit = 10, - filter = Filters.neutral(), - sort = QuerySortByField(), - ).await().onSuccessSuspend { members -> - if (members.isNotEmpty()) { - channel.addMembers(listOf(user!!.id)).await() - } else { - channel.create(listOf(user!!.id), emptyMap()).await() - } - } - } - } - } - } - - private fun goBackToMainScreen() { - if (!isFinishing) { - val intent = Intent(this@CallActivity, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - startActivity(intent) - finish() - } - } - - companion object { - const val EXTRA_CID: String = NotificationHandler.INTENT_EXTRA_CALL_CID - const val EXTRA_DISABLE_MIC_BOOLEAN: String = "EXTRA_DISABLE_MIC" - - /** - * @param callId the Call ID you want to join - * @param disableMicOverride optional parameter if you want to override the users setting - * and disable the microphone. - */ - @JvmStatic - fun createIntent( - context: Context, - callId: StreamCallId, - disableMicOverride: Boolean = false, - ): Intent { - return Intent(context, CallActivity::class.java).apply { - putExtra(EXTRA_CID, callId) - putExtra(EXTRA_DISABLE_MIC_BOOLEAN, disableMicOverride) - } - } - } -} diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt index 2098024078..91e0c44805 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/lobby/CallLobbyScreen.kt @@ -64,6 +64,7 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.BuildConfig +import io.getstream.video.android.CallActivity import io.getstream.video.android.R import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar @@ -76,7 +77,7 @@ import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall import io.getstream.video.android.mock.previewUsers import io.getstream.video.android.model.User -import io.getstream.video.android.ui.call.CallActivity +import io.getstream.video.android.ui.common.StreamCallActivity import io.getstream.video.android.util.LockScreenOrientation import kotlinx.coroutines.delay @@ -338,9 +339,10 @@ private fun HandleCallLobbyUiState( LaunchedEffect(key1 = callLobbyUiState) { when (callLobbyUiState) { is CallLobbyUiState.JoinCompleted -> { - val intent = CallActivity.createIntent( + val intent = StreamCallActivity.callIntent( context = context, - callId = callLobbyViewModel.callId, + cid = callLobbyViewModel.callId, + clazz = CallActivity::class.java, ).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt index f30bd981ff..a3d67e3935 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.material.RadioButtonDefaults import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Call +import androidx.compose.material.icons.filled.VideoCall import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -53,13 +54,15 @@ import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.avatar.UserAvatar import io.getstream.video.android.compose.ui.components.base.StreamButton import io.getstream.video.android.mock.previewUsers +import io.getstream.video.android.model.StreamCallId import io.getstream.video.android.model.User import io.getstream.video.android.models.GoogleAccount +import java.util.UUID @Composable fun DirectCallJoinScreen( viewModel: DirectCallJoinViewModel = hiltViewModel(), - navigateToDirectCall: (memberList: String) -> Unit, + navigateToDirectCall: (cid: StreamCallId, memberList: String) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -125,7 +128,7 @@ private fun Header(user: User?) { private fun Body( uiState: DirectCallUiState, toggleUserSelection: (Int) -> Unit, - onStartCallClick: (membersList: String) -> Unit, + onStartCallClick: (cid: StreamCallId, membersList: String) -> Unit, ) { Box( modifier = Modifier @@ -145,23 +148,52 @@ private fun Body( entries = users, onUserClick = { clickedIndex -> toggleUserSelection(clickedIndex) }, ) - StreamButton( - // Floating button - modifier = Modifier + + Row( + Modifier + .fillMaxWidth() .align(Alignment.BottomCenter) .padding(bottom = 10.dp), - enabled = users.any { it.isSelected }, - icon = Icons.Default.Call, - text = "Start call", - style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), - onClick = { - onStartCallClick( - users - .filter { it.isSelected } - .joinToString(separator = ",") { it.account.id ?: "" }, - ) - }, - ) + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + StreamButton( + // Floating button + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(bottom = 10.dp), + enabled = users.any { it.isSelected }, + icon = Icons.Default.Call, + text = "Audio call", + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), + onClick = { + onStartCallClick( + StreamCallId("audio_call", UUID.randomUUID().toString()), + users + .filter { it.isSelected } + .joinToString(separator = ",") { it.account.id ?: "" }, + ) + }, + ) + + StreamButton( + // Floating button + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(bottom = 10.dp), + enabled = users.any { it.isSelected }, + icon = Icons.Default.VideoCall, + text = "Video call", + style = VideoTheme.styles.buttonStyles.secondaryButtonStyle(), + onClick = { + onStartCallClick( + StreamCallId("default", UUID.randomUUID().toString()), + users + .filter { it.isSelected } + .joinToString(separator = ",") { it.account.id ?: "" }, + ) + }, + ) + } } ?: Text( text = stringResource(io.getstream.video.android.R.string.cannot_load_google_account_list), modifier = Modifier @@ -256,7 +288,7 @@ private fun HeaderPreview() { }, ), toggleUserSelection = {}, - ) { + ) { _, _ -> } } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/ProgressBar.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/ProgressBar.kt new file mode 100644 index 0000000000..896df6eb33 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/ProgressBar.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.util + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.getstream.video.android.R +import io.getstream.video.android.compose.theme.VideoTheme + +@Composable +fun FullScreenCircleProgressBar(text: String) { + Box( + modifier = Modifier + .fillMaxSize() + .background(VideoTheme.colors.baseSheetPrimary), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Image( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + painter = painterResource(id = R.drawable.stream_calls_logo), + contentDescription = null, + contentScale = ContentScale.FillWidth, + ) + Spacer(modifier = Modifier.size(32.dp)) + CircularProgressIndicator(color = VideoTheme.colors.brandPrimary) + Text( + text = text, + style = VideoTheme.typography.bodyL, + ) + } + } +} + +@Preview +@Composable +private fun FullScreenProgressBarPreview() { + VideoTheme { + FullScreenCircleProgressBar(text = "Loading...") + } +} From cd7dd7062b9ca0d128278974fd7df6e03d2abd79 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Thu, 2 May 2024 07:06:36 +0300 Subject: [PATCH 4/6] Improve peer connection health check to prevent reconnecting (#1081) Co-authored-by: Aleksandar Apostolov --- .../video/android/core/CallHealthMonitor.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt index ba80162ae6..cfc0de15ab 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallHealthMonitor.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.openapitools.client.models.OwnCapability import org.threeten.bp.OffsetDateTime import org.threeten.bp.temporal.ChronoUnit import org.webrtc.PeerConnection @@ -117,13 +118,19 @@ public class CallHealthMonitor( val subscriberState = call.session?.subscriber?.state?.value val publisherState = call.session?.publisher?.state?.value - val healthyPeerConnections = subscriberState in goodStates && publisherState in goodStates + val canPublish = call.state.ownCapabilities.value.any { + it == OwnCapability.SendAudio || it == OwnCapability.SendVideo + } + + val isSubConnectionHealthy = subscriberState in goodStates + val isPubConnectionHealthyOrNotNeeded = publisherState in goodStates || (publisherState == null && !canPublish) + val arePeerConnectionsHealthy = isSubConnectionHealthy && isPubConnectionHealthyOrNotNeeded logger.d { - "checking call health: peers are healthy: $healthyPeerConnections publisher $publisherState subscriber $subscriberState" + "checking call health: peers are healthy: $arePeerConnectionsHealthy publisher $publisherState subscriber $subscriberState" } - if (healthyPeerConnections) { + if (arePeerConnectionsHealthy) { // don't reconnect if things are healthy timeoutJob?.cancel() timeoutJob = null From db1d2373f2a1a08c70316b11d3ffed280718c6df Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Fri, 3 May 2024 20:18:08 +0700 Subject: [PATCH 5/6] Allow to implement pip content for AudioRoomContent (#1083) --- .../api/stream-video-android-ui-compose.api | 2 +- .../ui/components/audio/AudioRoomContent.kt | 93 +++++++++++++++---- .../src/main/AndroidManifest.xml | 2 + .../android/tutorial/audio/MainActivity.kt | 4 + 4 files changed, 80 insertions(+), 21 deletions(-) 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 1020c9a3cb..e6b2102118 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 @@ -430,7 +430,7 @@ public final class io/getstream/video/android/compose/ui/components/audio/AudioR } public final class io/getstream/video/android/compose/ui/components/audio/AudioRoomContentKt { - public static final fun AudioRoomContent (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZLio/getstream/video/android/compose/permission/VideoPermissionsState;Ljava/lang/String;Lkotlin/jvm/functions/Function3;Lio/getstream/video/android/compose/ui/components/audio/AudioRendererStyle;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun AudioRoomContent (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZLio/getstream/video/android/compose/permission/VideoPermissionsState;Ljava/lang/String;Lkotlin/jvm/functions/Function3;Lio/getstream/video/android/compose/ui/components/audio/AudioRendererStyle;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/audio/ComposableSingletons$AudioAppBarKt { 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 55390e08df..486765419f 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 @@ -17,6 +17,7 @@ package io.getstream.video.android.compose.ui.components.audio import android.content.res.Configuration +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -27,7 +28,9 @@ import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag @@ -36,8 +39,12 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.getstream.log.StreamLog +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.theme.VideoTheme import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState @@ -55,6 +62,8 @@ import io.getstream.video.android.mock.previewCall * @param controlsContent Content is shown that allows users to trigger different actions to control a joined call. * @param audioRenderer A single audio renderer renders each individual participant. * @param onLeaveRoom A lambda that will be invoked when the leave quietly button was clicked. + * @param enableInPictureInPicture If the user has engaged in Picture-In-Picture mode. + * @param pictureInPictureContent Content shown when the user enters Picture in Picture mode, if it's been enabled in the app. * @param audioContent Content is shown by rendering audio when we're connected to a call successfully. */ @Composable @@ -95,6 +104,12 @@ public fun AudioRoomContent( ) }, onLeaveRoom: (() -> Unit)? = null, + onBackPressed: () -> Unit = {}, + enableInPictureInPicture: Boolean = true, + pictureInPictureContent: @Composable ( + call: Call, + orientation: Int, + ) -> Unit = { call, _ -> DefaultPictureInPictureContent(call, audioContent) }, controlsContent: @Composable (call: Call) -> Unit = { AudioControlActions( modifier = Modifier @@ -105,29 +120,67 @@ public fun AudioRoomContent( ) }, ) { + val context = LocalContext.current + val orientation = LocalConfiguration.current.orientation + val isInPictureInPicture = context.isInPictureInPictureMode + DefaultPermissionHandler(videoPermission = permissions) - Scaffold( - modifier = modifier - .background(VideoTheme.colors.baseSheetPrimary) - .padding(32.dp), - contentColor = VideoTheme.colors.baseSheetPrimary, - topBar = { - if (isShowingAppBar) { - appBarContent.invoke(call) - } - }, - bottomBar = { controlsContent.invoke(call) }, - content = { paddings -> - Box( - modifier = Modifier - .background(color = VideoTheme.colors.baseSheetPrimary) - .padding(paddings), - ) { - audioContent.invoke(this, call) - } - }, + MediaPiPLifecycle( + call = call, + enableInPictureInPicture = enableInPictureInPicture, ) + + BackHandler { + if (enableInPictureInPicture) { + try { + enterPictureInPicture(context = context, call = call) + } catch (e: Exception) { + StreamLog.e(tag = "AudioRoomContent") { e.stackTraceToString() } + call.leave() + } + } else { + onBackPressed.invoke() + } + } + + if (isInPictureInPicture && enableInPictureInPicture) { + pictureInPictureContent(call, orientation) + } else { + Scaffold( + modifier = modifier + .background(VideoTheme.colors.baseSheetPrimary) + .padding(32.dp), + contentColor = VideoTheme.colors.baseSheetPrimary, + topBar = { + if (isShowingAppBar) { + appBarContent.invoke(call) + } + }, + bottomBar = { controlsContent.invoke(call) }, + content = { paddings -> + Box( + modifier = Modifier + .background(color = VideoTheme.colors.baseSheetPrimary) + .padding(paddings), + ) { + audioContent.invoke(this, call) + } + }, + ) + } +} + +@Composable +internal fun DefaultPictureInPictureContent( + call: Call, + audioContent: @Composable BoxScope.(call: Call) -> Unit, +) { + Box( + modifier = Modifier.background(color = VideoTheme.colors.baseSheetPrimary), + ) { + audioContent.invoke(this, call) + } } @Composable diff --git a/tutorials/tutorial-audio/src/main/AndroidManifest.xml b/tutorials/tutorial-audio/src/main/AndroidManifest.xml index 767d39b1fe..daa982e368 100644 --- a/tutorials/tutorial-audio/src/main/AndroidManifest.xml +++ b/tutorials/tutorial-audio/src/main/AndroidManifest.xml @@ -32,7 +32,9 @@ diff --git a/tutorials/tutorial-audio/src/main/kotlin/io/getstream/video/android/tutorial/audio/MainActivity.kt b/tutorials/tutorial-audio/src/main/kotlin/io/getstream/video/android/tutorial/audio/MainActivity.kt index 7fec3955c7..b3ec0ab2e9 100644 --- a/tutorials/tutorial-audio/src/main/kotlin/io/getstream/video/android/tutorial/audio/MainActivity.kt +++ b/tutorials/tutorial-audio/src/main/kotlin/io/getstream/video/android/tutorial/audio/MainActivity.kt @@ -79,6 +79,10 @@ class MainActivity : ComponentActivity() { AudioRoomContent( call = call, title = "Audio Room Number 05", + onLeaveRoom = { + call.leave() + finish() + }, ) } else { Box(modifier = Modifier.fillMaxSize()) { From ba0f534226a7c0784bcb24434af208c8358b1b63 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 3 May 2024 15:38:37 +0200 Subject: [PATCH 6/6] Prepare release 0.5.8 (#1084) --- .../main/kotlin/io/getstream/video/android/Configuration.kt | 6 +++--- docusaurus/docs/Android/02-tutorials/01-video-calling.mdx | 2 +- docusaurus/docs/Android/02-tutorials/02-audio-room.mdx | 2 +- docusaurus/docs/Android/02-tutorials/03-livestream.mdx | 2 +- docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt b/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt index 6ee0dd0192..9e19931de0 100644 --- a/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt +++ b/buildSrc/src/main/kotlin/io/getstream/video/android/Configuration.kt @@ -6,11 +6,11 @@ object Configuration { const val minSdk = 24 const val majorVersion = 0 const val minorVersion = 5 - const val patchVersion = 7 + const val patchVersion = 8 const val versionName = "$majorVersion.$minorVersion.$patchVersion" - const val versionCode = 21 + const val versionCode = 22 const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" const val artifactGroup = "io.getstream" - const val streamVideoCallGooglePlayVersion = "1.0.9" + const val streamVideoCallGooglePlayVersion = "1.0.10" const val streamWebRtcVersionName = "1.1.1" } diff --git a/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx b/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx index 1c87c46334..7eecaa53bc 100644 --- a/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx +++ b/docusaurus/docs/Android/02-tutorials/01-video-calling.mdx @@ -31,7 +31,7 @@ If you're new to android, note that there are 2 `build.gradle` files, you want t ```kotlin dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-ui-compose:0.5.7") + implementation("io.getstream:stream-video-android-ui-compose:0.5.8") // Optionally add Jetpack Compose if Android studio didn't automatically include them implementation(platform("androidx.compose:compose-bom:2023.08.00")) diff --git a/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx b/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx index 4d09914166..15656e0111 100644 --- a/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx +++ b/docusaurus/docs/Android/02-tutorials/02-audio-room.mdx @@ -35,7 +35,7 @@ If you're new to android, note that there are 2 `build.gradle` files, you want t ```groovy dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-ui-compose:0.5.7") + implementation("io.getstream:stream-video-android-ui-compose:0.5.8") // Jetpack Compose (optional/ android studio typically adds them when you create a new project) implementation(platform("androidx.compose:compose-bom:2023.08.00")) diff --git a/docusaurus/docs/Android/02-tutorials/03-livestream.mdx b/docusaurus/docs/Android/02-tutorials/03-livestream.mdx index 3bd8ac0c4d..3509ef9b6b 100644 --- a/docusaurus/docs/Android/02-tutorials/03-livestream.mdx +++ b/docusaurus/docs/Android/02-tutorials/03-livestream.mdx @@ -35,7 +35,7 @@ If you're new to android, note that there are 2 `build.gradle` files, you want t ```kotlin dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-ui-compose:0.5.7") + implementation("io.getstream:stream-video-android-ui-compose:0.5.8") // Jetpack Compose (optional/ android studio typically adds them when you create a new project) implementation(platform("androidx.compose:compose-bom:2023.08.00")) diff --git a/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx b/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx index c8ed359a59..7d2f28a79f 100644 --- a/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx +++ b/docusaurus/docs/Android/06-advanced/07-chat-with-video.mdx @@ -31,7 +31,7 @@ Let the project sync. It should have all the dependencies required for you to fi ```groovy dependencies { // Stream Video Compose SDK - implementation("io.getstream:stream-video-android-ui-compose:0.5.7") + implementation("io.getstream:stream-video-android-ui-compose:0.5.8") // Stream Chat implementation(libs.stream.chat.compose)