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...")
+ }
+}