Skip to content

Commit 4e4e9c0

Browse files
committed
Implement WhatsAppVideoCallViewModel
1 parent 0c38218 commit 4e4e9c0

File tree

10 files changed

+155
-67
lines changed

10 files changed

+155
-67
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848

4949
<provider
5050
android:name="androidx.startup.InitializationProvider"
51-
android:authorities="io.getstream.whatsappclone.androidx-startup"
51+
android:authorities="${applicationId}.androidx-startup"
5252
android:exported="false"
5353
tools:node="merge">
5454
<meta-data

app/src/main/kotlin/io/getstream/whatsappclone/initializer/MainInitializer.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package io.getstream.whatsappclone.initializer
1919
import android.content.Context
2020
import androidx.startup.Initializer
2121
import io.getstream.whatsappclone.chats.initializer.StreamChatInitializer
22+
import io.getstream.whatsappclone.chats.initializer.StreamLogInitializer
2223
import io.getstream.whatsappclone.video.initializer.StreamVideoInitializer
2324

2425
class MainInitializer : Initializer<Unit> {
@@ -27,6 +28,7 @@ class MainInitializer : Initializer<Unit> {
2728
}
2829

2930
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(
31+
StreamLogInitializer::class.java,
3032
StreamChatInitializer::class.java,
3133
StreamVideoInitializer::class.java
3234
)

app/src/main/kotlin/io/getstream/whatsappclone/navigation/WhatsAppNavigation.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import io.getstream.whatsappclone.chats.messages.WhatsAppMessages
2626
import io.getstream.whatsappclone.model.WhatsAppUser
2727
import io.getstream.whatsappclone.ui.WhatsAppTabPager
2828
import io.getstream.whatsappclone.ui.WhatsAppTopBar
29+
import io.getstream.whatsappclone.video.WhatsAppVideoCall
2930

3031
fun NavGraphBuilder.whatsAppHomeNavigation() {
3132
composable(route = WhatsAppScreens.Home.name) {
@@ -63,8 +64,8 @@ fun NavGraphBuilder.whatsAppHomeNavigation() {
6364
route = WhatsAppScreens.VideoCall.name,
6465
arguments = WhatsAppScreens.VideoCall.navArguments
6566
) {
66-
val callId = it.arguments?.getString(WhatsAppScreens.VideoCall.KEY_CALL_ID)
67+
val callId = it.arguments?.getString(WhatsAppScreens.VideoCall.KEY_CALL_ID) ?: return@composable
6768

68-
// TODO
69+
WhatsAppVideoCall(id = callId)
6970
}
7071
}

core/designsystem/src/main/kotlin/io/getstream/whatsappclone/designsystem/component/WhatsAppLoadingIndicator.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ package io.getstream.whatsappclone.designsystem.component
1818

1919
import androidx.compose.material3.CircularProgressIndicator
2020
import androidx.compose.runtime.Composable
21+
import androidx.compose.ui.Modifier
2122
import io.getstream.whatsappclone.designsystem.theme.GREEN450
2223

2324
@Composable
24-
fun WhatsAppLoadingIndicator() {
25-
CircularProgressIndicator(color = GREEN450)
25+
fun WhatsAppLoadingIndicator(modifier: Modifier = Modifier) {
26+
CircularProgressIndicator(
27+
modifier = modifier,
28+
color = GREEN450
29+
)
2630
}

core/model/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ android {
2626

2727
dependencies {
2828
api(libs.stream.chat.client)
29+
api(libs.stream.video.core)
30+
2931
api(libs.retrofit.kotlin.serialization)
3032
api(libs.kotlinx.serialization.json)
3133
compileOnly(libs.compose.stable.marker)

core/uistate/src/main/kotlin/io/getstream/whatsappclone/uistate/UiState.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.skydoves.sealedx.core.Extensive
2121
import com.skydoves.sealedx.core.annotations.ExtensiveModel
2222
import com.skydoves.sealedx.core.annotations.ExtensiveSealed
2323
import io.getstream.chat.android.client.models.Channel
24+
import io.getstream.video.android.core.Call
2425

2526
/**
2627
* Generates restartable and skippable UI states based on KSP and extensive models.
@@ -29,11 +30,13 @@ import io.getstream.chat.android.client.models.Channel
2930
@ExtensiveSealed(
3031
models = [
3132
ExtensiveModel(type = Channel::class, name = "WhatsAppMessage"),
32-
ExtensiveModel(type = WhatsAppUserExtensive::class, name = "WhatsAppUser")
33+
ExtensiveModel(type = WhatsAppUserExtensive::class, name = "WhatsAppUser"),
34+
ExtensiveModel(type = Call::class, name = "WhatsAppVideo")
3335
]
3436
)
3537
@Immutable
3638
sealed interface UiState {
39+
3740
data class Success(val data: Extensive) : UiState
3841
data object Loading : UiState
3942
data object Error : UiState

features/video/src/main/kotlin/io/getstream/whatsappclone/video/WhatsAppVideoCall.kt

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,54 +16,70 @@
1616

1717
package io.getstream.whatsappclone.video
1818

19-
import android.widget.Toast
19+
import androidx.compose.foundation.layout.Box
20+
import androidx.compose.foundation.layout.fillMaxSize
21+
import androidx.compose.material.Text
22+
import androidx.compose.material3.MaterialTheme
2023
import androidx.compose.runtime.Composable
2124
import androidx.compose.runtime.LaunchedEffect
2225
import androidx.compose.runtime.getValue
23-
import androidx.compose.runtime.mutableStateOf
24-
import androidx.compose.runtime.remember
25-
import androidx.compose.runtime.setValue
26-
import androidx.compose.ui.platform.LocalContext
26+
import androidx.compose.ui.Alignment
27+
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.unit.sp
29+
import androidx.hilt.navigation.compose.hiltViewModel
30+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
31+
import io.getstream.video.android.compose.theme.VideoTheme
32+
import io.getstream.video.android.compose.ui.components.call.activecall.CallContent
2733
import io.getstream.video.android.core.Call
28-
import io.getstream.video.android.core.GEO
29-
import io.getstream.video.android.core.StreamVideoBuilder
30-
import io.getstream.video.android.model.User
34+
import io.getstream.whatsappclone.designsystem.component.WhatsAppLoadingIndicator
35+
import io.getstream.whatsappclone.uistate.WhatsAppVideoUiState
3136

3237
@Composable
33-
fun WhatsAppVideoCall(id: String) {
34-
val context = LocalContext.current
38+
fun WhatsAppVideoCall(
39+
id: String,
40+
viewModel: WhatsAppVideoCallViewModel = hiltViewModel()
41+
) {
42+
val uiState by viewModel.videoUiSate.collectAsStateWithLifecycle()
3543

36-
var call: Call? by remember { mutableStateOf(null) }
44+
LaunchedEffect(key1 = id) {
45+
viewModel.joinCall(type = "default", id = id)
46+
}
3747

38-
LaunchedEffect(key1 = Unit) {
39-
val userToken = ""
40-
val userId = "Ben_Skywalker"
41-
val callId = "dE8AsD5Qxqrt"
48+
when (uiState) {
49+
is WhatsAppVideoUiState.Success ->
50+
WhatsAppVideoCallContent(call = (uiState as WhatsAppVideoUiState.Success).data)
4251

43-
// step1 - create a user.
44-
val user = User(
45-
id = userId, // any string
46-
name = "Tutorial", // name and image are used in the UI
47-
role = "admin"
48-
)
52+
is WhatsAppVideoUiState.Error -> WhatsAppVideoCallError()
4953

50-
// step2 - initialize StreamVideo. For a production app we recommend adding the client to your Application class or di module.
51-
val client = StreamVideoBuilder(
52-
context = context,
53-
apiKey = BuildConfig.STREAM_API_KEY,
54-
geo = GEO.GlobalEdgeNetwork,
55-
user = user,
56-
token = userToken,
57-
ensureSingleInstance = false
58-
).build()
54+
else -> WhatsAppVideoLoading()
55+
}
56+
}
5957

60-
// step3 - join a call, which type is `default` and id is `123`.
61-
call = client.call("livestream", callId)
58+
@Composable
59+
fun WhatsAppVideoCallContent(
60+
call: Call
61+
) {
62+
VideoTheme {
63+
CallContent(call = call)
64+
}
65+
}
6266

63-
// join the cal
64-
val result = call?.join()
65-
result?.onError {
66-
Toast.makeText(context, "uh oh $it", Toast.LENGTH_SHORT).show()
67-
}
67+
@Composable
68+
fun WhatsAppVideoCallError() {
69+
Box(modifier = Modifier.fillMaxSize()) {
70+
Text(
71+
modifier = Modifier.align(Alignment.Center),
72+
text = "Something went wrong; failed to join a call",
73+
fontSize = 14.sp,
74+
style = MaterialTheme.typography.bodyMedium,
75+
color = MaterialTheme.colorScheme.onTertiary
76+
)
77+
}
78+
}
79+
80+
@Composable
81+
fun WhatsAppVideoLoading() {
82+
Box(modifier = Modifier.fillMaxSize()) {
83+
WhatsAppLoadingIndicator(modifier = Modifier.align(Alignment.Center))
6884
}
6985
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2023 Stream.IO, Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.whatsappclone.video
18+
19+
import android.util.Log
20+
import androidx.lifecycle.ViewModel
21+
import androidx.lifecycle.viewModelScope
22+
import dagger.hilt.android.lifecycle.HiltViewModel
23+
import io.getstream.video.android.core.StreamVideo
24+
import io.getstream.whatsappclone.uistate.WhatsAppVideoUiState
25+
import javax.inject.Inject
26+
import kotlinx.coroutines.flow.MutableStateFlow
27+
import kotlinx.coroutines.flow.StateFlow
28+
import kotlinx.coroutines.launch
29+
30+
@HiltViewModel
31+
class WhatsAppVideoCallViewModel @Inject constructor() : ViewModel() {
32+
33+
private val videoMutableUiState =
34+
MutableStateFlow<WhatsAppVideoUiState>(WhatsAppVideoUiState.Loading)
35+
val videoUiSate: StateFlow<WhatsAppVideoUiState> = videoMutableUiState
36+
37+
fun joinCall(type: String, id: String) {
38+
viewModelScope.launch {
39+
val streamVideo = StreamVideo.instance()
40+
val activeCall = streamVideo.state.activeCall.value
41+
val call = if (activeCall != null) {
42+
if (activeCall.id != id) {
43+
Log.w("CallActivity", "A call with id: $id existed. Leaving.")
44+
// If the call id is different leave the previous call
45+
activeCall.leave()
46+
// Return a new call
47+
streamVideo.call(type = type, id = id)
48+
} else {
49+
// Call ID is the same, use the active call
50+
activeCall
51+
}
52+
} else {
53+
// There is no active call, create new call
54+
streamVideo.call(type = type, id = id)
55+
}
56+
val result = call.join(create = true)
57+
result.onSuccess {
58+
videoMutableUiState.value = WhatsAppVideoUiState.Success(call)
59+
}.onError {
60+
videoMutableUiState.value = WhatsAppVideoUiState.Error
61+
}
62+
}
63+
}
64+
}

features/video/src/main/kotlin/io/getstream/whatsappclone/video/initializer/StreamVideoInitializer.kt

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,47 +17,30 @@
1717
package io.getstream.whatsappclone.video.initializer
1818

1919
import android.content.Context
20+
import android.util.Base64
2021
import androidx.startup.Initializer
2122
import io.getstream.video.android.core.StreamVideoBuilder
2223
import io.getstream.video.android.model.User
23-
import io.getstream.whatsappclone.network.Dispatcher
24-
import io.getstream.whatsappclone.network.WhatsAppDispatchers
25-
import io.getstream.whatsappclone.network.service.StreamVideoTokenService
2624
import io.getstream.whatsappclone.video.BuildConfig
27-
import javax.inject.Inject
28-
import kotlinx.coroutines.CoroutineDispatcher
25+
import java.nio.charset.StandardCharsets
2926
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.Dispatchers
3028
import kotlinx.coroutines.launch
3129

3230
class StreamVideoInitializer : Initializer<Unit> {
3331

34-
@Inject
35-
lateinit var streamVideoTokenService: StreamVideoTokenService
36-
37-
@Inject
38-
@Dispatcher(WhatsAppDispatchers.IO)
39-
lateinit var dispatcher: CoroutineDispatcher
40-
41-
private val coroutineScope = CoroutineScope(dispatcher)
42-
4332
override fun create(context: Context) {
44-
StreamVideoEntryPoint.resolve(context)
45-
46-
val userId = "stream_user"
33+
val userId = "stream"
34+
val coroutineScope = CoroutineScope(Dispatchers.IO)
4735
coroutineScope.launch {
48-
val token = streamVideoTokenService.fetchToken(
49-
userId = userId,
50-
apiKey = BuildConfig.STREAM_API_KEY
51-
).getOrThrow().token
52-
5336
// initialize Stream Video SDK
5437
StreamVideoBuilder(
5538
context = context,
5639
apiKey = BuildConfig.STREAM_API_KEY,
57-
token = token,
40+
token = devToken(userId),
5841
user = User(
5942
id = userId,
60-
name = "Stream User",
43+
name = "stream",
6144
image = "http://placekitten.com/200/300",
6245
role = "admin"
6346
)
@@ -67,3 +50,15 @@ class StreamVideoInitializer : Initializer<Unit> {
6750

6851
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
6952
}
53+
54+
fun devToken(userId: String): String {
55+
require(userId.isNotEmpty()) { "User id must not be empty" }
56+
val header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" // {"alg": "HS256", "typ": "JWT"}
57+
val devSignature = "devtoken"
58+
val payload: String =
59+
Base64.encodeToString(
60+
"{\"user_id\":\"$userId\"}".toByteArray(StandardCharsets.UTF_8),
61+
Base64.NO_WRAP
62+
)
63+
return "$header.$payload.$devSignature"
64+
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ turbine = "1.0.0"
4646
[libraries]
4747
stream-chat-client = { group = "io.getstream", name = "stream-chat-android-client", version.ref = "streamChatSDK" }
4848
stream-chat-compose = { group = "io.getstream", name = "stream-chat-android-compose", version.ref = "streamChatSDK" }
49+
stream-video-core = { group = "io.getstream", name = "stream-video-android-core", version.ref = "streamVideoSDK" }
4950
stream-video-compose = { group = "io.getstream", name = "stream-video-android-compose", version.ref = "streamVideoSDK" }
5051
stream-log = { group = "io.getstream", name = "stream-log-android", version.ref = "streamLog" }
5152
sealedx-core = { group = "com.github.skydoves", name = "sealedx-core", version.ref = "sealedx" }

0 commit comments

Comments
 (0)