-
Notifications
You must be signed in to change notification settings - Fork 58
Expand file tree
/
Copy pathCallActivity.kt
More file actions
338 lines (312 loc) · 13.1 KB
/
Copy pathCallActivity.kt
File metadata and controls
338 lines (312 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
/*
* Copyright (c) 2014-2026 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 android.os.Bundle
import android.os.PersistableBundle
import android.util.Log
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
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.core.CallLeaveReason
import io.getstream.video.android.core.MemberState
import io.getstream.video.android.core.RingingState
import io.getstream.video.android.core.StreamVideo
import io.getstream.video.android.core.call.state.CallAction
import io.getstream.video.android.core.e2ee.E2EEEventType
import io.getstream.video.android.core.e2ee.StreamEncryptionManager
import io.getstream.video.android.datastore.delegate.StreamUserDataStore
import io.getstream.video.android.ui.call.CallScreen
import io.getstream.video.android.ui.call.DemoComponentFactory
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.ui.lobby.deriveE2EEKey
import io.getstream.video.android.util.DemoE2eeKeys
import io.getstream.video.android.util.FullScreenCircleProgressBar
import io.getstream.video.android.util.StreamVideoInitHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.ConcurrentHashMap
@OptIn(StreamCallActivityDelicateApi::class)
class CallActivity : ComposeStreamCallActivity() {
companion object {
var USE_CALL_JOIN_INTERCEPTOR = false
/** Shared E2EE passphrase, forwarded from a deeplink or scanned QR code. */
const val EXTRA_E2EE_PASSPHRASE = "e2ee_passphrase"
/**
* The key index every participant agrees on. A frame carries the index it was encrypted
* with, so a receiver looking elsewhere fails every decrypt. Kept in step with the lobby
* and with the web demo, which both use slot 0.
*/
private const val E2EE_KEY_INDEX = 0
private const val E2EE_TAG = "CallActivityE2EE"
}
override val uiDelegate: StreamActivityUiDelegate<StreamCallActivity> = StreamDemoUiDelegate()
var observeCallReadyToJoinJob: Job? = null
var observeRingingJob: Job? = null
private val previousRingingStates = ConcurrentHashMap.newKeySet<RingingState>()
override val callJoinInterceptor = DemoCallJoinInterceptor(previousRingingStates)
private var e2eeManager: StreamEncryptionManager? = null
private var e2eeCid: String? = null
/**
* This code is required to pass the UI-tests (as it hardcodes the configuration)
* Later, improve the UI-tests
*/
override fun loadConfigFromIntent(intent: Intent?): StreamCallActivityConfiguration {
return super.loadConfigFromIntent(intent)
.copy(closeScreenOnCallEnded = false, canSkipPermissionRationale = false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
observeRingingState()
}
private fun observeRingingState() {
previousRingingStates.clear()
observeRingingJob?.cancel()
observeRingingJob = CoroutineScope(Dispatchers.Default).launch {
StreamVideo.instanceState
.flatMapLatest { instance ->
instance?.state?.ringingCall ?: flowOf(null)
}.filterNotNull()
.collectLatest { call ->
call.state.ringingState.collectLatest {
previousRingingStates.add(it)
}
}
}
}
@StreamCallActivityDelicateApi
override fun onPreCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
runBlocking {
if (!StreamVideo.isInstalled) {
runBlocking { StreamVideoInitHelper.reloadSdk(StreamUserDataStore.instance()) }
}
}
super.onPreCreate(savedInstanceState, persistentState)
}
/**
* Applies the shared key carried by the invite link before the call is joined.
* [Call.setE2EEManager] is rejected once a session exists, so this is the last point at which a
* scanned link can still become an encrypted call.
*/
@StreamCallActivityDelicateApi
override fun join(
call: Call,
onSuccess: (suspend (Call) -> Unit)?,
onError: (suspend (Exception) -> Unit)?,
) {
intent.getStringExtra(EXTRA_E2EE_PASSPHRASE)
?.takeIf { it.isNotBlank() }
?.let { enableE2EE(call, it) }
// Lobby already attached a manager and then died (CLEAR_TASK). Adopt it so leave/finish
// can dispose it — Call.leave() only detaches, and the 1 Hz PERF_REPORT timer keeps
// firing until dispose().
if (e2eeManager == null) {
DemoE2eeKeys.manager(call.cid)?.let { existing ->
e2eeManager = existing
e2eeCid = call.cid
}
}
super.join(call, onSuccess, onError)
}
@StreamCallActivityDelicateApi
override fun leave(
call: Call,
callLeaveReason: CallLeaveReason,
onSuccess: (suspend (Call) -> Unit)?,
onError: (suspend (Exception) -> Unit)?,
) {
super.leave(
call,
callLeaveReason,
onSuccess = { left ->
releaseE2EE()
onSuccess?.invoke(left)
},
onError,
)
}
private fun releaseE2EE() {
e2eeManager?.dispose()
e2eeManager = null
e2eeCid?.let { DemoE2eeKeys.forget(it) }
e2eeCid = null
}
private fun enableE2EE(call: Call, passphrase: String) {
// join() cannot suspend, but the derivation is 100k PBKDF2 iterations - keep it off main.
val key = runCatching {
runBlocking(Dispatchers.Default) { deriveE2EEKey(passphrase) }
}.getOrElse {
Log.e(E2EE_TAG, "Could not derive the E2EE key from the link", it)
return
}
// EncryptionManager's JNI is registered by libjingle_peerconnection_so's JNI_OnLoad. The
// lobby gets that for free from its camera preview, but a scanned link joins without ever
// building a factory first, so without this create() fails with an UnsatisfiedLinkError.
runCatching { System.loadLibrary("jingle_peerconnection_so") }.onFailure {
Log.e(E2EE_TAG, "Could not load the WebRTC native library", it)
return
}
val manager = StreamEncryptionManager.create(call.user.id).getOrElse {
Log.e(E2EE_TAG, "Could not create the E2EE manager", it)
return
}
manager.setEventListener { event ->
val message = "Native E2EE event: $event"
when (event.type) {
E2EEEventType.DECRYPTION_RESUMED -> Log.i(E2EE_TAG, message)
E2EEEventType.KEY_STATE,
E2EEEventType.PERF_REPORT,
-> Log.d(E2EE_TAG, message)
E2EEEventType.DECRYPTION_FAILED,
E2EEEventType.DECRYPTION_STALLED,
E2EEEventType.ENCRYPTION_FAILED,
E2EEEventType.MISSING_KEY,
E2EEEventType.UNENCRYPTED_FRAME,
E2EEEventType.UNSUPPORTED_VERSION,
E2EEEventType.UNKNOWN,
-> Log.w(E2EE_TAG, message)
}
}
manager.enablePerformanceReporting(true)
manager.setSharedKey(E2EE_KEY_INDEX, key)
val attached = call.setE2EEManager(manager)
if (attached.isFailure) {
Log.e(E2EE_TAG, "Could not attach the E2EE manager", attached.exceptionOrNull())
manager.dispose()
return
}
e2eeManager?.dispose()
e2eeManager = manager
// Kept so the in-call share sheet can put the passphrase back on the invite link.
e2eeCid = call.cid
DemoE2eeKeys.remember(call.cid, passphrase, manager)
}
private class StreamDemoUiDelegate : StreamCallActivityComposeDelegate() {
override val componentFactory = DemoComponentFactory
@Composable
override fun StreamCallActivity.LoadingContent(call: Call) {
// Use as loading screen.. so the layout is shown.
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()
}
}
}
}
}
@Composable
override fun StreamCallActivity.OutgoingCallContent(
modifier: Modifier,
call: Call,
isVideoType: Boolean,
isShowingHeader: Boolean,
headerContent: @Composable (ColumnScope.() -> Unit)?,
detailsContent: @Composable (ColumnScope.(List<MemberState>, Dp) -> Unit)?,
controlsContent: @Composable (BoxScope.() -> Unit)?,
onBackPressed: () -> Unit,
onCallAction: (CallAction) -> Unit,
) {
DemoOutgoingCallContent(
call = call,
isVideoType = isVideoType,
modifier = modifier,
isShowingHeader = isShowingHeader,
headerContent = headerContent,
detailsContent = detailsContent,
onBackPressed = onBackPressed,
onCallAction = onCallAction,
)
}
@Composable
override fun StreamCallActivity.AudioCallContent(call: Call) {
DemoAudioCallContent(
call,
{ onCallAction(call, it) },
{ onBackPressed(call) },
)
}
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)
safeFinish()
}
}
}
override fun finish() {
super.finish()
observeCallReadyToJoinJob?.cancel()
observeRingingJob?.cancel()
previousRingingStates.clear()
releaseE2EE()
}
}