From 461c650642150ad0e8596d956d2c7e31c74af0a1 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:22:56 +0300 Subject: [PATCH 01/22] Add SecurityException handling to NetworkStateProvider (#1146) Co-authored-by: Aleksandar Apostolov --- .../internal/network/NetworkStateProvider.kt | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/network/NetworkStateProvider.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/network/NetworkStateProvider.kt index 2539e452d8..a216f7b3c2 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/network/NetworkStateProvider.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/internal/network/NetworkStateProvider.kt @@ -20,6 +20,7 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import io.getstream.log.StreamLog import java.util.concurrent.atomic.AtomicBoolean /** @@ -96,14 +97,24 @@ public class NetworkStateProvider(private val connectivityManager: ConnectivityM synchronized(lock) { listeners = listeners + listener if (isRegistered.compareAndSet(false, true)) { - connectivityManager.registerNetworkCallback( - NetworkRequest.Builder().build(), - callback, - ) + safelyRegisterNetworkCallback(NetworkRequest.Builder().build(), callback) } } } + /** + * Calls [ConnectivityManager.registerNetworkCallback] and catches potential [SecurityException]. + * This is a known [bug](https://android-review.googlesource.com/c/platform/frameworks/base/+/1758029) on Android 11. + */ + private fun safelyRegisterNetworkCallback( + networkRequest: NetworkRequest, + callback: ConnectivityManager.NetworkCallback, + ) { + connectivityManager.callWithSecurityExceptionHandling { + registerNetworkCallback(networkRequest, callback) + } + } + /** * Removes a listener for network state changes. * @@ -113,12 +124,24 @@ public class NetworkStateProvider(private val connectivityManager: ConnectivityM synchronized(lock) { listeners = (listeners - listener).also { if (it.isEmpty() && isRegistered.compareAndSet(true, false)) { - connectivityManager.unregisterNetworkCallback(callback) + safelyUnregisterNetworkCallback(callback) } } } } + /** + * Calls [ConnectivityManager.unregisterNetworkCallback] and catches potential [SecurityException]. + * This is a known [bug](https://android-review.googlesource.com/c/platform/frameworks/base/+/1758029) on Android 11. + */ + private fun safelyUnregisterNetworkCallback(callback: ConnectivityManager.NetworkCallback) { + connectivityManager.callWithSecurityExceptionHandling { + unregisterNetworkCallback( + callback, + ) + } + } + /** * Listener which is used to listen and react to network state changes. */ @@ -128,3 +151,13 @@ public class NetworkStateProvider(private val connectivityManager: ConnectivityM public fun onDisconnected() } } + +private fun ConnectivityManager.callWithSecurityExceptionHandling(method: ConnectivityManager.() -> Unit) { + try { + method() + } catch (e: SecurityException) { + StreamLog.e("ConnectivityManager", e) { + "SecurityException occurred. This is a known bug on Android 11. We log and prevent the app from crashing. Cause: ${e.message}" + } + } +} From d03e00df186992376c0c530030ba6bde2e89ccdc Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:18:34 +0300 Subject: [PATCH 02/22] [PBE-5431] Meet Android 14 foreground service requirements (#1152) * Adapt startForeground to Android 14 requirements in CallService * Create extension function * Use also in StreamScreenShareService * Rename function and add inline docs * Refactor Android version conditions --- .../internal/service/CallService.kt | 41 ++++++------------ .../screenshare/StreamScreenShareService.kt | 4 +- .../video/android/core/utils/AndroidUtils.kt | 43 +++++++++++++++++++ 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index ef46836ed6..0e541625fc 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -22,13 +22,10 @@ import android.app.Service import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.pm.ServiceInfo import android.media.MediaPlayer -import android.os.Build import android.os.IBinder import androidx.annotation.RawRes import androidx.core.app.NotificationManagerCompat -import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import io.getstream.log.taggedLogger import io.getstream.video.android.core.R @@ -39,6 +36,7 @@ import io.getstream.video.android.core.notifications.NotificationHandler.Compani import io.getstream.video.android.core.notifications.NotificationHandler.Companion.INTENT_EXTRA_CALL_CID import io.getstream.video.android.core.notifications.NotificationHandler.Companion.INTENT_EXTRA_CALL_DISPLAY_NAME import io.getstream.video.android.core.notifications.internal.receivers.ToggleCameraBroadcastReceiver +import io.getstream.video.android.core.utils.startForegroundWithServiceType import io.getstream.video.android.model.StreamCallId import io.getstream.video.android.model.streamCallDisplayName import io.getstream.video.android.model.streamCallId @@ -182,6 +180,7 @@ internal class CallService : Service() { maybePromoteToForegroundService( videoClient = streamVideo, notificationId = intentCallId.hashCode(), + trigger, ) val type = intentCallId.type @@ -241,27 +240,12 @@ internal class CallService : Service() { if (notification != null) { if (trigger == TRIGGER_INCOMING_CALL) { showIncomingCall( - notification = notification, notificationId = notificationData.second, + notification = notification, ) } else { callId = intentCallId - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - val foregroundServiceType = when (trigger) { - TRIGGER_ONGOING_CALL -> ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL - TRIGGER_OUTGOING_CALL -> ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE - else -> ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE - } - ServiceCompat.startForeground( - this@CallService, - intentCallId.hashCode(), - notification, - foregroundServiceType, - ) - } else { - startForeground(intentCallId.hashCode(), notification) - } + startForegroundWithServiceType(intentCallId.hashCode(), notification, trigger) } true } else { @@ -303,7 +287,7 @@ internal class CallService : Service() { } } - private fun maybePromoteToForegroundService(videoClient: StreamVideoImpl, notificationId: Int) { + private fun maybePromoteToForegroundService(videoClient: StreamVideoImpl, notificationId: Int, trigger: String) { val hasActiveCall = videoClient.state.activeCall.value != null val not = if (hasActiveCall) " not" else "" @@ -312,19 +296,20 @@ internal class CallService : Service() { } if (!hasActiveCall) { - startForeground(notificationId, videoClient.getSettingUpCallNotification()) + videoClient.getSettingUpCallNotification()?.let { + startForegroundWithServiceType(notificationId, it, trigger) + } } } @SuppressLint("MissingPermission") - private fun showIncomingCall(notification: Notification, notificationId: Int) { + private fun showIncomingCall(notificationId: Int, notification: Notification) { if (callId == null) { // If there isn't another call in progress (callId is set in onStartCommand()) - startForeground( - notificationId, - notification, - ) // The service was started with startForegroundService() (from companion object), so we need to call startForeground(). + // The service was started with startForegroundService() (from companion object), so we need to call startForeground(). + startForegroundWithServiceType(notificationId, notification, TRIGGER_INCOMING_CALL) } else { - NotificationManagerCompat // Else, we show a simple notification (the service was already started as a foreground service). + // Else, we show a simple notification (the service was already started as a foreground service). + NotificationManagerCompat .from(this) .notify(notificationId, notification) } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt index 76c377de86..2b63db15a2 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt @@ -28,6 +28,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import io.getstream.video.android.core.R import io.getstream.video.android.core.notifications.internal.receivers.StopScreenshareBroadcastReceiver +import io.getstream.video.android.core.utils.startForegroundWithServiceType /** * Screen-sharing in Android requires a ForegroundService (with type foregroundServiceType set to "mediaProjection"). @@ -102,7 +103,7 @@ internal class StreamScreenShareService : Service() { ) } - startForeground(NOTIFICATION_ID, builder.build()) + startForegroundWithServiceType(NOTIFICATION_ID, builder.build(), TRIGGER_SHARE_SCREEN) return super.onStartCommand(intent, flags, startId) } @@ -111,6 +112,7 @@ internal class StreamScreenShareService : Service() { internal const val EXTRA_CALL_ID = "EXTRA_CALL_ID" internal const val BROADCAST_CANCEL_ACTION = "io.getstream.video.android.action.CANCEL_SCREEN_SHARE" internal const val INTENT_EXTRA_CALL_ID = "io.getstream.video.android.intent-extra.call_cid" + internal const val TRIGGER_SHARE_SCREEN = "share_screen" fun createIntent(context: Context, callId: String) = Intent(context, StreamScreenShareService::class.java).apply { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt index 878b9a1276..7139a1a04d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt @@ -19,17 +19,25 @@ package io.getstream.video.android.core.utils import android.app.Activity +import android.app.Notification import android.app.NotificationManager +import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.ServiceInfo import android.os.Build import android.os.Vibrator import android.os.VibratorManager import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat +import androidx.core.app.ServiceCompat import io.getstream.log.StreamLog +import io.getstream.video.android.core.notifications.internal.service.CallService.Companion.TRIGGER_INCOMING_CALL +import io.getstream.video.android.core.notifications.internal.service.CallService.Companion.TRIGGER_ONGOING_CALL +import io.getstream.video.android.core.notifications.internal.service.CallService.Companion.TRIGGER_OUTGOING_CALL +import io.getstream.video.android.core.screenshare.StreamScreenShareService.Companion.TRIGGER_SHARE_SCREEN import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking @@ -162,3 +170,38 @@ inline fun safeCall(default: T, block: () -> T): T { default } } + +/** + * Start a foreground service with a service type to meet requirements introduced in Android 14. + * + * @param notificationId The notification ID + * @param notification The notification to show + * @param trigger The trigger that started the service: [TRIGGER_ONGOING_CALL], [TRIGGER_OUTGOING_CALL], [TRIGGER_INCOMING_CALL], [TRIGGER_SHARE_SCREEN] + */ +internal fun Service.startForegroundWithServiceType( + notificationId: Int, + notification: Notification, + trigger: String, +) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + startForeground(notificationId, notification) + } else { + val beforeOrAfterAndroid14Type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE + } else { + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + } + + ServiceCompat.startForeground( + this, + notificationId, + notification, + when (trigger) { + TRIGGER_ONGOING_CALL -> ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + TRIGGER_OUTGOING_CALL, TRIGGER_INCOMING_CALL -> beforeOrAfterAndroid14Type + TRIGGER_SHARE_SCREEN -> ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION + else -> beforeOrAfterAndroid14Type + }, + ) + } +} From ba623a882aa1441623666c5a71424cba7c0a5edb Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 19 Aug 2024 12:22:45 +0200 Subject: [PATCH 03/22] Do not leave the call when `StreamCallActivity` goes into background (#1154) * Fix an issue where StreamCallActivity would leave the call when going into background * Do not leave call in onStop --- .../getstream/video/android/ui/common/StreamCallActivity.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt index c5f3a27f78..b6dda4d512 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt @@ -383,11 +383,8 @@ public abstract class StreamCallActivity : ComponentActivity() { * @param call the call */ public open fun onStop(call: Call) { + // Extension point only. logger.d { "Default activity - stopped (call -> $call)" } - if (isVideoCall(call) && !isInPictureInPictureMode) { - logger.d { "Default activity - stopped: No PiP detected, will leave call. (call -> $call)" } - leave(call) // Already finishing - } } /** From 7e9734cf7858bfc80c11c7063b40f9d4795be4af Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:27:22 +0300 Subject: [PATCH 04/22] Use LazyThreadSafetyMode.PUBLICATION thread-safety mode for camera, microphone, speaker, screenShare Call properties. (#1156) --- .../main/kotlin/io/getstream/video/android/core/Call.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 526ee6a89a..71f8b4c24c 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 @@ -133,10 +133,10 @@ public class Call( private val network by lazy { clientImpl.connectionModule.networkStateProvider } /** Camera gives you access to the local camera */ - val camera by lazy { mediaManager.camera } - val microphone by lazy { mediaManager.microphone } - val speaker by lazy { mediaManager.speaker } - val screenShare by lazy { mediaManager.screenShare } + val camera by lazy(LazyThreadSafetyMode.PUBLICATION) { mediaManager.camera } + val microphone by lazy(LazyThreadSafetyMode.PUBLICATION) { mediaManager.microphone } + val speaker by lazy(LazyThreadSafetyMode.PUBLICATION) { mediaManager.speaker } + val screenShare by lazy(LazyThreadSafetyMode.PUBLICATION) { mediaManager.screenShare } /** The cid is type:id */ val cid = "$type:$id" From 9e0af19c613387d0a0b89595ecae60830e48c421 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 22 Aug 2024 17:49:54 +0200 Subject: [PATCH 05/22] [PBE-5625] Stop the call service if its running when cleaning up the video client (#1155) * Stop the call service if its running when cleaning up the video client * Add safe call just in case the stopService method fails --- .../io/getstream/video/android/core/StreamVideoImpl.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt index b670583550..341c909d0b 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt @@ -45,6 +45,7 @@ import io.getstream.video.android.core.model.UpdateUserPermissionsData import io.getstream.video.android.core.model.toRequest import io.getstream.video.android.core.notifications.NotificationHandler import io.getstream.video.android.core.notifications.internal.StreamNotificationManager +import io.getstream.video.android.core.notifications.internal.service.CallService import io.getstream.video.android.core.permission.android.DefaultStreamPermissionCheck import io.getstream.video.android.core.permission.android.StreamPermissionCheck import io.getstream.video.android.core.socket.ErrorResponse @@ -205,6 +206,13 @@ internal class StreamVideoImpl internal constructor( // call cleanup on the active call val activeCall = state.activeCall.value activeCall?.leave() + // Stop the call service if it was running + if (runForegroundService) { + safeCall { + val serviceIntent = CallService.buildStopIntent(context) + context.stopService(serviceIntent) + } + } } /** From c61ad58e55178413a6f57db9cee38fba9593c9cd Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:53:20 +0300 Subject: [PATCH 06/22] Enable/disable tracks directly in livestreamFlow (#1167) --- .../getstream/video/android/core/CallState.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 f911ef77a6..c7b3695109 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 @@ -283,6 +283,26 @@ public class CallState( // TODO: could optimize performance by subscribing only to relevant events call.subscribe { + if (it is TrackPublishedEvent) { + val participant = getOrCreateParticipant(it.sessionId, it.userId) + + if (it.trackType == TrackType.TRACK_TYPE_VIDEO) { + participant._videoEnabled.value = true + } else if (it.trackType == TrackType.TRACK_TYPE_AUDIO) { + participant._audioEnabled.value = true + } + } + + if (it is TrackUnpublishedEvent) { + val participant = getOrCreateParticipant(it.sessionId, it.userId) + + if (it.trackType == TrackType.TRACK_TYPE_VIDEO) { + participant._videoEnabled.value = false + } else if (it.trackType == TrackType.TRACK_TYPE_AUDIO) { + participant._audioEnabled.value = false + } + } + emitLivestreamVideo() } From a77a13f5db708ae4cea47bf2812d6dc6653f7a36 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 26 Aug 2024 16:09:31 +0200 Subject: [PATCH 07/22] Don't unlock the mutex when its not locked (#1165) --- .../video/android/core/call/RtcSession.kt | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index 210b903166..ddec0150ec 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -907,9 +907,10 @@ public class RtcSession internal constructor( return@synchronized } - val enabledRids = event.changePublishQuality.video_senders.firstOrNull()?.layers?.associate { - it.name to it.active - } + val enabledRids = + event.changePublishQuality.video_senders.firstOrNull()?.layers?.associate { + it.name to it.active + } dynascaleLogger.i { "enabled rids: $enabledRids}" } val params = sender.parameters val updatedEncodings: MutableList = mutableListOf() @@ -1069,9 +1070,10 @@ public class RtcSession internal constructor( fun handleEvent(event: VideoEvent) { logger.i { "[rtc handleEvent] #sfu; event: $event" } if (event is JoinCallResponseEvent) { - logger.i { "[rtc handleEvent] unlocking joinEventReceivedMutex" } - - joinEventReceivedMutex.unlock() + if (joinEventReceivedMutex.isLocked) { + logger.i { "[rtc handleEvent] unlocking joinEventReceivedMutex" } + joinEventReceivedMutex.unlock() + } } if (event is SfuDataEvent) { coroutineScope.launch { @@ -1410,7 +1412,13 @@ public class RtcSession internal constructor( track_id = track.id(), track_type = trackType, layers = layers, - mid = transceiver.mid ?: extractMid(sdp, track, screenShareTrack, trackType, transceivers), + mid = transceiver.mid ?: extractMid( + sdp, + track, + screenShareTrack, + trackType, + transceivers, + ), ) } return tracks @@ -1429,6 +1437,7 @@ public class RtcSession internal constructor( TrackType.TRACK_TYPE_VIDEO } } + else -> TrackType.TRACK_TYPE_UNSPECIFIED } @@ -1475,7 +1484,10 @@ public class RtcSession internal constructor( return media.mid.toString() } - private fun createVideoLayers(transceiver: RtpTransceiver, captureResolution: CaptureFormat): List { + private fun createVideoLayers( + transceiver: RtpTransceiver, + captureResolution: CaptureFormat, + ): List { // we tell the Sfu which resolutions we're sending return transceiver.sender.parameters.encodings.map { val scaleBy = it.scaleResolutionDownBy ?: 1.0 @@ -1715,7 +1727,13 @@ public class RtcSession internal constructor( } } - suspend fun switchSfu(sfuName: String, sfuUrl: String, sfuToken: String, remoteIceServers: List, failedToSwitch: () -> Unit) { + suspend fun switchSfu( + sfuName: String, + sfuUrl: String, + sfuToken: String, + remoteIceServers: List, + failedToSwitch: () -> Unit, + ) { logger.i { "[switchSfu] from ${this.sfuUrl} to $sfuUrl" } val timer = clientImpl.debugInfo.trackTime("call.switchSfu") From 62e41d15c7fd563e1c59ea52b214306256a5eed9 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Tue, 27 Aug 2024 15:04:17 +0200 Subject: [PATCH 08/22] [PBE-5674] Add multiple services with different types for livestreaming calls (#1164) * Add multiple services with different types for livestreaming calls * Remove duplicate line in CallService * Rename to `Viewer` instead of `Member` * Add incomplete sentence in manifest --------- Co-authored-by: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> --- .../api/stream-video-android-core.api | 36 ++++- .../src/main/AndroidManifest.xml | 32 ++++- .../video/android/core/ClientState.kt | 10 +- .../video/android/core/StreamVideoBuilder.kt | 9 +- .../video/android/core/StreamVideoImpl.kt | 8 +- .../internal/service/CallService.kt | 73 ++++++++-- .../internal/service/CallServiceConfig.kt | 84 ++++++++++++ .../internal/service/LivestreamCallService.kt | 37 +++++ .../screenshare/StreamScreenShareService.kt | 11 +- .../video/android/core/utils/AndroidUtils.kt | 5 +- .../internal/service/CallServiceConfigTest.kt | 127 ++++++++++++++++++ .../src/main/AndroidManifest.xml | 5 + .../android/tutorial/livestream/LiveGuest.kt | 11 +- .../android/tutorial/livestream/LiveHost.kt | 2 + 14 files changed, 416 insertions(+), 34 deletions(-) create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfig.kt create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/LivestreamCallService.kt create mode 100644 stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfigTest.kt diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 7a0bb0a553..1d6bde92f1 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -817,12 +817,13 @@ public final class io/getstream/video/android/core/StreamVideoBuilder { public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZ)V public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;)V public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;Z)V - public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;)V - public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;)V - public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;Z)V - public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;)V - public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;I)V - public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLjava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;Z)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;I)V + public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun build ()Lio/getstream/video/android/core/StreamVideo; } @@ -4218,6 +4219,29 @@ public final class io/getstream/video/android/core/notifications/internal/receiv public fun onReceive (Landroid/content/Context;Landroid/content/Intent;)V } +public final class io/getstream/video/android/core/notifications/internal/service/CallServiceConfig { + public fun ()V + public fun (ZILjava/util/Map;)V + public synthetic fun (ZILjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()I + public final fun component3 ()Ljava/util/Map; + public final fun copy (ZILjava/util/Map;)Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; + public static synthetic fun copy$default (Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;ZILjava/util/Map;ILjava/lang/Object;)Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getAudioUsage ()I + public final fun getCallServicePerType ()Ljava/util/Map; + public final fun getRunCallServiceInForeground ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/core/notifications/internal/service/CallServiceConfigKt { + public static final fun callServiceConfig ()Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; + public static final fun livestreamCallServiceConfig ()Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; + public static final fun livestreamGuestCallServiceConfig ()Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; +} + public final class io/getstream/video/android/core/permission/PermissionRequest { public fun (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Lorg/threeten/bp/OffsetDateTime;Ljava/util/List;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;)V public synthetic fun (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Lorg/threeten/bp/OffsetDateTime;Ljava/util/List;Lorg/threeten/bp/OffsetDateTime;Lorg/threeten/bp/OffsetDateTime;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/stream-video-android-core/src/main/AndroidManifest.xml b/stream-video-android-core/src/main/AndroidManifest.xml index f9e5b5fdb6..701e3aadb5 100644 --- a/stream-video-android-core/src/main/AndroidManifest.xml +++ b/stream-video-android-core/src/main/AndroidManifest.xml @@ -34,12 +34,26 @@ - + + + - - + + + - + + + + + + + + + + + + @@ -83,5 +97,15 @@ + + + + \ No newline at end of file diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt index 14d1e555eb..ca111ef206 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt @@ -154,12 +154,13 @@ class ClientState(client: StreamVideo) { * This depends on the flag in [StreamVideoBuilder] called `runForegroundServiceForCalls` */ internal fun maybeStartForegroundService(call: Call, trigger: String) { - if (clientImpl.runForegroundService) { + if (clientImpl.callServiceConfig.runCallServiceInForeground) { val context = clientImpl.context val serviceIntent = CallService.buildStartIntent( context, StreamCallId.fromCallCid(call.cid), trigger, + callServiceConfiguration = clientImpl.callServiceConfig, ) ContextCompat.startForegroundService(context, serviceIntent) } @@ -169,9 +170,12 @@ class ClientState(client: StreamVideo) { * Stop the foreground service that manages the call even when the UI is gone. */ internal fun maybeStopForegroundService() { - if (clientImpl.runForegroundService) { + if (clientImpl.callServiceConfig.runCallServiceInForeground) { val context = clientImpl.context - val serviceIntent = CallService.buildStopIntent(context) + val serviceIntent = CallService.buildStopIntent( + context, + callServiceConfiguration = clientImpl.callServiceConfig, + ) context.stopService(serviceIntent) } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt index aa1657758a..1d0b579d4b 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt @@ -28,6 +28,8 @@ import io.getstream.video.android.core.internal.module.ConnectionModule import io.getstream.video.android.core.logging.LoggingLevel import io.getstream.video.android.core.notifications.NotificationConfig import io.getstream.video.android.core.notifications.internal.StreamNotificationManager +import io.getstream.video.android.core.notifications.internal.service.CallServiceConfig +import io.getstream.video.android.core.notifications.internal.service.callServiceConfig import io.getstream.video.android.core.notifications.internal.storage.DeviceTokenStorage import io.getstream.video.android.core.permission.android.DefaultStreamPermissionCheck import io.getstream.video.android.core.permission.android.StreamPermissionCheck @@ -94,6 +96,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( private var ensureSingleInstance: Boolean = true, private val videoDomain: String = "video.stream-io-api.com", private val runForegroundServiceForCalls: Boolean = true, + private val callServiceConfig: CallServiceConfig? = null, private val localSfuAddress: String? = null, private val sounds: Sounds = Sounds(), private val crashOnMissingPermission: Boolean = false, @@ -186,7 +189,11 @@ public class StreamVideoBuilder @JvmOverloads constructor( lifecycle = lifecycle, connectionModule = connectionModule, streamNotificationManager = streamNotificationManager, - runForegroundService = runForegroundServiceForCalls, + callServiceConfig = callServiceConfig + ?: callServiceConfig().copy( + runCallServiceInForeground = runForegroundServiceForCalls, + audioUsage = audioUsage, + ), testSfuAddress = localSfuAddress, sounds = sounds, permissionCheck = permissionCheck, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt index 341c909d0b..d4b95fe578 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt @@ -46,6 +46,8 @@ import io.getstream.video.android.core.model.toRequest import io.getstream.video.android.core.notifications.NotificationHandler import io.getstream.video.android.core.notifications.internal.StreamNotificationManager import io.getstream.video.android.core.notifications.internal.service.CallService +import io.getstream.video.android.core.notifications.internal.service.CallServiceConfig +import io.getstream.video.android.core.notifications.internal.service.callServiceConfig import io.getstream.video.android.core.permission.android.DefaultStreamPermissionCheck import io.getstream.video.android.core.permission.android.StreamPermissionCheck import io.getstream.video.android.core.socket.ErrorResponse @@ -148,7 +150,7 @@ internal class StreamVideoImpl internal constructor( internal val connectionModule: ConnectionModule, internal val tokenProvider: (suspend (error: Throwable?) -> String)?, internal val streamNotificationManager: StreamNotificationManager, - internal val runForegroundService: Boolean = true, + internal val callServiceConfig: CallServiceConfig = callServiceConfig(), internal val testSfuAddress: String? = null, internal val sounds: Sounds, internal val permissionCheck: StreamPermissionCheck = DefaultStreamPermissionCheck(), @@ -207,9 +209,9 @@ internal class StreamVideoImpl internal constructor( val activeCall = state.activeCall.value activeCall?.leave() // Stop the call service if it was running - if (runForegroundService) { + if (callServiceConfig.runCallServiceInForeground) { safeCall { - val serviceIntent = CallService.buildStopIntent(context) + val serviceIntent = CallService.buildStopIntent(context, callServiceConfig) context.stopService(serviceIntent) } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index 0e541625fc..9beb88a79f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -17,16 +17,19 @@ package io.getstream.video.android.core.notifications.internal.service import android.annotation.SuppressLint +import android.app.ActivityManager import android.app.Notification import android.app.Service import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.ServiceInfo import android.media.MediaPlayer import android.os.IBinder import androidx.annotation.RawRes import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import io.getstream.log.StreamLog import io.getstream.log.taggedLogger import io.getstream.video.android.core.R import io.getstream.video.android.core.RingingState @@ -36,6 +39,7 @@ import io.getstream.video.android.core.notifications.NotificationHandler.Compani import io.getstream.video.android.core.notifications.NotificationHandler.Companion.INTENT_EXTRA_CALL_CID import io.getstream.video.android.core.notifications.NotificationHandler.Companion.INTENT_EXTRA_CALL_DISPLAY_NAME import io.getstream.video.android.core.notifications.internal.receivers.ToggleCameraBroadcastReceiver +import io.getstream.video.android.core.utils.safeCall import io.getstream.video.android.core.utils.startForegroundWithServiceType import io.getstream.video.android.model.StreamCallId import io.getstream.video.android.model.streamCallDisplayName @@ -52,8 +56,11 @@ import org.openapitools.client.models.CallRejectedEvent /** * A foreground service that is running when there is an active call. */ -internal class CallService : Service() { - private val logger by taggedLogger("CallService") +internal open class CallService : Service() { + internal open val logger by taggedLogger("CallService") + + // Service type + open val serviceType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL // Data private var callId: StreamCallId? = null @@ -72,6 +79,7 @@ internal class CallService : Service() { private var mediaPlayer: MediaPlayer? = null internal companion object { + private const val TAG = "CallServiceCompanion" const val TRIGGER_KEY = "io.getstream.video.android.core.notifications.internal.service.CallService.call_trigger" const val TRIGGER_INCOMING_CALL = "incoming_call" @@ -92,8 +100,11 @@ internal class CallService : Service() { callId: StreamCallId, trigger: String, callDisplayName: String? = null, + callServiceConfiguration: CallServiceConfig = callServiceConfig(), ): Intent { - val serviceIntent = Intent(context, CallService::class.java) + val serviceClass = resolveServiceClass(callId, callServiceConfiguration) + StreamLog.i(TAG) { "Resolved service class: $serviceClass" } + val serviceIntent = Intent(context, serviceClass) serviceIntent.putExtra(INTENT_EXTRA_CALL_CID, callId) when (trigger) { @@ -128,9 +139,22 @@ internal class CallService : Service() { * * @param context the context. */ - fun buildStopIntent(context: Context) = Intent(context, CallService::class.java) + fun buildStopIntent( + context: Context, + callServiceConfiguration: CallServiceConfig = callServiceConfig(), + ) = safeCall(Intent(context, CallService::class.java)) { + val intent = callServiceConfiguration.callServicePerType.firstNotNullOfOrNull { + val serviceClass = it.value + if (isServiceRunning(context, serviceClass)) { + Intent(context, serviceClass) + } else { + null + } + } + intent ?: Intent(context, CallService::class.java) + } - fun showIncomingCall(context: Context, callId: StreamCallId, callDisplayName: String?) { + fun showIncomingCall(context: Context, callId: StreamCallId, callDisplayName: String?, callServiceConfiguration: CallServiceConfig = callServiceConfig()) { val hasActiveCall = StreamVideo.instanceOrNull()?.state?.activeCall?.value != null if (!hasActiveCall) { @@ -141,6 +165,7 @@ internal class CallService : Service() { callId, TRIGGER_INCOMING_CALL, callDisplayName, + callServiceConfiguration, ), ) } else { @@ -150,20 +175,39 @@ internal class CallService : Service() { callId, TRIGGER_INCOMING_CALL, callDisplayName, + callServiceConfiguration, ), ) } } - fun removeIncomingCall(context: Context, callId: StreamCallId) { + fun removeIncomingCall(context: Context, callId: StreamCallId, config: CallServiceConfig = callServiceConfig()) { context.startService( buildStartIntent( context, callId, TRIGGER_REMOVE_INCOMING_CALL, + callServiceConfiguration = config, ), ) } + + private fun isServiceRunning(context: Context, serviceClass: Class<*>): Boolean = safeCall( + true, + ) { + val activityManager = context.getSystemService( + Context.ACTIVITY_SERVICE, + ) as ActivityManager + val runningServices = activityManager.getRunningServices(Int.MAX_VALUE) + for (service in runningServices) { + if (serviceClass.name == service.service.className) { + StreamLog.w(TAG) { "Service is running: $serviceClass" } + return true + } + } + StreamLog.w(TAG) { "Service is NOT running: $serviceClass" } + return false + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -245,7 +289,13 @@ internal class CallService : Service() { ) } else { callId = intentCallId - startForegroundWithServiceType(intentCallId.hashCode(), notification, trigger) + + startForegroundWithServiceType( + intentCallId.hashCode(), + notification, + trigger, + serviceType, + ) } true } else { @@ -297,7 +347,7 @@ internal class CallService : Service() { if (!hasActiveCall) { videoClient.getSettingUpCallNotification()?.let { - startForegroundWithServiceType(notificationId, it, trigger) + startForegroundWithServiceType(notificationId, it, trigger, serviceType) } } } @@ -306,7 +356,12 @@ internal class CallService : Service() { private fun showIncomingCall(notificationId: Int, notification: Notification) { if (callId == null) { // If there isn't another call in progress (callId is set in onStartCommand()) // The service was started with startForegroundService() (from companion object), so we need to call startForeground(). - startForegroundWithServiceType(notificationId, notification, TRIGGER_INCOMING_CALL) + startForegroundWithServiceType( + notificationId, + notification, + TRIGGER_INCOMING_CALL, + serviceType, + ) } else { // Else, we show a simple notification (the service was already started as a foreground service). NotificationManagerCompat diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfig.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfig.kt new file mode 100644 index 0000000000..cc7899c635 --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfig.kt @@ -0,0 +1,84 @@ +/* + * 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.core.notifications.internal.service + +import android.media.AudioAttributes +import io.getstream.video.android.model.StreamCallId + +// Constants +/** Marker for all the call types. */ +internal const val ANY_MARKER = "ALL_CALL_TYPES" + +// API +/** + * Configuration class for the call service. + * @param runCallServiceInForeground If the call service should run in the foreground. + * @param callServicePerType A map of call service per type. + */ +public data class CallServiceConfig( + val runCallServiceInForeground: Boolean = true, + val audioUsage: Int = AudioAttributes.USAGE_VOICE_COMMUNICATION, + val callServicePerType: Map> = mapOf( + Pair(ANY_MARKER, CallService::class.java), + ), +) + +/** + * Return a default configuration for the call service configuration. + */ +public fun callServiceConfig(): CallServiceConfig { + return CallServiceConfig( + runCallServiceInForeground = true, + callServicePerType = mapOf( + Pair(ANY_MARKER, CallService::class.java), + ), + ) +} + +/** + * Return a default configuration for the call service configuration. + */ +public fun livestreamCallServiceConfig(): CallServiceConfig { + return CallServiceConfig( + runCallServiceInForeground = true, + callServicePerType = mapOf( + Pair(ANY_MARKER, CallService::class.java), + Pair("livestream", LivestreamCallService::class.java), + ), + ) +} + +/** + * Return a default configuration for the call service configuration. + */ +public fun livestreamGuestCallServiceConfig(): CallServiceConfig { + return CallServiceConfig( + runCallServiceInForeground = true, + audioUsage = AudioAttributes.USAGE_MEDIA, + callServicePerType = mapOf( + Pair(ANY_MARKER, CallService::class.java), + Pair("livestream", LivestreamViewerService::class.java), + ), + ) +} + +// Internal +internal fun resolveServiceClass(callId: StreamCallId, config: CallServiceConfig): Class<*> { + val callType = callId.type + val resolvedServiceClass = config.callServicePerType[callType] + return resolvedServiceClass ?: config.callServicePerType[ANY_MARKER] ?: CallService::class.java +} diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/LivestreamCallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/LivestreamCallService.kt new file mode 100644 index 0000000000..608bf5bdcc --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/LivestreamCallService.kt @@ -0,0 +1,37 @@ +/* + * 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.core.notifications.internal.service + +import android.content.pm.ServiceInfo +import io.getstream.log.TaggedLogger +import io.getstream.log.taggedLogger + +/** + * Due to the nature of the livestream calls, the service that is used is of different type. + */ +internal open class LivestreamCallService : CallService() { + override val logger: TaggedLogger by taggedLogger("LivestreamHostCallService") + override val serviceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE +} + +/** + * Due to the nature of the livestream calls, the service that is used is of different type. + */ +internal class LivestreamViewerService : LivestreamCallService() { + override val logger: TaggedLogger by taggedLogger("LivestreamHostCallService") + override val serviceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK +} diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt index 2b63db15a2..b7e1ca325f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt @@ -21,6 +21,7 @@ import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.ServiceInfo import android.os.Binder import android.os.IBinder import androidx.core.app.NotificationChannelCompat @@ -103,14 +104,20 @@ internal class StreamScreenShareService : Service() { ) } - startForegroundWithServiceType(NOTIFICATION_ID, builder.build(), TRIGGER_SHARE_SCREEN) + startForegroundWithServiceType( + NOTIFICATION_ID, + builder.build(), + TRIGGER_SHARE_SCREEN, + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION, + ) return super.onStartCommand(intent, flags, startId) } companion object { internal const val NOTIFICATION_ID = 43534 internal const val EXTRA_CALL_ID = "EXTRA_CALL_ID" - internal const val BROADCAST_CANCEL_ACTION = "io.getstream.video.android.action.CANCEL_SCREEN_SHARE" + internal const val BROADCAST_CANCEL_ACTION = + "io.getstream.video.android.action.CANCEL_SCREEN_SHARE" internal const val INTENT_EXTRA_CALL_ID = "io.getstream.video.android.intent-extra.call_cid" internal const val TRIGGER_SHARE_SCREEN = "share_screen" diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt index 7139a1a04d..4073ec7e95 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt @@ -182,6 +182,7 @@ internal fun Service.startForegroundWithServiceType( notificationId: Int, notification: Notification, trigger: String, + foregroundServiceType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL, ) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { startForeground(notificationId, notification) @@ -189,7 +190,7 @@ internal fun Service.startForegroundWithServiceType( val beforeOrAfterAndroid14Type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE } else { - ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + foregroundServiceType } ServiceCompat.startForeground( @@ -197,7 +198,7 @@ internal fun Service.startForegroundWithServiceType( notificationId, notification, when (trigger) { - TRIGGER_ONGOING_CALL -> ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + TRIGGER_ONGOING_CALL -> foregroundServiceType TRIGGER_OUTGOING_CALL, TRIGGER_INCOMING_CALL -> beforeOrAfterAndroid14Type TRIGGER_SHARE_SCREEN -> ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION else -> beforeOrAfterAndroid14Type diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfigTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfigTest.kt new file mode 100644 index 0000000000..6962fd80fa --- /dev/null +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/CallServiceConfigTest.kt @@ -0,0 +1,127 @@ +/* + * 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.core.notifications.internal.service + +import android.media.AudioAttributes +import io.getstream.video.android.model.StreamCallId +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import kotlin.test.Test + +class CallServiceConfigTest { + + @Test + fun `callServiceConfig should return correct default configuration`() { + // Given + val config = callServiceConfig() + + // When + val runInForeground = config.runCallServiceInForeground + val servicePerTypeSize = config.callServicePerType.size + val serviceClass = config.callServicePerType[ANY_MARKER] + val audioUsage = config.audioUsage + + // Then + assertEquals(true, runInForeground) + assertEquals(1, servicePerTypeSize) + assertEquals(CallService::class.java, serviceClass) + assertEquals(AudioAttributes.USAGE_VOICE_COMMUNICATION, audioUsage) + } + + @Test + fun `livestreamCallServiceConfig should return correct default configuration`() { + // Given + val config = livestreamCallServiceConfig() + + // When + val runInForeground = config.runCallServiceInForeground + val servicePerTypeSize = config.callServicePerType.size + val hostServiceClass = config.callServicePerType[ANY_MARKER] + val livestreamServiceClass = config.callServicePerType["livestream"] + val audioUsage = config.audioUsage + + // Then + assertEquals(true, runInForeground) + assertEquals(2, servicePerTypeSize) + assertEquals(CallService::class.java, hostServiceClass) + assertEquals(LivestreamCallService::class.java, livestreamServiceClass) + assertEquals(AudioAttributes.USAGE_VOICE_COMMUNICATION, audioUsage) + } + + @Test + fun `resolveServiceClass should return correct service class for livestream type`() { + // Given + val streamCallId = mockk() + every { streamCallId.type } returns "livestream" + val config = livestreamCallServiceConfig() + + // When + val resolvedClass = resolveServiceClass(streamCallId, config) + + // Then + assertEquals(LivestreamCallService::class.java, resolvedClass) + } + + @Test + fun `resolveServiceClass should return default service class for unknown type`() { + // Given + val streamCallId = mockk() + every { streamCallId.type } returns "unknown" + val config = livestreamCallServiceConfig() + + // When + val resolvedClass = resolveServiceClass(streamCallId, config) + + // Then + assertEquals(CallService::class.java, resolvedClass) + } + + @Test + fun `resolveServiceClass should return default service class when no type is provided`() { + // Given + val streamCallId = mockk() + every { streamCallId.type } returns "" + val config = livestreamCallServiceConfig() + + // When + val resolvedClass = resolveServiceClass(streamCallId, config) + + // Then + assertEquals(CallService::class.java, resolvedClass) + } + + @Test + fun `livestreamGuestCallServiceConfig should return correct default configuration`() { + // Given + val config = livestreamGuestCallServiceConfig() + + // When + val runInForeground = config.runCallServiceInForeground + val servicePerTypeSize = config.callServicePerType.size + val hostServiceClass = config.callServicePerType[ANY_MARKER] + val livestreamServiceClass = config.callServicePerType["livestream"] + val audioUsage = config.audioUsage + + // Then + assertEquals(true, runInForeground) + assertEquals(2, servicePerTypeSize) + assertEquals(CallService::class.java, hostServiceClass) + assertEquals(LivestreamViewerService::class.java, livestreamServiceClass) + assertEquals(AudioAttributes.USAGE_MEDIA, audioUsage) + } +} diff --git a/tutorials/tutorial-livestream/src/main/AndroidManifest.xml b/tutorials/tutorial-livestream/src/main/AndroidManifest.xml index ab6a771b3d..8d8fe5da21 100644 --- a/tutorials/tutorial-livestream/src/main/AndroidManifest.xml +++ b/tutorials/tutorial-livestream/src/main/AndroidManifest.xml @@ -19,6 +19,11 @@ + + + + + Date: Tue, 27 Aug 2024 16:19:33 +0200 Subject: [PATCH 09/22] Prepare release 1.0.13 (#1168) --- .../main/kotlin/io/getstream/video/android/Configuration.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 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 820a0d3845..7d2bbfd85c 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 = 1 const val minorVersion = 0 - const val patchVersion = 12 + const val patchVersion = 13 const val versionName = "$majorVersion.$minorVersion.$patchVersion" - const val versionCode = 36 + const val versionCode = 37 const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" const val artifactGroup = "io.getstream" - const val streamVideoCallGooglePlayVersion = "1.1.5" + const val streamVideoCallGooglePlayVersion = "1.1.6" const val streamWebRtcVersionName = "1.1.1" } From efbe44040c389434844c21b8f711d674a5bf7d30 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 5 Sep 2024 10:54:34 +0200 Subject: [PATCH 10/22] Add audio only `CallService` to avoid requiring `CAMERA` permission (#1171) * Add audio only livestream call service. * Apidump --- .../api/stream-video-android-core.api | 1 + .../src/main/AndroidManifest.xml | 5 +++++ .../internal/service/CallServiceConfig.kt | 13 ++++++++++++ .../internal/service/LivestreamCallService.kt | 8 ++++++++ .../internal/service/CallServiceConfigTest.kt | 20 +++++++++++++++++++ 5 files changed, 47 insertions(+) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 1d6bde92f1..aeb44a3dc6 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -4238,6 +4238,7 @@ public final class io/getstream/video/android/core/notifications/internal/servic public final class io/getstream/video/android/core/notifications/internal/service/CallServiceConfigKt { public static final fun callServiceConfig ()Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; + public static final fun livestreamAudioCallServiceConfig ()Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; public static final fun livestreamCallServiceConfig ()Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; public static final fun livestreamGuestCallServiceConfig ()Lio/getstream/video/android/core/notifications/internal/service/CallServiceConfig; } diff --git a/stream-video-android-core/src/main/AndroidManifest.xml b/stream-video-android-core/src/main/AndroidManifest.xml index 701e3aadb5..7360e764c4 100644 --- a/stream-video-android-core/src/main/AndroidManifest.xml +++ b/stream-video-android-core/src/main/AndroidManifest.xml @@ -103,6 +103,11 @@ android:foregroundServiceType="camera|microphone" android:exported="false" /> + + Date: Mon, 9 Sep 2024 15:42:32 +0300 Subject: [PATCH 11/22] Improve camera select method (#1174) --- .../video/android/core/MediaManager.kt | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt index 1bcc227d30..52523ee401 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt @@ -611,25 +611,30 @@ public class CameraManager( CameraDirection.Back -> CameraDirection.Front } val device = devices.firstOrNull { it.direction == newDirection } - device?.let { select(it.id, false) } + device?.let { select(it.id, triggeredByFlip = true) } videoCapturer.switchCamera(null) } } - fun select(deviceId: String, startCapture: Boolean = false) { + fun select(deviceId: String, triggeredByFlip: Boolean = false) { + if (!triggeredByFlip) { + stopCapture() + if (!::devices.isInitialized) initDeviceList() + } + val selectedDevice = devices.firstOrNull { it.id == deviceId } if (selectedDevice != null) { _direction.value = selectedDevice.direction ?: CameraDirection.Back _selectedDevice.value = selectedDevice - _availableResolutions.value = - selectedDevice.supportedFormats?.toList() ?: emptyList() + _availableResolutions.value = selectedDevice.supportedFormats?.toList() ?: emptyList() _resolution.value = selectDesiredResolution( selectedDevice.supportedFormats, mediaManager.call.state.settings.value?.video, ) - if (startCapture) { + if (!triggeredByFlip) { + setup(force = true) startCapture() } } @@ -681,14 +686,13 @@ public class CameraManager( * Handle the setup of the camera manager and enumerator * You should only call this once the permissions have been granted */ - internal fun setup() { - if (setupCompleted) { + internal fun setup(force: Boolean = false) { + if (setupCompleted && !force) { return } - cameraManager = mediaManager.context.getSystemService() - enumerator = Camera2Enumerator(mediaManager.context) - val cameraIds = cameraManager?.cameraIdList ?: emptyArray() - devices = sortDevices(cameraIds, cameraManager, enumerator) + + initDeviceList() + val devicesMatchingDirection = devices.filter { it.direction == _direction.value } val selectedDevice = devicesMatchingDirection.firstOrNull() if (selectedDevice != null) { @@ -707,6 +711,13 @@ public class CameraManager( } } + private fun initDeviceList() { + cameraManager = mediaManager.context.getSystemService() + enumerator = Camera2Enumerator(mediaManager.context) + val cameraIds = cameraManager?.cameraIdList ?: emptyArray() + devices = sortDevices(cameraIds, cameraManager, enumerator) + } + /** * Creates a sorted list of camera devices * From 4fd53454ea877eee4b6677be0d7dcc40a36eba97 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:35:57 +0300 Subject: [PATCH 12/22] Fix unit test CI job failure (#1175) Use v4 of upload-artifact action instead of v2 (deprecated) --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 2911dd7960..dd9f6cfce9 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -120,13 +120,13 @@ jobs: ./gradlew :stream-video-android-core:testDebugUnitTest --scan --stacktrace - name: Unit tests core results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: unit-tests-core-results path: stream-video-android-core/build/reports/tests/testDebugUnitTest/index.html - name: Unit tests compose results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: unit-tests-compose-results path: stream-video-android-ui-compose/build/reports/tests/testDebugUnitTest/index.html From 85610ae77059019778fe722aedb9d24652364d2e Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:41:10 +0300 Subject: [PATCH 13/22] Update documentation (#1170) --- .../Android/03-guides/07-querying-calls.mdx | 33 +++++---- .../docs/Android/06-advanced/08-events.mdx | 53 ++++++++++++-- .../docs/Android/06-advanced/09-recording.mdx | 71 +++++++++--------- .../Android/06-advanced/10-broadcasting.mdx | 73 +++++++++++++++++++ ...{10-custom-data.mdx => 11-custom-data.mdx} | 0 5 files changed, 175 insertions(+), 55 deletions(-) create mode 100644 docusaurus/docs/Android/06-advanced/10-broadcasting.mdx rename docusaurus/docs/Android/06-advanced/{10-custom-data.mdx => 11-custom-data.mdx} (100%) diff --git a/docusaurus/docs/Android/03-guides/07-querying-calls.mdx b/docusaurus/docs/Android/03-guides/07-querying-calls.mdx index c9c12fc39e..07a279560b 100644 --- a/docusaurus/docs/Android/03-guides/07-querying-calls.mdx +++ b/docusaurus/docs/Android/03-guides/07-querying-calls.mdx @@ -25,6 +25,16 @@ val sort = listOf(SortField.Asc("starts_at")) val result = client.queryCalls(filters=filters, sort=sort, limit=10, watch=true) ``` +**Calls that are ongoing / currently have participants** + +```kotlin +client.queryCalls(mapOf("ongoing" to true)).let { result -> + result + .onSuccess { calls: QueriedCalls -> Log.d(TAG, "Query success: $calls") } + .onError { error: Error -> Log.e(TAG, "Query failure: ${error.message}") } +} +``` + **Calls filters on a custom property** ```kotlin @@ -36,6 +46,7 @@ val result = client.queryCalls(filters=filters, sort=sort, limit=10, watch=true) ``` **Pagination** + The query response is paginated and the maximum count of items is defined by the `limit` parameter. Use the `prev` and `next` parameters from the last response as parameters for requesting the next page. @@ -54,12 +65,6 @@ val resultPage2 = client.queryCalls( ) ``` -**Calls that live/ currently have participants** - -```kotlin -TODO -``` - ### Fields for Query Calls You can filter on the following fields @@ -78,15 +83,15 @@ You can filter on the following fields | `members` | Check if you are a member of this call | | `custom` | You can query custom data using the "custom.myfield" syntax | -Sorting is supported on these fields below: +Sorting is supported on the fields below: -* starts_at -* created_at -* updated_at -* ended_at -* type -* id -* cid +* `starts_at` +* `created_at` +* `updated_at` +* `ended_at` +* `type` +* `id` +* `cid` If you specify `watch` the SDK will automatically keep the data about these calls updated. This allows you to show a live preview of who's in the call. \ No newline at end of file diff --git a/docusaurus/docs/Android/06-advanced/08-events.mdx b/docusaurus/docs/Android/06-advanced/08-events.mdx index 09fb99a9bb..4ca216ce87 100644 --- a/docusaurus/docs/Android/06-advanced/08-events.mdx +++ b/docusaurus/docs/Android/06-advanced/08-events.mdx @@ -3,7 +3,7 @@ title: Events description: How to listen to events --- -In most cases you can simply use the stateflow objects Stream exposes. +In most cases you can simply use the `Stateflow` objects Stream exposes. However for some customizations you'll want to listen to the underlying events that power these state objects. ### Listening to events @@ -19,7 +19,7 @@ val sub = client.subscribe { event: VideoEvent -> sub.dispose() ``` -You can also subscribe for a specific call +You can also subscribe to call events. ```kotlin val call = client.call("default", "123") @@ -30,11 +30,11 @@ val sub = call.subscribe { event: VideoEvent -> sub.dispose() ``` -Or listen to a specific event +Or listen to a specific event. ```kotlin -val sub = client.subscribeFor { newMessageEvent -> - logger.d { newMessageEvent.toString() } +val sub = client.subscribeFor { event -> + logger.d { event.toString() } } // stop listening sub.dispose() @@ -42,6 +42,45 @@ sub.dispose() ### Events -The following events are triggered by the client: +The following events are emitted by the client: -TODO \ No newline at end of file +| Event Name | Description | +|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BlockedUserEvent` | This event is sent to call participants to notify when a user is blocked on a call. Clients can use this event to show a notification. If the user is the current user, the client should leave the call screen as well. | +| `CallAcceptedEvent` | This event is sent when a user accepts a notification to join a call. | +| `CallCreatedEvent` | This event is sent when a call is created. Clients receiving this event should check if the ringing field is set to true and if so, show the call screen. | +| `CallEndedEvent` | This event is sent when a call is marked as ended for all its participants. Clients receiving this event should leave the call screen. | +| `CallHLSBroadcastingStartedEvent` | This event is sent when call HLS broadcasting has started. | +| `CallHLSBroadcastingStoppedEvent` | This event is sent when call HLS broadcasting has stopped. | +| `CallHLSBroadcastingFailedEvent` | This event indicates that call HLS broadcasting has failed. | +| `CallLiveStartedEvent` | This event is sent when a livestream has started. | +| `CallMemberAddedEvent` | This event is sent when one or more members are added to a call. | +| `CallMemberRemovedEvent` | This event is sent when one or more members are removed from a call. | +| `CallMemberUpdatedEvent` | This event is sent when one or more members are updated. | +| `CallMemberUpdatedPermissionEvent` | This event is sent when one or more members get their role capabilities updated. | +| `CallReactionEvent` | This event is sent when a reaction is sent in a call, clients should use this to show the reaction in the call screen | +| `CallRecordingStartedEvent` | This event is sent when call recording has started. | +| `CallRecordingStoppedEvent` | This event is sent when call recording has stopped. | +| `CallRecordingReadyEvent` | Indicates that a call recording is ready. | +| `CallRecordingFailedEvent` | Indicates that recording a call failed. | +| `CallRejectedEvent` | This event is sent when a user rejects a notification to join a call. | +| `CallRingEvent` | This event is sent to all call members to notify they are getting called. | +| `CallSessionStartedEvent` | This event is sent when a call session starts. | +| `CallSessionEndedEvent` | This event is sent when a call session ends. | +| `CallSessionParticipantJoinedEvent` | This event is sent when a participant joins a call session. | +| `CallSessionParticipantLeftEvent` | This event is sent when a participant leaves a call session. | +| `CallTranscriptionStartedEvent` | This event indicates that call transcribing has started. | +| `CallTranscriptionStoppedEvent` | Indicates that call transcribing has stopped. | +| `CallTranscriptionReadyEvent` | This event is sent when call transcriptions are ready. | +| `CallTranscriptionFailedEvent` | Indicates that call transcribing failed. | +| `CallUpdatedEvent` | This event is sent when a call is updated. Clients should use this update the local state of the call. This event also contains the capabilities by role for the call, clients should update the own_capability for the current. | +| `ConnectedEvent` | This event is sent when the WS connection is established and authenticated. This event contains the full user object as it is stored on the server. | +| `ConnectionErrorEvent` | This event is sent when the WS connection attempt fails. | +| `HealthCheckEvent` | Periodic event used to check the connection health. | +| `PermissionRequestEvent` | This event is sent when a user requests access to a feature on a call, clients receiving this event should display a permission request to the user. | +| `UnblockedUserEvent` | This event is sent when a user is unblocked on a call. This can be useful to notify the user that they can now join the call again. | +| `UpdatedCallPermissionsEvent` | This event is sent to notify about permission changes for a user. Clients receiving this event should update their UI accordingly. | +| `VideoEvent` | The discriminator object for all websocket events, you should use this to map event payloads to the correct type. | +| `WSCallEvent` | Placeholder for all call events. | +| `WSClientEvent` | Placeholder for all client events. | +| `CustomVideoEvent` | A custom event. This event is used to send custom events to other participants in the call. | diff --git a/docusaurus/docs/Android/06-advanced/09-recording.mdx b/docusaurus/docs/Android/06-advanced/09-recording.mdx index 9c92a4a4c3..eba79edfc3 100644 --- a/docusaurus/docs/Android/06-advanced/09-recording.mdx +++ b/docusaurus/docs/Android/06-advanced/09-recording.mdx @@ -1,59 +1,62 @@ --- -title: Recording & Broadcasting -description: Recording & Broadcasting +title: Recording +description: Recording Calls --- -### Recording +In certain situations, you may need to record a call and share the recording with the participants. The Stream Video SDK supports this functionality via the `Call` recording API. -The example below shows how to start and stop recording a call +### Start and Stop Recording + +To start recording, we simply invoke `call.startRecording()`. To stop recording, we use `call.stopRecording()`. ```kotlin call.startRecording() call.stopRecording() ``` -You can retrieve recordings: +The `call.state.recording` property of type `StateFlow` will be updated when call recording starts/stops. ```kotlin -val result = call.listRecordings() +val isRecording by call.state.recording.collectAsStateWithLifecycle() // Use to update the UI ``` -There are several layout options... TODO - -### Broadcasting to HLS - -You can broadcast your call to HLS - -```kotlin -call.startBroadcasting() -call.stopBroadcasting() -``` +### Get a List of Recordings -The HLS url is available in the call state +You can retrieve recordings by using `call.listRecordings()`. If the query is successful, `result` will contain a list of recordings, each containing information about the filename, URL and the start and end times. You can use the URL to show the recording in a video player. ```kotlin +val result = call.listRecordings() +result + .onSuccess { response: ListRecordingsResponse -> + response.recordings.forEach { recording: CallRecording -> + Log.d(TAG, recording.filename) + Log.d(TAG, recording.url) + Log.d(TAG, recording.startTime.toString()) + Log.d(TAG, recording.endTime.toString()) + } + } + .onError { error: Error -> + Log.e(TAG, "Failure: ${error.message}") + } ``` -### RTMP-in - -You can also add RTMPs stream's to your call. - -```kotlin -val url = call.state.ingress.value?.rtmp?.address -// TODO: streaming key -``` - -We plan to add support for other livestreaming protocols in the future. If something is missing be sure to let us know. - -### Displaying HLS +### Listening to Recording Events -On android you can display HLS using ExoPlayer +You can listen to recording-related events and change to UI accordingly. ```kotlin -implementation "androidx.media3:media3-exoplayer:1.0.2" -implementation "androidx.media3:media3-ui:1.0.2" -implementation "androidx.media3:media3-exoplayer-hls:1.0.2" +val sub = call.subscribeFor( + CallRecordingStartedEvent::class.java, + CallRecordingStoppedEvent::class.java, + CallRecordingReadyEvent::class.java, + CallRecordingFailedEvent::class.java +) { + Log.e(TAG, "Event type: ${it.getEventType()}") +} + +// stop listening +sub.dispose() ``` -This article explains how to use (ExoPlayer with compose)[https://proandroiddev.com/learn-with-code-jetpack-compose-playing-media-part-3-3792bdfbe1ea]. +Read more about subscribing to events on the [events](08-events.mdx) page. \ No newline at end of file diff --git a/docusaurus/docs/Android/06-advanced/10-broadcasting.mdx b/docusaurus/docs/Android/06-advanced/10-broadcasting.mdx new file mode 100644 index 0000000000..b4a3b7a0ae --- /dev/null +++ b/docusaurus/docs/Android/06-advanced/10-broadcasting.mdx @@ -0,0 +1,73 @@ +--- +title: Broadcasting +description: Broadcasting Calls +--- + +The Stream Video SDK has support for HLS broadcasting. + +### Start and Stop HLS broadcasting + +```kotlin +call.startHLS() +call.stopHLS() +``` + +After few seconds of setup, broadcasting will start and the state of the call will be updated: the `call.state.broadcasting` boolean flag will become `true`. + +### Listening to Broadcasting Events + +You can listen to broadcasting-related events and change to UI accordingly. + +```kotlin +val sub = subscribeFor( + CallHLSBroadcastingStartedEvent::class.java, + CallHLSBroadcastingStoppedEvent::class.java, + CallHLSBroadcastingFailedEvent::class.java, +) { + Log.e(TAG, "Event type: ${it.getEventType()}") +} + +// stop listening +sub.dispose() +``` + +See more about subscribing to events on the [events](08-events.mdx) page. + +### Retrieving the Broadcast URL + +The URL for the broadcast can be retrieved from the `CallHLSBroadcastingStartedEvent` event. It can be used by others to watch the broadcast. + +```kotlin +call.subscribe { event -> + when (event) { + is CallHLSBroadcastingStartedEvent -> { + Log.d(TAG, event.hlsPlaylistUrl) + } + } +} +``` + +### Displaying HLS + +On Android you can play a HLS broadcast by using ExoPlayer. + +```kotlin +implementation "androidx.media3:media3-exoplayer:1.0.2" +implementation "androidx.media3:media3-ui:1.0.2" +implementation "androidx.media3:media3-exoplayer-hls:1.0.2" +``` + +[This](https://proandroiddev.com/learn-with-code-jetpack-compose-playing-media-part-3-3792bdfbe1ea) article explains how to use ExoPlayer with Compose. + +### RTMP-In + +You can also use RTMP streams as input for a call. + +```kotlin +val url = call.state.ingress.value?.rtmp?.address +val streamingKey = call.state.ingress.value?.rtmp?.streamKey +``` + +You can read more about RTMP-In in our [livestreaming tutorial](https://getstream.io/video/sdk/android/tutorial/livestreaming). + +We plan to add support for other livestreaming protocols in the future. If something is missing be sure to let us know. \ No newline at end of file diff --git a/docusaurus/docs/Android/06-advanced/10-custom-data.mdx b/docusaurus/docs/Android/06-advanced/11-custom-data.mdx similarity index 100% rename from docusaurus/docs/Android/06-advanced/10-custom-data.mdx rename to docusaurus/docs/Android/06-advanced/11-custom-data.mdx From b0c0ad8b77250d60af4288df35b6efb85e3ec130 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:55:22 +0300 Subject: [PATCH 14/22] Fix upload artifact and distribution CI jobs (#1176) --- .github/workflows/app-distribute.yml | 2 +- .github/workflows/artifact-upload.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/app-distribute.yml b/.github/workflows/app-distribute.yml index f67dcc418e..ccc307da8b 100644 --- a/.github/workflows/app-distribute.yml +++ b/.github/workflows/app-distribute.yml @@ -27,7 +27,7 @@ jobs: - name: Assemble run: bash ./gradlew :demo-app:assembleRelease --stacktrace - name: Upload APK - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: demo-app-release path: demo-app/build/outputs/apk/demo-app/release/ diff --git a/.github/workflows/artifact-upload.yaml b/.github/workflows/artifact-upload.yaml index e0979be1e4..70a0e54121 100644 --- a/.github/workflows/artifact-upload.yaml +++ b/.github/workflows/artifact-upload.yaml @@ -35,7 +35,7 @@ jobs: run: ./gradlew bundleRelease --stacktrace - name: Upload AAB as artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: app-bundle path: demo-app/build/outputs/bundle/productionRelease/demo-app-production-release.aab From fa44c8d35a793f662e1ef13aa588e7c80f9c1ea9 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Tue, 10 Sep 2024 17:32:58 +0900 Subject: [PATCH 15/22] Migrate to Compose 1.7.0 stable and other compose relevant dependencies (#1177) --- .../io/getstream/video/AndroidCompose.kt | 3 +- .../android/ui/call/LandscapeControls.kt | 2 -- .../video/android/ui/call/LayoutChooser.kt | 3 -- .../android/ui/call/ParticipantsDialog.kt | 2 -- .../video/android/ui/login/LoginScreen.kt | 6 ++-- .../video/android/ui/menu/SettingsMenu.kt | 3 +- gradle/libs.versions.toml | 26 +++++++------- .../api/stream-video-android-ui-compose.api | 10 ++++-- ...eTheme.kt => StreamRippleConfiguration.kt} | 35 ++++++++----------- .../video/android/compose/theme/VideoTheme.kt | 19 +++++----- .../compose/ui/components/avatar/Avatar.kt | 4 +-- 11 files changed, 54 insertions(+), 59 deletions(-) rename stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/{StreamRippleTheme.kt => StreamRippleConfiguration.kt} (52%) diff --git a/build-logic/convention/src/main/kotlin/io/getstream/video/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/io/getstream/video/AndroidCompose.kt index 659a289666..a14f1234f2 100644 --- a/build-logic/convention/src/main/kotlin/io/getstream/video/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/io/getstream/video/AndroidCompose.kt @@ -8,6 +8,7 @@ import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.getByType import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag /** * Configure Compose-specific options @@ -31,7 +32,7 @@ internal fun Project.configureAndroidCompose( } extensions.configure { - enableStrongSkippingMode = true + featureFlags.addAll(ComposeFeatureFlag.StrongSkipping, ComposeFeatureFlag.IntrinsicRemember) reportsDestination = layout.buildDirectory.dir("compose_compiler") stabilityConfigurationFile = rootProject.layout.projectDirectory.file("compose_compiler_config.conf") } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt index 67379e3d52..2fdf65f63f 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt @@ -29,7 +29,6 @@ import androidx.compose.material.icons.filled.CallEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview @@ -48,7 +47,6 @@ import io.getstream.video.android.mock.StreamPreviewDataUtils import io.getstream.video.android.mock.previewCall import io.getstream.video.android.tooling.extensions.toPx -@OptIn(ExperimentalComposeUiApi::class) @Composable fun LandscapeControls(call: Call, onDismiss: () -> Unit) { val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle() diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt index b4e135cd73..d6c9539fec 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LayoutChooser.kt @@ -14,13 +14,10 @@ * limitations under the License. */ -@file:OptIn(ExperimentalLayoutApi::class) - package io.getstream.video.android.ui.call import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AutoAwesome diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ParticipantsDialog.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ParticipantsDialog.kt index ae39e7b058..be87934629 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ParticipantsDialog.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/ParticipantsDialog.kt @@ -18,7 +18,6 @@ package io.getstream.video.android.ui.call import android.content.ClipboardManager import android.content.Context -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -100,7 +99,6 @@ fun ParticipantsList(call: Call) { ParticipantsListContent(call, clipboardManager, participants) } -@OptIn(ExperimentalFoundationApi::class) @Composable fun ParticipantsListContent( call: Call, diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt index 0730d5b048..bf37897108 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/login/LoginScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -48,7 +47,7 @@ import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Adb import androidx.compose.material.icons.outlined.GroupAdd -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -408,7 +407,7 @@ private fun BuiltInUsersLoginDialog( .fillMaxWidth() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = true), + indication = ripple(bounded = true), onClick = { login(true, LoginEvent.SignIn(user)) onDismissRequest() @@ -439,7 +438,6 @@ private fun BuiltInUsersLoginDialog( ) } -@OptIn(ExperimentalLayoutApi::class) @Composable fun SelectableDialog( items: List, diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt index c3fa19bb3e..7447eff3ce 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/menu/SettingsMenu.kt @@ -39,7 +39,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview @@ -63,7 +62,7 @@ import io.getstream.video.android.util.filters.SampleAudioFilter import kotlinx.coroutines.launch import java.nio.ByteBuffer -@OptIn(ExperimentalComposeUiApi::class, ExperimentalPermissionsApi::class) +@OptIn(ExperimentalPermissionsApi::class) @Composable internal fun SettingsMenu( call: Call, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2cfc4aa40..3420408de4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,11 +3,11 @@ androidGradlePlugin = "8.4.2" cameraCamera2 = "1.3.0" spotless = "6.21.0" nexusPlugin = "1.3.0" -kotlin = "2.0.0" -ksp = "2.0.0-1.0.22" -kotlinSerialization = "1.6.3" +kotlin = "2.0.20" +ksp = "2.0.20-1.0.25" +kotlinSerialization = "1.7.1" kotlinSerializationConverter = "1.0.0" -kotlinxCoroutines = "1.8.0" +kotlinxCoroutines = "1.8.1" kotlinDokka = "1.9.20" jvmTarget = "11" @@ -16,21 +16,21 @@ androidxMaterial = "1.11.0" androidxAppCompat = "1.6.1" androidxCore = "1.12.0" androidxAnnotation = "1.7.1" -androidxLifecycle = "2.7.0" +androidxLifecycle = "2.8.5" androidxStartup = "1.1.1" -androidxActivity = "1.9.0" +androidxActivity = "1.9.2" androidxDataStore = "1.0.0" googleService = "4.3.14" -androidxComposeBom = "2024.06.00" +androidxComposeBom = "2024.09.00" androidxComposeTracing = "1.0.0-beta01" androidxHiltNavigation = "1.2.0" -androidxComposeNavigation = "2.7.7" +androidxComposeNavigation = "2.8.0" composeStableMarker = "1.0.5" coil = "2.6.0" -landscapist = "2.3.5" -accompanist = "0.32.0" +landscapist = "2.3.6" +accompanist = "0.34.0" telephoto = "0.3.0" audioswitch = "1.1.8" libyuv = "0.30.0" @@ -44,7 +44,7 @@ tink = "1.9.0" turbine = "0.13.0" streamWebRTC = "1.1.2" -streamResult = "1.1.0" +streamResult = "1.2.0" streamChat = "6.0.13" streamLog = "1.1.4" streamPush = "1.1.7" @@ -72,10 +72,10 @@ playAppUpdate = "2.1.0" hilt = "2.51.1" leakCanary = "2.13" -binaryCompatabilityValidator = "0.14.0" +binaryCompatabilityValidator = "0.16.3" playPublisher = "3.8.4" -googleMlKitSelfieSegmentation = "16.0.0-beta4" +googleMlKitSelfieSegmentation = "16.0.0-beta6" [libraries] androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCamera2" } 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 6daa63645b..cf9a49199b 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 @@ -260,6 +260,12 @@ public final class io/getstream/video/android/compose/theme/StreamDimens$Compani public final fun defaultDimens (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/StreamDimens; } +public final class io/getstream/video/android/compose/theme/StreamRippleConfiguration { + public static final field $stable I + public static final field INSTANCE Lio/getstream/video/android/compose/theme/StreamRippleConfiguration; + public final fun default (Landroidx/compose/runtime/Composer;I)Landroidx/compose/material/RippleConfiguration; +} + public final class io/getstream/video/android/compose/theme/StreamShapes { public static final field $stable I public static final field Companion Lio/getstream/video/android/compose/theme/StreamShapes$Companion; @@ -295,7 +301,7 @@ public abstract interface class io/getstream/video/android/compose/theme/StreamT public fun getColors (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/StreamColors; public fun getDimens (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/StreamDimens; public fun getReactionMapper (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/core/mapper/ReactionMapper; - public fun getRippleTheme (Landroidx/compose/runtime/Composer;I)Landroidx/compose/material/ripple/RippleTheme; + public fun getRippleConfiguration (Landroidx/compose/runtime/Composer;I)Landroidx/compose/material/RippleConfiguration; public fun getShapes (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/StreamShapes; public fun getStyles (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider; public fun getTypography (Landroidx/compose/runtime/Composer;I)Lio/getstream/video/android/compose/theme/StreamTypography; @@ -350,7 +356,7 @@ public final class io/getstream/video/android/compose/theme/VideoTheme : io/gets } public final class io/getstream/video/android/compose/theme/VideoThemeKt { - public static final fun VideoTheme (ZLio/getstream/video/android/compose/theme/StreamColors;Lio/getstream/video/android/compose/theme/StreamDimens;Lio/getstream/video/android/compose/theme/StreamTypography;Lio/getstream/video/android/compose/theme/StreamShapes;Landroidx/compose/material/ripple/RippleTheme;Lio/getstream/video/android/core/mapper/ReactionMapper;ZLio/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun VideoTheme (ZLio/getstream/video/android/compose/theme/StreamColors;Lio/getstream/video/android/compose/theme/StreamDimens;Lio/getstream/video/android/compose/theme/StreamTypography;Lio/getstream/video/android/compose/theme/StreamShapes;Lio/getstream/video/android/compose/theme/StreamRippleConfiguration;Lio/getstream/video/android/core/mapper/ReactionMapper;ZLio/getstream/video/android/compose/ui/components/base/styling/CompositeStyleProvider;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/video/android/compose/ui/ComposableSingletons$StreamCallActivityComposeDelegateKt { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleTheme.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleConfiguration.kt similarity index 52% rename from stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleTheme.kt rename to stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleConfiguration.kt index 55427cd27c..9231b22ae1 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleTheme.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/StreamRippleConfiguration.kt @@ -14,36 +14,31 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterialApi::class) + package io.getstream.video.android.compose.theme -import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.LocalContentColor +import androidx.compose.material.LocalRippleConfiguration import androidx.compose.material.MaterialTheme -import androidx.compose.material.ripple.RippleAlpha -import androidx.compose.material.ripple.RippleTheme +import androidx.compose.material.RippleConfiguration import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.ReadOnlyComposable /** - * A modified version of the default [RippleTheme] from [MaterialTheme] which + * A modified version of the default [RippleConfiguration] from [MaterialTheme] which * works in case the [MaterialTheme] is not initialized. */ -@Immutable -internal object StreamRippleTheme : RippleTheme { - @Composable - override fun defaultColor(): Color { - return RippleTheme.defaultRippleColor( - contentColor = LocalContentColor.current, - lightTheme = !isSystemInDarkTheme(), - ) - } +public object StreamRippleConfiguration { @Composable - override fun rippleAlpha(): RippleAlpha { - return RippleTheme.defaultRippleAlpha( - contentColor = LocalContentColor.current, - lightTheme = !isSystemInDarkTheme(), - ) + @ReadOnlyComposable + public fun default(): RippleConfiguration { + val rippleConfiguration = LocalRippleConfiguration.current + if (rippleConfiguration != null) return rippleConfiguration + + val contentColor = LocalContentColor.current + return RippleConfiguration(color = contentColor) } } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt index 62c3d79156..58eb881d05 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/theme/VideoTheme.kt @@ -20,8 +20,9 @@ package io.getstream.video.android.compose.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.material.ripple.LocalRippleTheme -import androidx.compose.material.ripple.RippleTheme +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalRippleConfiguration +import androidx.compose.material.RippleConfiguration import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable @@ -72,18 +73,19 @@ private val LocalStyles = compositionLocalOf { * @param dimens The set of dimens we provide, wrapped in [StreamDimens]. * @param typography The set of typography styles we provide, wrapped in [StreamTypography]. * @param shapes The set of shapes we provide, wrapped in [StreamShapes]. - * @param rippleTheme Defines the appearance for ripples. + * @param rippleConfiguration Defines the appearance for ripples. * @param reactionMapper Defines a mapper of the emoji code from the reaction events. * @param content The content shown within the theme wrapper. */ @Composable +@OptIn(ExperimentalMaterialApi::class) public fun VideoTheme( isInDarkMode: Boolean = isSystemInDarkTheme(), colors: StreamColors = StreamColors.defaultColors(), dimens: StreamDimens = StreamDimens.defaultDimens(), typography: StreamTypography = StreamTypography.defaultTypography(colors, dimens), shapes: StreamShapes = StreamShapes.defaultShapes(dimens), - rippleTheme: RippleTheme = StreamRippleTheme, + rippleConfiguration: StreamRippleConfiguration = StreamRippleConfiguration, reactionMapper: ReactionMapper = ReactionMapper.defaultReactionMapper(), allowUIAutomationTest: Boolean = true, styles: CompositeStyleProvider = CompositeStyleProvider(), @@ -94,7 +96,7 @@ public fun VideoTheme( LocalDimens provides dimens, LocalTypography provides typography, LocalShapes provides shapes, - LocalRippleTheme provides rippleTheme, + LocalRippleConfiguration provides rippleConfiguration.default(), LocalReactionMapper provides reactionMapper, LocalStyles provides styles, ) { @@ -108,6 +110,7 @@ public fun VideoTheme( } } +@OptIn(ExperimentalMaterialApi::class) public interface StreamTheme { /** * Retrieves the current [StreamColors] at the call site's position in the hierarchy. @@ -138,11 +141,11 @@ public interface StreamTheme { get() = LocalShapes.current /** - * Retrieves the current [RippleTheme] at the call site's position in the hierarchy. + * Retrieves the current [RippleConfiguration] at the call site's position in the hierarchy. */ - public val rippleTheme: RippleTheme + public val rippleConfiguration: RippleConfiguration? @Composable @ReadOnlyComposable - get() = LocalRippleTheme.current + get() = StreamRippleConfiguration.default() /** * Retrieves the current [ReactionMapper] at the call site's position in the hierarchy. diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt index 7362f9fa86..58041a0ff4 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/Avatar.kt @@ -23,7 +23,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -107,7 +107,7 @@ internal fun Avatar( val clickableModifier: Modifier = if (onClick != null) { modifier.clickable( onClick = onClick, - indication = rememberRipple(bounded = false), + indication = ripple(bounded = false), interactionSource = remember { MutableInteractionSource() }, ) } else { From 0cac26c4f26f88b89870fe1f07cf346a0e871c57 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:40:30 +0300 Subject: [PATCH 16/22] [PBE-6015] Add extra info to request client header (#1178) * Add app name * Do not add header when creating socket (duplicated) * Use package name when app name is not provided --- .../android/util/StreamVideoInitHelper.kt | 1 + .../api/stream-video-android-core.api | 3 +- .../video/android/core/StreamVideo.kt | 30 +++++++++---------- .../video/android/core/StreamVideoBuilder.kt | 3 ++ .../video/android/core/StreamVideoImpl.kt | 1 + .../android/core/socket/PersistentSocket.kt | 2 -- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt index 786c3fcf6e..7d11d48e0e 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt @@ -206,6 +206,7 @@ object StreamVideoInitHelper { ) authData.token }, + appName = "Stream Video Demo App", ).build() } } diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index aeb44a3dc6..78171e6a4a 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -823,7 +823,8 @@ public final class io/getstream/video/android/core/StreamVideoBuilder { public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;Z)V public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;)V public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;I)V - public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;ILjava/lang/String;)V + public synthetic fun (Landroid/content/Context;Ljava/lang/String;Lio/getstream/video/android/core/GEO;Lio/getstream/video/android/model/User;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lio/getstream/video/android/core/logging/LoggingLevel;Lio/getstream/video/android/core/notifications/NotificationConfig;Lkotlin/jvm/functions/Function1;JZLjava/lang/String;ZLio/getstream/video/android/core/notifications/internal/service/CallServiceConfig;Ljava/lang/String;Lio/getstream/video/android/core/sounds/Sounds;ZLio/getstream/video/android/core/permission/android/StreamPermissionCheck;ILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun build ()Lio/getstream/video/android/core/StreamVideo; } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt index c2f1a928f0..c244b9610e 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt @@ -203,26 +203,26 @@ public interface StreamVideo : NotificationHandler { } /** - * Builds a detailed header of information we track around the SDK, Android OS, API Level, device name and - * vendor and more. + * Builds the client information header (X-Stream-Client) that will be added to requests. * - * @return String formatted header that contains all the information. + * @return Header value as a string. */ internal fun buildSdkTrackingHeaders(): String { - val clientInformation = "stream-video-android-${BuildConfig.STREAM_VIDEO_VERSION}" - - val buildModel = Build.MODEL - val deviceManufacturer = Build.MANUFACTURER - val apiLevel = Build.VERSION.SDK_INT - val osName = "Android ${Build.VERSION.RELEASE}" - - return clientInformation + - "|os=$osName" + - "|api_version=$apiLevel" + - "|device_vendor=$deviceManufacturer" + - "|device_model=$buildModel" + val streamVideoVersion = "stream-video-android-${BuildConfig.STREAM_VIDEO_VERSION}" + val os = "|os=Android ${Build.VERSION.RELEASE}" + val apiVersion = "|api_version=${Build.VERSION.SDK_INT}" + val deviceVendor = "|device_vendor=${Build.MANUFACTURER}" + val deviceModel = "|device_model=${Build.MODEL}" + val appName = buildAppName() + + return streamVideoVersion + os + apiVersion + deviceVendor + deviceModel + appName } + private fun buildAppName(): String = + (internalStreamVideo as? StreamVideoImpl)?.let { streamVideoImpl -> + "|app_name=" + (streamVideoImpl.appName ?: streamVideoImpl.context.packageName) + } ?: "" + /** * Uninstall a previous [StreamVideo] instance. */ diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt index 1d0b579d4b..180dc5ccb0 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt @@ -77,6 +77,7 @@ import java.net.ConnectException * @property permissionCheck Used to check for system permission based on call capabilities. See [StreamPermissionCheck]. * @property crashOnMissingPermission Throw an exception or just log an error if [permissionCheck] fails. * @property audioUsage Used to signal to the system how to treat the audio tracks (voip or media). + * @property appName Optional name for the application that is using the Stream Video SDK. Used for logging and debugging purposes. * * @see build * @see ClientState.connection @@ -102,6 +103,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( private val crashOnMissingPermission: Boolean = false, private val permissionCheck: StreamPermissionCheck = DefaultStreamPermissionCheck(), private val audioUsage: Int = defaultAudioUsage, + private val appName: String? = null, ) { private val context: Context = context.applicationContext private val scope = CoroutineScope(DispatcherProvider.IO) @@ -199,6 +201,7 @@ public class StreamVideoBuilder @JvmOverloads constructor( permissionCheck = permissionCheck, crashOnMissingPermission = crashOnMissingPermission, audioUsage = audioUsage, + appName = appName, ) if (user.type == UserType.Guest) { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt index d4b95fe578..8b4163ab6b 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt @@ -156,6 +156,7 @@ internal class StreamVideoImpl internal constructor( internal val permissionCheck: StreamPermissionCheck = DefaultStreamPermissionCheck(), internal val crashOnMissingPermission: Boolean = false, internal val audioUsage: Int = defaultAudioUsage, + internal val appName: String? = null, ) : StreamVideo, NotificationHandler by streamNotificationManager { private var locationJob: Deferred>? = null diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt index 0fd5ad5418..21a3cfeab4 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/PersistentSocket.kt @@ -17,7 +17,6 @@ package io.getstream.video.android.core.socket import io.getstream.log.taggedLogger -import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.core.dispatchers.DispatcherProvider import io.getstream.video.android.core.errors.VideoErrorCode import io.getstream.video.android.core.internal.network.NetworkStateProvider @@ -277,7 +276,6 @@ public open class PersistentSocket( .url(url) .addHeader("Connection", "Upgrade") .addHeader("Upgrade", "websocket") - .addHeader("X-Stream-Client", StreamVideo.buildSdkTrackingHeaders()) .build() return httpClient.newWebSocket(request, this) From 7080ea26e05ab4490c5c68f68d71a2b2aedcba80 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Tue, 17 Sep 2024 13:04:05 +0200 Subject: [PATCH 17/22] Prepare release 1.0.14 (#1180) --- .../main/kotlin/io/getstream/video/android/Configuration.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 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 7d2bbfd85c..fba92c7b00 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 = 1 const val minorVersion = 0 - const val patchVersion = 13 + const val patchVersion = 14 const val versionName = "$majorVersion.$minorVersion.$patchVersion" - const val versionCode = 37 + const val versionCode = 38 const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" const val artifactGroup = "io.getstream" - const val streamVideoCallGooglePlayVersion = "1.1.6" + const val streamVideoCallGooglePlayVersion = "1.1.7" const val streamWebRtcVersionName = "1.1.1" } From 18c505748807938ec1181d4349edbb159dfbee94 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 19 Sep 2024 09:58:18 +0200 Subject: [PATCH 18/22] Remove debug info (#1181) * Remove `DebugInfo` class * Spotless and ApiDump --- .../video/android/core/AndroidDeviceTest.kt | 3 - .../video/android/core/CallSwitchingTest.kt | 61 ------- .../io/getstream/video/android/core/Call.kt | 10 +- .../video/android/core/StreamVideoImpl.kt | 12 +- .../video/android/core/call/RtcSession.kt | 15 -- .../video/android/core/utils/DebugInfo.kt | 157 ------------------ 6 files changed, 4 insertions(+), 254 deletions(-) delete mode 100644 stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/CallSwitchingTest.kt delete mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/DebugInfo.kt diff --git a/stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/AndroidDeviceTest.kt b/stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/AndroidDeviceTest.kt index 22c873c2b2..c3ddd72332 100644 --- a/stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/AndroidDeviceTest.kt +++ b/stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/AndroidDeviceTest.kt @@ -295,8 +295,6 @@ class AndroidDeviceTest : IntegrationTestBase(connectCoordinatorWS = false) { delay(1000) - clientImpl.debugInfo.log() - // leave and cleanup the joining call call.leave() call.cleanup() @@ -471,7 +469,6 @@ class AndroidDeviceTest : IntegrationTestBase(connectCoordinatorWS = false) { // log debug info logger.d { networkOut.toString() } - clientImpl.debugInfo.log() // leave and clean up a call call.leave() diff --git a/stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/CallSwitchingTest.kt b/stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/CallSwitchingTest.kt deleted file mode 100644 index f4e80fa3af..0000000000 --- a/stream-video-android-core/src/androidTest/kotlin/io/getstream/video/android/core/CallSwitchingTest.kt +++ /dev/null @@ -1,61 +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.core - -import io.getstream.log.taggedLogger -import io.getstream.video.android.core.utils.Timer -import kotlinx.coroutines.test.runTest -import org.junit.Test -import kotlin.time.Duration.Companion.seconds - -class CallSwitchingTest : IntegrationTestBase(connectCoordinatorWS = false) { - - private val logger by taggedLogger("Test:AndroidDeviceTest") - - @Test - fun switch() = runTest(timeout = 30.seconds) { - val location = clientImpl.getCachedLocation() - println("location: $location") - val numberOfCalls = 10 - // create 3 calls - val calls = (0 until numberOfCalls).map { - val call = client.call("audio_room", "switch-test-$it") - val result = call.create(custom = mapOf("switchtest" to "1")) - assertSuccess(result) - call - } - - // loop over the calls, join them and leave - val timer = Timer("switch to location $location") - (0 until numberOfCalls).map { - val call = client.call("audio_room", "switch-test-$it") - val result = call.join() - assertSuccess(result) - assertSuccess(result) - timer.split("iteration $it") - call.leave() - } - timer.finish() - - timer.let { - logger.i { "${it.name} took ${it.duration}" } - it.durations.forEach { (s, t) -> - logger.i { " - ${it.name}:$s took $t" } - } - } - } -} 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 71f8b4c24c..18e16145af 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 @@ -380,14 +380,12 @@ public class Call( } // step 1. call the join endpoint to get a list of SFUs - val timer = clientImpl.debugInfo.trackTime("call.join") val locationResult = clientImpl.getCachedLocation() if (locationResult !is Success) { return locationResult as Failure } location = locationResult.value - timer.split("location found") val options = createOptions ?: if (create) { @@ -403,7 +401,6 @@ public class Call( val sfuToken = result.value.credentials.token val sfuUrl = clientImpl.testSfuAddress ?: result.value.credentials.server.url val iceServers = result.value.credentials.iceServers.map { it.toIceServer() } - timer.split("join request completed") session = if (testInstanceProvider.rtcSessionCreator != null) { testInstanceProvider.rtcSessionCreator!!.invoke() @@ -425,21 +422,18 @@ public class Call( session?.let { state._connection.value = RealtimeConnection.Joined(it) } - timer.split("rtc session init") try { session?.connect() } catch (e: Exception) { return Failure(Error.GenericError(e.message ?: "RtcSession error occurred.")) - } finally { - timer.split("rtc connect completed") } scope.launch { // wait for the first stream to be added session?.let { rtcSession -> val mainRtcSession = rtcSession.lastVideoStreamAdded.filter { it != null }.first() - timer.finish("stream added, rtc completed, ready to display video $mainRtcSession") + logger.d { "stream added, rtc completed, ready to display video $mainRtcSession" } } } @@ -458,8 +452,6 @@ public class Call( } } - timer.finish() - return Success(value = session!!) } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt index 8b4163ab6b..6b4b201966 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoImpl.kt @@ -54,7 +54,6 @@ import io.getstream.video.android.core.socket.ErrorResponse import io.getstream.video.android.core.socket.PersistentSocket import io.getstream.video.android.core.socket.SocketState import io.getstream.video.android.core.sounds.Sounds -import io.getstream.video.android.core.utils.DebugInfo import io.getstream.video.android.core.utils.LatencyResult import io.getstream.video.android.core.utils.getLatencyMeasurementsOKHttp import io.getstream.video.android.core.utils.safeCall @@ -175,8 +174,6 @@ internal class StreamVideoImpl internal constructor( CoroutineScope(_scope.coroutineContext + SupervisorJob() + coroutineExceptionHandler) /** if true we fail fast on errors instead of logging them */ - var developmentMode = true - val debugInfo = DebugInfo(this) /** session id is generated client side */ public val sessionId = UUID.randomUUID().toString() @@ -201,7 +198,6 @@ internal class StreamVideoImpl internal constructor( override fun cleanup() { // remove all cached calls calls.clear() - debugInfo.stop() // stop all running coroutines scope.cancel() // stop the socket @@ -411,8 +407,6 @@ internal class StreamVideoImpl internal constructor( } } } - - debugInfo.start() } var location: String? = null @@ -456,10 +450,10 @@ internal class StreamVideoImpl internal constructor( // wait for the guest user setup if we're using guest users guestUserJob?.await() try { - val timer = debugInfo.trackTime("coordinator connect") + val startTime = System.currentTimeMillis() socketImpl.connect() - timer.finish() - Success(timer.duration) + val duration = System.currentTimeMillis() - startTime + Success(duration) } catch (e: ErrorResponse) { if (e.code == VideoErrorCode.TOKEN_EXPIRED.code) { refreshToken(e) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt index ddec0150ec..838aeb4b8c 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt @@ -380,9 +380,6 @@ public class RtcSession internal constructor( errorJob = coroutineScope.launch { sfuConnectionModule.sfuSocket.errors.collect { logger.e(it) { "permanent failure on socket connection" } - if (clientImpl.developmentMode) { - throw it - } } } } @@ -422,10 +419,7 @@ public class RtcSession internal constructor( } suspend fun connect() { - val timer = clientImpl.debugInfo.trackTime("sfu ws") sfuConnectionModule.sfuSocket.connect() - timer.finish() - // ensure that the join event has been handled before starting RTC try { withTimeout(2000L) { @@ -605,7 +599,6 @@ public class RtcSession internal constructor( private suspend fun connectRtc() { val settings = call.state.settings.value - val timer = clientImpl.debugInfo.trackTime("connectRtc") // turn of the speaker if needed if (settings?.audio?.speakerDefaultOn == false) { @@ -620,14 +613,12 @@ public class RtcSession internal constructor( if (canPublish) { publisher = createPublisher() - timer.split("createPublisher") } else { // enable the publisher if you receive the send audio or send video capability coroutineScope.launch { call.state.ownCapabilities.collect { if (it.any { it == OwnCapability.SendAudio || it == OwnCapability.SendVideo }) { publisher = createPublisher() - timer.split("createPublisher") } } } @@ -653,7 +644,6 @@ public class RtcSession internal constructor( // step 2 ensure all tracks are setup correctly // start capturing the video - timer.split("media enabled") // step 4 add the audio track to the publisher setLocalTrack( TrackType.TRACK_TYPE_AUDIO, @@ -687,7 +677,6 @@ public class RtcSession internal constructor( } // step 6 - onNegotiationNeeded will trigger and complete the setup using SetPublisherRequest - timer.finish() listenToMediaChanges() // subscribe to the tracks of other participants @@ -1735,7 +1724,6 @@ public class RtcSession internal constructor( failedToSwitch: () -> Unit, ) { logger.i { "[switchSfu] from ${this.sfuUrl} to $sfuUrl" } - val timer = clientImpl.debugInfo.trackTime("call.switchSfu") // Prepare SDP val getSdp = suspend { @@ -1767,7 +1755,6 @@ public class RtcSession internal constructor( when (it) { is SocketState.Connected -> { logger.d { "[switchSfu] Migration SFU socket state changed to Connected" } - timer.split("SFU socket connected") // Disconnect the old SFU and stop listening to SFU stateflows eventJob?.cancel() @@ -1807,7 +1794,6 @@ public class RtcSession internal constructor( subscriber?.state?.collect { if (it == PeerConnectionState.CONNECTED) { logger.d { "[switchSfu] Migration subscriber state changed to Connected" } - timer.split("Subscriber connected") tempSubscriber?.let { tempSubscriberValue -> tempSubscriberValue.connection.close() tempSubscriber = null @@ -1815,7 +1801,6 @@ public class RtcSession internal constructor( onMigrationCompleted.invoke() - timer.finish() cancel() } else if (it == PeerConnectionState.CLOSED || it == PeerConnectionState.DISCONNECTED || diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/DebugInfo.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/DebugInfo.kt deleted file mode 100644 index bc65500f55..0000000000 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/DebugInfo.kt +++ /dev/null @@ -1,157 +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.core.utils - -import android.os.Build -import io.getstream.log.taggedLogger -import io.getstream.video.android.core.StreamVideoImpl -import io.getstream.video.android.core.dispatchers.DispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.util.Collections - -internal data class Timer(val name: String, val start: Long = System.currentTimeMillis()) { - var end: Long = 0 - var duration: Long = 0 - var splits: List> = mutableListOf() - var durations: List> = mutableListOf() - - fun split(s: String) { - val now = System.currentTimeMillis() - val last = splits.lastOrNull()?.second ?: start - splits += s to now - durations += s to (now - last) - } - - fun finish(s: String? = null): Long { - s?.let { - split(s) - } - end = System.currentTimeMillis() - duration = end - start - return duration - } -} - -/** - * Handy helper gathering all relevant debug information - */ -internal class DebugInfo(val client: StreamVideoImpl) { - private var job: Job? = null - val scope = CoroutineScope(DispatcherProvider.IO) - - private val logger by taggedLogger("DebugInfo") - - // timers to help track performance issues in prod - val timers = Collections.synchronizedList(mutableListOf()) - // last 20 events - - // phone type - val phoneModel = Build.MODEL - - // android version - val version = Build.VERSION.SDK_INT - - // how many times the network dropped - - // how often the sockets reconnected - - // supported codecs - // resolution - val resolution by lazy { } - val availableResolutions by lazy { } - - fun start() { - if (client.developmentMode && !DispatcherProvider.inTest) { - job = scope.launch { - while (true) { - delay(20000) - log() - } - } - } - } - - fun stop() { - job?.cancel() - } - - fun log() { - val call = client.state.activeCall.value - val sessionId = call?.sessionId - val subscriber = call?.session?.subscriber - val publisher = call?.session?.publisher - val resolution = call?.camera?.resolution?.value - val availableResolutions = call?.camera?.availableResolutions?.value - val maxResolution = availableResolutions?.maxByOrNull { it.width * it.height } - - val publisherIce = publisher?.connection?.iceConnectionState() - val subIce = subscriber?.connection?.iceConnectionState() - - val videoTrackState = call?.mediaManager?.videoTrack?.state() - val coordinatorSocket = client.socketImpl.connectionState.value.javaClass.name - val sfuSocket = - call?.session?.sfuConnectionModule?.sfuSocket?.connectionState?.value?.javaClass?.name - - // good spot to attach your debugger - - logger.i { "Debug info $phoneModel running android $version" } - logger.i { "Active call is ${call?.cid}, session id $sessionId" } - logger.i { - "video quality: current resolution $resolution max resolution for camera is $maxResolution" - } - logger.i { - "Coordinator socket: $coordinatorSocket, SFU socket: $sfuSocket Subscriber: $publisherIce Publisher: $subIce" - } - logger.i { "Performance details" } - timers.forEach { - logger.i { "${it.name} took ${it.duration}" } - it.durations.forEach { (s, t) -> - logger.i { " - ${it.name}:$s took $t" } - } - } - /* - Stats wishlist - - selected sfu - - max resolution & fps capture - - incoming, rendering at resolution vs receiving resolution - - jitter & latency - - fir, pli, nack etc - - video limit reasons - - selected resolution - - TCP instead of UDP - - TODO: - - thermal profiles: https://proandroiddev.com/thermal-in-android-26cc202e9d3b - - webrtc get FPS levels (actually it's in the logs, but the format is clunky) - - match participant and track id.. - - */ - } - - fun listCodecs() { - // see https://developer.android.com/reference/kotlin/android/media/MediaCodecInfo - } - - fun trackTime(s: String): Timer { - val timer = Timer(s, System.currentTimeMillis()) - timers += timer - return timer - } -} From fa526e83ec242be4d549ec8997465753831e2049 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 19 Sep 2024 11:35:17 +0200 Subject: [PATCH 19/22] Improvements in `StreamCallActivity` and `demo-app` (#1184) * Add more controls in landscape, `configurationChange` is no longer considered a destructive action to `leave` the call. Add wake lock when a call is ongoing. * Use the official recommended way to detect PiP in the `CallScreen` as well as SDK. --- .../video/android/ui/call/CallScreen.kt | 54 ++++++++++------- .../android/ui/call/LandscapeControls.kt | 58 ++++++++++++++----- .../api/stream-video-android-ui-compose.api | 5 ++ .../android/compose/pip/PictureInPicture.kt | 48 ++++++++++++++- .../ui/components/audio/AudioRoomContent.kt | 4 +- .../components/call/activecall/CallContent.kt | 4 +- .../api/stream-video-android-ui-core.api | 12 ++-- .../android/ui/common/StreamCallActivity.kt | 30 ++++++++-- .../common/StreamCallActivityConfiguration.kt | 6 ++ 9 files changed, 170 insertions(+), 51 deletions(-) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt index 1a7120c8b1..c9520f5c69 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/CallScreen.kt @@ -70,6 +70,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState import io.getstream.video.android.BuildConfig import io.getstream.video.android.R +import io.getstream.video.android.compose.pip.rememberIsInPipMode import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.base.StreamBadgeBox import io.getstream.video.android.compose.ui.components.base.StreamDialogPositiveNegative @@ -411,27 +412,32 @@ fun CallScreen( }, ) - if (participantsSize.size == 1 && !chatState.isVisible && orientation == Configuration.ORIENTATION_PORTRAIT) { - val context = LocalContext.current - val clipboardManager = remember(context) { - context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - } - val env = AppConfig.currentEnvironment.collectAsStateWithLifecycle() - Popup( - alignment = Alignment.BottomCenter, - offset = IntOffset( - 0, - -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx() - .toInt(), - ), + val isPictureInPictureMode = rememberIsInPipMode() + if (!isPictureInPictureMode) { + if (participantsSize.size == 1 && + !chatState.isVisible && + orientation == Configuration.ORIENTATION_PORTRAIT ) { - ShareCallWithOthers( - modifier = Modifier.fillMaxWidth(), - call = call, - clipboardManager = clipboardManager, - env = env, - context = context, - ) + val clipboardManager = remember(context) { + context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + } + val env = AppConfig.currentEnvironment.collectAsStateWithLifecycle() + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset( + 0, + -(VideoTheme.dimens.componentHeightL + VideoTheme.dimens.spacingS).toPx() + .toInt(), + ), + ) { + ShareCallWithOthers( + modifier = Modifier.fillMaxWidth(), + call = call, + clipboardManager = clipboardManager, + env = env, + context = context, + ) + } } } @@ -440,7 +446,13 @@ fun CallScreen( } if (showingLandscapeControls && orientation == Configuration.ORIENTATION_LANDSCAPE) { - LandscapeControls(call) { + LandscapeControls(call, onChat = { + showingLandscapeControls = false + scope.launch { chatState.show() } + }, onSettings = { + showingLandscapeControls = false + isShowingSettingMenu = true + }) { showingLandscapeControls = !showingLandscapeControls } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt index 2fdf65f63f..72d5b5b7ed 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/call/LandscapeControls.kt @@ -21,11 +21,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CallEnd +import androidx.compose.material.icons.filled.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -38,6 +41,7 @@ import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.base.StreamButton +import io.getstream.video.android.compose.ui.components.call.controls.actions.ChatDialogAction import io.getstream.video.android.compose.ui.components.call.controls.actions.FlipCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleCameraAction import io.getstream.video.android.compose.ui.components.call.controls.actions.ToggleMicrophoneAction @@ -48,7 +52,12 @@ import io.getstream.video.android.mock.previewCall import io.getstream.video.android.tooling.extensions.toPx @Composable -fun LandscapeControls(call: Call, onDismiss: () -> Unit) { +fun LandscapeControls( + call: Call, + onChat: () -> Unit, + onSettings: () -> Unit, + onDismiss: () -> Unit, +) { val isCameraEnabled by call.camera.isEnabled.collectAsStateWithLifecycle() val isMicrophoneEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle() val toggleCamera = { @@ -76,6 +85,8 @@ fun LandscapeControls(call: Call, onDismiss: () -> Unit) { camera = toggleCamera, mic = toggleMicrophone, onClick = onClick, + onChat = onChat, + onSettings = onSettings, ) { onDismiss() } @@ -87,6 +98,8 @@ fun LandscapeControlsContent( isCameraEnabled: Boolean, isMicEnabled: Boolean, call: Call, + onChat: () -> Unit, + onSettings: () -> Unit, camera: () -> Unit, mic: () -> Unit, onClick: () -> Unit, @@ -107,21 +120,36 @@ fun LandscapeControlsContent( ReactionsMenu(call = call, reactionMapper = ReactionMapper.defaultReactionMapper()) { onDismiss() } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - ToggleCameraAction(isCameraEnabled = isCameraEnabled) { - camera() - } - ToggleMicrophoneAction(isMicrophoneEnabled = isMicEnabled) { - mic() - } - FlipCameraAction { - call.camera.flip() - } + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ToggleCameraAction(isCameraEnabled = isCameraEnabled) { + camera() + } + ToggleMicrophoneAction(isMicrophoneEnabled = isMicEnabled) { + mic() + } + FlipCameraAction { + call.camera.flip() + } + ChatDialogAction( + messageCount = 0, + onCallAction = { onChat() }, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + StreamButton( + modifier = Modifier.fillMaxWidth(), + style = VideoTheme.styles.buttonStyles.primaryIconButtonStyle(), + icon = Icons.Default.Settings, + text = "Settings", + onClick = onSettings, + ) StreamButton( + modifier = Modifier.fillMaxWidth(), style = VideoTheme.styles.buttonStyles.alertButtonStyle(), icon = Icons.Default.CallEnd, text = "Leave call", @@ -144,6 +172,8 @@ fun LandscapeControlsPreview() { {}, {}, {}, + {}, + {}, ) { } } diff --git a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api index cf9a49199b..5370b2c7b7 100644 --- a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api +++ b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api @@ -42,6 +42,11 @@ public abstract interface class io/getstream/video/android/compose/permission/Vi public abstract fun launchPermissionRequest ()V } +public final class io/getstream/video/android/compose/pip/PictureInPictureKt { + public static final fun isInPictureInPictureMode (Landroid/content/Context;)Z + public static final fun rememberIsInPipMode (Landroidx/compose/runtime/Composer;I)Z +} + public abstract interface class io/getstream/video/android/compose/state/ui/internal/CallParticipantInfoMode { } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt index 1aca609532..c6146dfa51 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/pip/PictureInPicture.kt @@ -24,6 +24,16 @@ import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.os.Build import android.util.Rational +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.util.Consumer import io.getstream.video.android.core.Call @Suppress("DEPRECATION") @@ -58,12 +68,48 @@ internal fun enterPictureInPicture(context: Context, call: Call) { } } -internal val Context.isInPictureInPictureMode: Boolean +/** + * Remember if the current activity is in Picture-in-Picture mode. + * To be used in compose to decide weather the current mode is PiP or not. + */ +@Composable +public fun rememberIsInPipMode(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val activity = LocalContext.current.findComponentActivity() + var pipMode by remember { mutableStateOf(activity?.isInPictureInPictureMode) } + DisposableEffect(activity) { + val observer = Consumer { info -> + pipMode = info.isInPictureInPictureMode + } + activity?.addOnPictureInPictureModeChangedListener( + observer, + ) + onDispose { activity?.removeOnPictureInPictureModeChangedListener(observer) } + } + return pipMode ?: false + } else { + return false + } +} + +/** + * Used in other parts of the app to check if the current context is in Picture-in-Picture mode. + */ +public val Context.isInPictureInPictureMode: Boolean get() { val currentActivity = findActivity() return currentActivity?.isInPictureInPictureMode == true } +internal fun Context.findComponentActivity(): ComponentActivity? { + var context = this + while (context is ContextWrapper) { + if (context is ComponentActivity) return context + context = context.baseContext + } + return null +} + internal fun Context.findActivity(): Activity? { var context = this while (context is ContextWrapper) { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt index 486765419f..9ce3728fbe 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/audio/AudioRoomContent.kt @@ -44,7 +44,7 @@ import io.getstream.video.android.compose.lifecycle.MediaPiPLifecycle import io.getstream.video.android.compose.permission.VideoPermissionsState import io.getstream.video.android.compose.permission.rememberMicrophonePermissionState import io.getstream.video.android.compose.pip.enterPictureInPicture -import io.getstream.video.android.compose.pip.isInPictureInPictureMode +import io.getstream.video.android.compose.pip.rememberIsInPipMode import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState @@ -122,7 +122,7 @@ public fun AudioRoomContent( ) { val context = LocalContext.current val orientation = LocalConfiguration.current.orientation - val isInPictureInPicture = context.isInPictureInPictureMode + val isInPictureInPicture = rememberIsInPipMode() DefaultPermissionHandler(videoPermission = permissions) diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt index 8a2f8e8983..b54f34d2a7 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt @@ -55,7 +55,7 @@ import io.getstream.video.android.compose.lifecycle.MediaPiPLifecycle import io.getstream.video.android.compose.permission.VideoPermissionsState import io.getstream.video.android.compose.permission.rememberCallPermissionsState import io.getstream.video.android.compose.pip.enterPictureInPicture -import io.getstream.video.android.compose.pip.isInPictureInPictureMode +import io.getstream.video.android.compose.pip.rememberIsInPipMode import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.call.CallAppBar import io.getstream.video.android.compose.ui.components.call.activecall.internal.DefaultPermissionHandler @@ -151,7 +151,7 @@ public fun CallContent( ) { val context = LocalContext.current val orientation = LocalConfiguration.current.orientation - val isInPictureInPicture = context.isInPictureInPictureMode + val isInPictureInPicture = rememberIsInPipMode() DefaultPermissionHandler(videoPermission = permissions) diff --git a/stream-video-android-ui-core/api/stream-video-android-ui-core.api b/stream-video-android-ui-core/api/stream-video-android-ui-core.api index 3c7208df4a..37b85ba400 100644 --- a/stream-video-android-ui-core/api/stream-video-android-ui-core.api +++ b/stream-video-android-ui-core/api/stream-video-android-ui-core.api @@ -80,15 +80,17 @@ public final class io/getstream/video/android/ui/common/StreamCallActivity$Compa public final class io/getstream/video/android/ui/common/StreamCallActivityConfiguration { public fun ()V - public fun (ZZZLandroid/os/Bundle;)V - public synthetic fun (ZZZLandroid/os/Bundle;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZZLandroid/os/Bundle;)V + public synthetic fun (ZZZZLandroid/os/Bundle;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Z public final fun component2 ()Z public final fun component3 ()Z - public final fun component4 ()Landroid/os/Bundle; - public final fun copy (ZZZLandroid/os/Bundle;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; - public static synthetic fun copy$default (Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;ZZZLandroid/os/Bundle;ILjava/lang/Object;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public final fun component4 ()Z + public final fun component5 ()Landroid/os/Bundle; + public final fun copy (ZZZZLandroid/os/Bundle;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public static synthetic fun copy$default (Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;ZZZZLandroid/os/Bundle;ILjava/lang/Object;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; public fun equals (Ljava/lang/Object;)Z + public final fun getCanKeepScreenOn ()Z public final fun getCanSkiPermissionRationale ()Z public final fun getCloseScreenOnCallEnded ()Z public final fun getCloseScreenOnError ()Z diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt index b6dda4d512..f3c7ecbc22 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt @@ -24,6 +24,7 @@ import android.os.Build import android.os.Bundle import android.os.PersistableBundle import android.util.Rational +import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.annotation.CallSuper import androidx.lifecycle.lifecycleScope @@ -71,7 +72,7 @@ public abstract class StreamCallActivity : ComponentActivity() { private const val EXTRA_MEMBERS_ARRAY: String = "members_extra" // Extra default values - private const val DEFAULT_LEAVE_WHEN_LAST: Boolean = true + private const val DEFAULT_LEAVE_WHEN_LAST: Boolean = false private val defaultExtraMembers = emptyList() private val logger by taggedLogger("DefaultCallActivity") @@ -90,7 +91,7 @@ public abstract class StreamCallActivity : ComponentActivity() { context: Context, cid: StreamCallId, members: List = defaultExtraMembers, - leaveWhenLastInCall: Boolean = DEFAULT_LEAVE_WHEN_LAST, + leaveWhenLastInCall: Boolean = true, action: String? = null, clazz: Class, configuration: StreamCallActivityConfiguration = StreamCallActivityConfiguration(), @@ -178,7 +179,6 @@ public abstract class StreamCallActivity : ComponentActivity() { // Platform restriction public final override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - onPreCreate(savedInstanceState, null) logger.d { "Entered [onCreate(Bundle?)" } initializeCallOrFail( savedInstanceState, @@ -344,7 +344,9 @@ public abstract class StreamCallActivity : ComponentActivity() { * @param call */ public open fun onResume(call: Call) { - // No - op + if (configuration.canKeepScreenOn) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } logger.d { "DefaultCallActivity - Resumed (call -> $call)" } } @@ -354,7 +356,13 @@ public abstract class StreamCallActivity : ComponentActivity() { * @param call the call */ public open fun onPause(call: Call) { - if (isVideoCall(call) && !isInPictureInPictureMode) { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + // Default PiP behavior + if (isConnected(call) && + !isChangingConfigurations && + isVideoCall(call) && + !isInPictureInPictureMode + ) { enterPictureInPicture() } logger.d { "DefaultCallActivity - Paused (call -> $call)" } @@ -383,7 +391,7 @@ public abstract class StreamCallActivity : ComponentActivity() { * @param call the call */ public open fun onStop(call: Call) { - // Extension point only. + // No-op logger.d { "Default activity - stopped (call -> $call)" } } @@ -738,6 +746,7 @@ public abstract class StreamCallActivity : ComponentActivity() { onSuccessFinish.invoke(call) } } + is RealtimeConnection.Failed -> { lifecycleScope.launch { val conn = state as? RealtimeConnection.Failed @@ -746,6 +755,7 @@ public abstract class StreamCallActivity : ComponentActivity() { onErrorFinish.invoke(throwable) } } + else -> { // No-op } @@ -846,6 +856,14 @@ public abstract class StreamCallActivity : ComponentActivity() { } } + private fun isConnected(call: Call): Boolean = + when (call.state.connection.value) { + RealtimeConnection.Disconnected -> false + RealtimeConnection.PreJoin -> false + is RealtimeConnection.Failed -> false + else -> true + } + private suspend fun Result.onOutcome( call: Call, onSuccess: (suspend (Call) -> Unit)? = null, diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt index 206b7ba62f..05473f74bd 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt @@ -22,6 +22,7 @@ internal object StreamCallActivityConfigStrings { const val EXTRA_STREAM_CONFIG = "stream-activity-config" const val EXTRA_CLOSE_ON_ERROR = "close-on-error" const val EXTRA_CLOSE_ON_ENDED = "close-on-ended" + const val EXTRA_KEEP_SCREEN_ON = "keep-screen-on" const val EXTRA_CAN_SKIP_RATIONALE = "skip-rationale-allowed" const val EXTRA_CUSTOM = "custom-fields" } @@ -36,6 +37,8 @@ public data class StreamCallActivityConfiguration( val closeScreenOnCallEnded: Boolean = true, /** When set to false, the activity will simply ignore the `showRationale` from the system and show the rationale screen anyway. */ val canSkiPermissionRationale: Boolean = true, + /** When set to true, the activity will keep the screen on. */ + val canKeepScreenOn: Boolean = true, /** * Custom configuration extension for any extending classes. * Can be used same as normal extras. @@ -53,10 +56,12 @@ public fun Bundle.extractStreamActivityConfig(): StreamCallActivityConfiguration getBoolean(StreamCallActivityConfigStrings.EXTRA_CLOSE_ON_ENDED, true) val canSkipPermissionRationale = getBoolean(StreamCallActivityConfigStrings.EXTRA_CAN_SKIP_RATIONALE, true) + val canKeepScreenOn = getBoolean(StreamCallActivityConfigStrings.EXTRA_KEEP_SCREEN_ON, true) val custom = getBundle(StreamCallActivityConfigStrings.EXTRA_CUSTOM) return StreamCallActivityConfiguration( closeScreenOnError = closeScreenOnError, closeScreenOnCallEnded = closeScreenOnCallEnded, + canKeepScreenOn = canKeepScreenOn, canSkiPermissionRationale = canSkipPermissionRationale, custom = custom, ) @@ -73,6 +78,7 @@ public fun StreamCallActivityConfiguration.toBundle(): Bundle { StreamCallActivityConfigStrings.EXTRA_CAN_SKIP_RATIONALE, canSkiPermissionRationale, ) + bundle.putBoolean(StreamCallActivityConfigStrings.EXTRA_KEEP_SCREEN_ON, canKeepScreenOn) bundle.putBundle(StreamCallActivityConfigStrings.EXTRA_CUSTOM, custom) return bundle } From 7a8221380b5507d859c87e013b5d57477017a593 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:22:16 +0300 Subject: [PATCH 20/22] Improve notifications (#1183) * Collect remote participants in CallService and set callDisplayName * Split observeCallState into several methods * Refactor incoming and ongoing notifications * Refactor outgoing notifications * Add getNotificationUpdates() in NotificationHandler and default implementation * Fix hang up bug * Set null as callDisplayName default value in getOngoingCallNotification and getRingingCallNotification * Add KDocs to getNotificationUpdates --------- Co-authored-by: Aleksandar Apostolov --- .../api/stream-video-android-core.api | 7 +- .../DefaultNotificationHandler.kt | 259 ++++++++++++------ .../core/notifications/NotificationHandler.kt | 28 +- .../internal/NoOpNotificationHandler.kt | 15 +- .../internal/service/CallService.kt | 95 ++++--- .../video/android/model/StreamCallId.kt | 2 +- .../res/drawable/stream_video_ic_user.xml | 28 ++ .../drawable/stream_video_ic_user_group.xml | 34 +++ .../src/main/res/values/strings.xml | 9 +- 9 files changed, 340 insertions(+), 137 deletions(-) create mode 100644 stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml create mode 100644 stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 78171e6a4a..833485348b 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -4139,7 +4139,8 @@ public class io/getstream/video/android/core/notifications/DefaultNotificationHa public final fun getHideRingingNotificationInForeground ()Z public final fun getNotificationIconRes ()I protected final fun getNotificationManager ()Landroidx/core/app/NotificationManagerCompat; - public fun getOngoingCallNotification (Ljava/lang/String;Lio/getstream/video/android/model/StreamCallId;)Landroid/app/Notification; + public fun getNotificationUpdates (Lkotlinx/coroutines/CoroutineScope;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Lkotlin/jvm/functions/Function1;)V + public fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZI)Landroid/app/Notification; public fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public fun getSettingUpCallNotification ()Landroid/app/Notification; public fun onLiveCall (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;)V @@ -4189,7 +4190,9 @@ public abstract interface class io/getstream/video/android/core/notifications/No public static final field INTENT_EXTRA_CALL_CID Ljava/lang/String; public static final field INTENT_EXTRA_CALL_DISPLAY_NAME Ljava/lang/String; public static final field INTENT_EXTRA_NOTIFICATION_ID Ljava/lang/String; - public abstract fun getOngoingCallNotification (Ljava/lang/String;Lio/getstream/video/android/model/StreamCallId;)Landroid/app/Notification; + public abstract fun getNotificationUpdates (Lkotlinx/coroutines/CoroutineScope;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Lkotlin/jvm/functions/Function1;)V + public abstract fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZI)Landroid/app/Notification; + public static synthetic fun getOngoingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZIILjava/lang/Object;)Landroid/app/Notification; public abstract fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public static synthetic fun getRingingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZILjava/lang/Object;)Landroid/app/Notification; public abstract fun getSettingUpCallNotification ()Landroid/app/Notification; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt index c8db32af1a..4027d2d355 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt @@ -32,18 +32,26 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.CallStyle import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat import io.getstream.android.push.permissions.DefaultNotificationPermissionHandler import io.getstream.android.push.permissions.NotificationPermissionHandler import io.getstream.log.taggedLogger +import io.getstream.video.android.core.Call import io.getstream.video.android.core.R import io.getstream.video.android.core.RingingState import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_LIVE_CALL import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_MISSED_CALL import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_NOTIFICATION -import io.getstream.video.android.core.notifications.NotificationHandler.Companion.INCOMING_CALL_NOTIFICATION_ID import io.getstream.video.android.core.notifications.internal.DefaultStreamIntentResolver import io.getstream.video.android.core.notifications.internal.service.CallService import io.getstream.video.android.model.StreamCallId +import io.getstream.video.android.model.User +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch public open class DefaultNotificationHandler( private val application: Application, @@ -107,7 +115,7 @@ public open class DefaultNotificationHandler( override fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, - callDisplayName: String, + callDisplayName: String?, shouldHaveContentIntent: Boolean, ): Notification? { return if (ringingState is RingingState.Incoming) { @@ -132,10 +140,10 @@ public open class DefaultNotificationHandler( val endCallPendingIntent = intentResolver.searchEndCallPendingIntent(callId) if (outgoingCallPendingIntent != null && endCallPendingIntent != null) { - getOutgoingCallNotification( - outgoingCallPendingIntent, - endCallPendingIntent, + getOngoingCallNotification( + callId, callDisplayName, + isOutgoingCall = true, ) } else { logger.e { "Ringing call notification not shown, one of the intents is null." } @@ -183,7 +191,7 @@ public open class DefaultNotificationHandler( fullScreenPendingIntent: PendingIntent, acceptCallPendingIntent: PendingIntent, rejectCallPendingIntent: PendingIntent, - callDisplayName: String, + callerName: String?, shouldHaveContentIntent: Boolean, ): Notification { // if the app is in foreground then don't interrupt the user with a high priority @@ -231,12 +239,12 @@ public open class DefaultNotificationHandler( return getNotification { priority = NotificationCompat.PRIORITY_HIGH - setContentTitle( - application.getString(R.string.stream_video_incoming_call_notification_title), + setContentTitle(callerName) + setContentText( + application.getString(R.string.stream_video_incoming_call_notification_description), ) - setContentText(callDisplayName) setChannelId(channelId) - setOngoing(false) + setOngoing(true) setCategory(NotificationCompat.CATEGORY_CALL) setFullScreenIntent(fullScreenPendingIntent, true) if (shouldHaveContentIntent) { @@ -251,49 +259,7 @@ public open class DefaultNotificationHandler( setContentIntent(emptyIntent) setAutoCancel(false) } - addCallActions(acceptCallPendingIntent, rejectCallPendingIntent, callDisplayName) - } - } - - private fun getOutgoingCallNotification( - outgoingCallPendingIntent: PendingIntent, - endCallPendingIntent: PendingIntent, - callDisplayName: String, - ): Notification { - val channelId = application.getString( - R.string.stream_video_outgoing_call_notification_channel_id, - ) - maybeCreateChannel( - channelId = channelId, - context = application, - configure = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - name = application.getString( - R.string.stream_video_outgoing_call_notification_channel_title, - ) - description = application.getString( - R.string.stream_video_outgoing_call_notification_channel_description, - ) - } - }, - ) - - return getNotification { - setContentTitle( - application.getString(R.string.stream_video_outgoing_call_notification_title), - ) - setContentText(callDisplayName) - setChannelId(channelId) - setOngoing(true) - setContentIntent(outgoingCallPendingIntent) - setCategory(NotificationCompat.CATEGORY_CALL) - addAction( - NotificationCompat.Action.Builder( - android.R.drawable.ic_menu_close_clear_cancel, - application.getString(R.string.stream_video_call_notification_action_cancel), - endCallPendingIntent, - ).build(), - ) + addCallActions(acceptCallPendingIntent, rejectCallPendingIntent, callerName) } } @@ -322,17 +288,30 @@ public open class DefaultNotificationHandler( } override fun getOngoingCallNotification( - callDisplayName: String?, callId: StreamCallId, + callDisplayName: String?, + isOutgoingCall: Boolean, + remoteParticipantCount: Int, ): Notification? { val notificationId = callId.hashCode() // Notification ID // Intents - val ongoingCallIntent = intentResolver.searchOngoingCallPendingIntent( - callId, - notificationId, - ) - val endCallIntent = intentResolver.searchEndCallPendingIntent(callId = callId) + val onClickIntent = if (isOutgoingCall) { + intentResolver.searchOutgoingCallPendingIntent( + callId, + notificationId, + ) + } else { + intentResolver.searchOngoingCallPendingIntent( + callId, + notificationId, + ) + } + val hangUpIntent = if (isOutgoingCall) { + intentResolver.searchRejectCallPendingIntent(callId) + } else { + intentResolver.searchEndCallPendingIntent(callId) + } // Channel preparation val ongoingCallsChannelId = application.getString( @@ -352,7 +331,7 @@ public open class DefaultNotificationHandler( }, ) - if (endCallIntent == null) { + if (hangUpIntent == null) { logger.e { "End call intent is null, not showing notification!" } return null } @@ -362,24 +341,115 @@ public open class DefaultNotificationHandler( .setSmallIcon(notificationIconRes) .also { // If the intent is configured, clicking the notification will return to the call - if (ongoingCallIntent != null) { - it.setContentIntent(ongoingCallIntent) + if (onClickIntent != null) { + it.setContentIntent(onClickIntent) } else { logger.w { "Ongoing intent is null click on the ongoing call notification will not work." } } } .setContentTitle( - application.getString(R.string.stream_video_ongoing_call_notification_title), + if (isOutgoingCall) { + application.getString(R.string.stream_video_outgoing_call_notification_title) + } else { + application.getString(R.string.stream_video_ongoing_call_notification_title) + }, ) .setContentText( application.getString(R.string.stream_video_ongoing_call_notification_description), ) .setAutoCancel(false) .setOngoing(true) - .addHangupAction(endCallIntent, callDisplayName ?: callId.toString()) + .addHangUpAction( + hangUpIntent, + callDisplayName ?: application.getString( + R.string.stream_video_ongoing_call_notification_title, + ), + remoteParticipantCount, + ) .build() } + override fun getNotificationUpdates( + coroutineScope: CoroutineScope, + call: Call, + localUser: User, + onUpdate: (Notification) -> Unit, + ) { + coroutineScope.launch { + var latestRemoteParticipantCount = -1 + + // Monitor call state and remote participants + combine( + call.state.ringingState, + call.state.members, + call.state.remoteParticipants, + ) { ringingState, members, remoteParticipants -> + Triple(ringingState, members, remoteParticipants) + } + .distinctUntilChanged() + .filter { it.first is RingingState.Active || it.first is RingingState.Outgoing } + .collectLatest { state -> + val ringingState = state.first + val members = state.second + val remoteParticipants = state.third + + if (ringingState is RingingState.Outgoing) { + val remoteMembersCount = members.size - 1 + + val callDisplayName = if (remoteMembersCount != 1) { + application.getString( + R.string.stream_video_outgoing_call_notification_title, + ) + } else { + members.firstOrNull { member -> + member.user.id != localUser.id + }?.user?.name ?: "Unknown" + } + + getOngoingCallNotification( + callId = StreamCallId.fromCallCid(call.cid), + callDisplayName = callDisplayName, + isOutgoingCall = true, + remoteParticipantCount = remoteMembersCount, + )?.let { + onUpdate(it) + } + } else if (ringingState is RingingState.Active) { + // If number of remote participants increased or decreased + if (remoteParticipants.size != latestRemoteParticipantCount) { + latestRemoteParticipantCount = remoteParticipants.size + + val callDisplayName = if (remoteParticipants.isEmpty()) { + // If no remote participants, get simple call notification title + application.getString( + R.string.stream_video_ongoing_call_notification_title, + ) + } else { + if (remoteParticipants.size > 1) { + // If more than 1 remote participant, get group call notification title + application.getString( + R.string.stream_video_ongoing_group_call_notification_title, + ) + } else { + // If 1 remote participant, get the name of the remote participant + remoteParticipants.firstOrNull()?.name?.value ?: "Unknown" + } + } + + // Use latest call display name in notification + getOngoingCallNotification( + callId = StreamCallId.fromCallCid(call.cid), + callDisplayName = callDisplayName, + remoteParticipantCount = remoteParticipants.size, + )?.let { + onUpdate(it) + } + } + } + } + } + } + private fun maybeCreateChannel( channelId: String, context: Context, @@ -436,25 +506,6 @@ public open class DefaultNotificationHandler( } } - private fun showIncomingCallNotification( - fullScreenPendingIntent: PendingIntent, - acceptCallPendingIntent: PendingIntent, - rejectCallPendingIntent: PendingIntent, - callDisplayName: String, - notificationId: Int = INCOMING_CALL_NOTIFICATION_ID, - ) { - showNotification(notificationId) { - priority = NotificationCompat.PRIORITY_HIGH - setContentTitle("Incoming call") - setContentText(callDisplayName) - setOngoing(false) - setContentIntent(fullScreenPendingIntent) - setFullScreenIntent(fullScreenPendingIntent, true) - setCategory(NotificationCompat.CATEGORY_CALL) - addCallActions(acceptCallPendingIntent, rejectCallPendingIntent, callDisplayName) - } - } - @SuppressLint("MissingPermission") private fun showNotification( notificationId: Int, @@ -474,17 +525,37 @@ public open class DefaultNotificationHandler( .build() } - private fun NotificationCompat.Builder.addHangupAction( - rejectCallPendingIntent: PendingIntent, + private fun NotificationCompat.Builder.addHangUpAction( + hangUpIntent: PendingIntent, callDisplayName: String, + remoteParticipantCount: Int, ): NotificationCompat.Builder = apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setStyle( CallStyle.forOngoingCall( Person.Builder() .setName(callDisplayName) + .apply { + if (remoteParticipantCount == 0) { + // Just one user in the call + setIcon( + IconCompat.createWithResource( + application, + R.drawable.stream_video_ic_user, + ), + ) + } else if (remoteParticipantCount > 1) { + // More than one user in the call + setIcon( + IconCompat.createWithResource( + application, + R.drawable.stream_video_ic_user_group, + ), + ) + } + } .build(), - rejectCallPendingIntent, + hangUpIntent, ), ) } else { @@ -492,7 +563,7 @@ public open class DefaultNotificationHandler( NotificationCompat.Action.Builder( null, application.getString(R.string.stream_video_call_notification_action_leave), - rejectCallPendingIntent, + hangUpIntent, ).build(), ) } @@ -501,13 +572,23 @@ public open class DefaultNotificationHandler( private fun NotificationCompat.Builder.addCallActions( acceptCallPendingIntent: PendingIntent, rejectCallPendingIntent: PendingIntent, - callDisplayName: String, + callDisplayName: String?, ): NotificationCompat.Builder = apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setStyle( CallStyle.forIncomingCall( Person.Builder() - .setName(callDisplayName) + .setName(callDisplayName ?: "Unknown") + .apply { + if (callDisplayName == null) { + setIcon( + IconCompat.createWithResource( + application, + R.drawable.stream_video_ic_user, + ), + ) + } + } .build(), rejectCallPendingIntent, acceptCallPendingIntent, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt index b9b7a41148..5926298082 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt @@ -18,23 +18,47 @@ package io.getstream.video.android.core.notifications import android.app.Notification import io.getstream.android.push.permissions.NotificationPermissionHandler +import io.getstream.video.android.core.Call import io.getstream.video.android.core.RingingState import io.getstream.video.android.model.StreamCallId +import io.getstream.video.android.model.User +import kotlinx.coroutines.CoroutineScope public interface NotificationHandler : NotificationPermissionHandler { fun onRingingCall(callId: StreamCallId, callDisplayName: String) fun onMissedCall(callId: StreamCallId, callDisplayName: String) fun onNotification(callId: StreamCallId, callDisplayName: String) fun onLiveCall(callId: StreamCallId, callDisplayName: String) - fun getOngoingCallNotification(callDisplayName: String?, callId: StreamCallId): Notification? + fun getOngoingCallNotification( + callId: StreamCallId, + callDisplayName: String? = null, + isOutgoingCall: Boolean = false, + remoteParticipantCount: Int = 0, + ): Notification? fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, - callDisplayName: String, + callDisplayName: String? = null, shouldHaveContentIntent: Boolean = true, ): Notification? fun getSettingUpCallNotification(): Notification? + /** + * Get subsequent updates to notifications. + * Initially, notifications are posted by one of the other methods, and then this method can be used to re-post them with updated content. + * + * @param coroutineScope Coroutine scope used for the updates. + * @param call The Stream call object. + * @param localUser The local Stream user. + * @param onUpdate Callback to be called when the notification is updated. + */ + fun getNotificationUpdates( + coroutineScope: CoroutineScope, + call: Call, + localUser: User, + onUpdate: (Notification) -> Unit, + ) + companion object { const val ACTION_NOTIFICATION = "io.getstream.video.android.action.NOTIFICATION" const val ACTION_MISSED_CALL = "io.getstream.video.android.action.MISSED_CALL" diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt index f4fa89e3ad..fb35929e08 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt @@ -17,9 +17,12 @@ package io.getstream.video.android.core.notifications.internal import android.app.Notification +import io.getstream.video.android.core.Call import io.getstream.video.android.core.RingingState import io.getstream.video.android.core.notifications.NotificationHandler import io.getstream.video.android.model.StreamCallId +import io.getstream.video.android.model.User +import kotlinx.coroutines.CoroutineScope internal object NoOpNotificationHandler : NotificationHandler { override fun onRingingCall(callId: StreamCallId, callDisplayName: String) { /* NoOp */ } @@ -27,16 +30,24 @@ internal object NoOpNotificationHandler : NotificationHandler { override fun onNotification(callId: StreamCallId, callDisplayName: String) { /* NoOp */ } override fun onLiveCall(callId: StreamCallId, callDisplayName: String) { /* NoOp */ } override fun getOngoingCallNotification( - callDisplayName: String?, callId: StreamCallId, + callDisplayName: String?, + isOutgoingCall: Boolean, + remoteParticipantCount: Int, ): Notification? = null override fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, - callDisplayName: String, + callDisplayName: String?, shouldHaveContentIntent: Boolean, ): Notification? = null override fun getSettingUpCallNotification(): Notification? = null + override fun getNotificationUpdates( + coroutineScope: CoroutineScope, + call: Call, + localUser: User, + onUpdate: (Notification) -> Unit, + ) { /* NoOp */ } override fun onPermissionDenied() { /* NoOp */ } override fun onPermissionGranted() { /* NoOp */ } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index 9beb88a79f..897e0868ab 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -248,8 +248,8 @@ internal open class CallService : Service() { val notificationData: Pair = when (trigger) { TRIGGER_ONGOING_CALL -> Pair( first = streamVideo.getOngoingCallNotification( - callDisplayName = intentCallDisplayName, callId = intentCallId, + callDisplayName = intentCallDisplayName, ), second = intentCallId.hashCode(), ) @@ -258,7 +258,7 @@ internal open class CallService : Service() { first = streamVideo.getRingingCallNotification( ringingState = RingingState.Incoming(), callId = intentCallId, - callDisplayName = intentCallDisplayName!!, + callDisplayName = intentCallDisplayName, shouldHaveContentIntent = streamVideo.state.activeCall.value == null, ), second = INCOMING_CALL_NOTIFICATION_ID, @@ -269,7 +269,7 @@ internal open class CallService : Service() { ringingState = RingingState.Outgoing(), callId = intentCallId, callDisplayName = getString( - R.string.stream_video_ongoing_call_notification_description, + R.string.stream_video_outgoing_call_notification_title, ), ), second = INCOMING_CALL_NOTIFICATION_ID, // Same for incoming and outgoing @@ -331,7 +331,7 @@ internal open class CallService : Service() { } else if (trigger == TRIGGER_OUTGOING_CALL) { if (mediaPlayer == null) mediaPlayer = MediaPlayer() } - observeCallState(intentCallId, streamVideo) + observeCall(intentCallId, streamVideo) registerToggleCameraBroadcastReceiver() return START_NOT_STICKY } @@ -414,8 +414,13 @@ internal open class CallService : Service() { } } - private fun observeCallState(callId: StreamCallId, streamVideo: StreamVideoImpl) { - // Ringing state + private fun observeCall(callId: StreamCallId, streamVideo: StreamVideoImpl) { + observeRingingState(callId, streamVideo) + observeCallEvents(callId, streamVideo) + observeNotificationUpdates(callId, streamVideo) + } + + private fun observeRingingState(callId: StreamCallId, streamVideo: StreamVideoImpl) { serviceScope.launch { val call = streamVideo.call(callId.type, callId.id) call.state.ringingState.collect { @@ -457,37 +462,6 @@ internal open class CallService : Service() { } } } - - // Call state - serviceScope.launch { - val call = streamVideo.call(callId.type, callId.id) - call.subscribe { event -> - logger.i { "Received event in service: $event" } - when (event) { - is CallAcceptedEvent -> { - handleIncomingCallAcceptedByMeOnAnotherDevice( - acceptedByUserId = event.user.id, - myUserId = streamVideo.userId, - callRingingState = call.state.ringingState.value, - ) - } - - is CallRejectedEvent -> { - handleIncomingCallRejectedByMeOrCaller( - rejectedByUserId = event.user.id, - myUserId = streamVideo.userId, - createdByUserId = call.state.createdBy.value?.id, - activeCallExists = streamVideo.state.activeCall.value != null, - ) - } - - is CallEndedEvent -> { - // When call ends for any reason - stopService() - } - } - } - } } private fun playCallSound(@RawRes sound: Int?) { @@ -524,6 +498,38 @@ internal open class CallService : Service() { } } + private fun observeCallEvents(callId: StreamCallId, streamVideo: StreamVideoImpl) { + serviceScope.launch { + val call = streamVideo.call(callId.type, callId.id) + call.subscribe { event -> + logger.i { "Received event in service: $event" } + when (event) { + is CallAcceptedEvent -> { + handleIncomingCallAcceptedByMeOnAnotherDevice( + acceptedByUserId = event.user.id, + myUserId = streamVideo.userId, + callRingingState = call.state.ringingState.value, + ) + } + + is CallRejectedEvent -> { + handleIncomingCallRejectedByMeOrCaller( + rejectedByUserId = event.user.id, + myUserId = streamVideo.userId, + createdByUserId = call.state.createdBy.value?.id, + activeCallExists = streamVideo.state.activeCall.value != null, + ) + } + + is CallEndedEvent -> { + // When call ends for any reason + stopService() + } + } + } + } + } + private fun handleIncomingCallAcceptedByMeOnAnotherDevice(acceptedByUserId: String, myUserId: String, callRingingState: RingingState) { // If accepted event was received, with event user being me, but current device is still ringing, it means the call was accepted on another device if (acceptedByUserId == myUserId && callRingingState is RingingState.Incoming) { @@ -543,6 +549,21 @@ internal open class CallService : Service() { } } + private fun observeNotificationUpdates(callId: StreamCallId, streamVideo: StreamVideoImpl) { + streamVideo.getNotificationUpdates( + serviceScope, + streamVideo.call(callId.type, callId.id), + streamVideo.user, + ) { notification -> + startForegroundWithServiceType( + callId.hashCode(), + notification, + TRIGGER_ONGOING_CALL, + serviceType, + ) + } + } + private fun registerToggleCameraBroadcastReceiver() { serviceScope.launch { if (!isToggleCameraBroadcastReceiverRegistered) { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt index 85db14bd41..cd805daa7f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt @@ -79,4 +79,4 @@ public data class StreamCallId constructor( public fun Intent.streamCallId(key: String): StreamCallId? = IntentCompat.getParcelableExtra(this, key, StreamCallId::class.java) -public fun Intent.streamCallDisplayName(key: String): String = this.getStringExtra(key) ?: "." +public fun Intent.streamCallDisplayName(key: String): String? = this.getStringExtra(key) diff --git a/stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml new file mode 100644 index 0000000000..da19c18bca --- /dev/null +++ b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml new file mode 100644 index 0000000000..064403f90e --- /dev/null +++ b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/stream-video-android-core/src/main/res/values/strings.xml b/stream-video-android-core/src/main/res/values/strings.xml index ef39925f05..ad9ee8f7ab 100644 --- a/stream-video-android-core/src/main/res/values/strings.xml +++ b/stream-video-android-core/src/main/res/values/strings.xml @@ -30,17 +30,18 @@ incoming_calls_low_priority Incoming audio and video call alerts Incoming audio and video call alerts - Incoming call + Incoming call outgoing_calls Outgoing Calls Outgoing call notifications Calling... - There is a call in progress, tap to go back to the call + Tap to go back to the call ongoing_calls Ongoing Calls Ongoing call notifications - Call in progress - There is a call in progress, tap to go back to the call + Call in Progress + Group Call in Progress + Tap to go back to the call call_setup Call Setup Temporary notifications used while setting up calls From b1e411ccc37ad71fbf3e5e16ec7a5d4d5c8e4513 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Sep 2024 13:18:00 +0200 Subject: [PATCH 21/22] Add config to skip notification permission on `build()` (#1186) * Do not request permissions if not allowed by configuration while building the instance. * Spotless and API --- .../api/stream-video-android-core.api | 12 +++++++----- .../android/core/notifications/NotificationConfig.kt | 1 + .../internal/StreamNotificationManager.kt | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 833485348b..2b05f0c989 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -4158,19 +4158,21 @@ public final class io/getstream/video/android/core/notifications/DefaultNotifica public final class io/getstream/video/android/core/notifications/NotificationConfig { public fun ()V - public fun (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;Z)V - public synthetic fun (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;Lkotlin/jvm/functions/Function0;Z)V + public synthetic fun (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;Lkotlin/jvm/functions/Function0;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/util/List; public final fun component2 ()Lkotlin/jvm/functions/Function0; public final fun component3 ()Lio/getstream/video/android/core/notifications/NotificationHandler; - public final fun component4 ()Z - public final fun copy (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;Z)Lio/getstream/video/android/core/notifications/NotificationConfig; - public static synthetic fun copy$default (Lio/getstream/video/android/core/notifications/NotificationConfig;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZILjava/lang/Object;)Lio/getstream/video/android/core/notifications/NotificationConfig; + public final fun component4 ()Lkotlin/jvm/functions/Function0; + public final fun component5 ()Z + public final fun copy (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;Lkotlin/jvm/functions/Function0;Z)Lio/getstream/video/android/core/notifications/NotificationConfig; + public static synthetic fun copy$default (Lio/getstream/video/android/core/notifications/NotificationConfig;Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;Lkotlin/jvm/functions/Function0;ZILjava/lang/Object;)Lio/getstream/video/android/core/notifications/NotificationConfig; public fun equals (Ljava/lang/Object;)Z public final fun getHideRingingNotificationInForeground ()Z public final fun getNotificationHandler ()Lio/getstream/video/android/core/notifications/NotificationHandler; public final fun getPushDeviceGenerators ()Ljava/util/List; public final fun getRequestPermissionOnAppLaunch ()Lkotlin/jvm/functions/Function0; + public final fun getRequestPermissionOnDeviceRegistration ()Lkotlin/jvm/functions/Function0; public fun hashCode ()I public fun toString ()Ljava/lang/String; } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationConfig.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationConfig.kt index bfede952e9..79370a887f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationConfig.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationConfig.kt @@ -23,6 +23,7 @@ public data class NotificationConfig( val pushDeviceGenerators: List = emptyList(), val requestPermissionOnAppLaunch: () -> Boolean = { true }, val notificationHandler: NotificationHandler = NoOpNotificationHandler, + val requestPermissionOnDeviceRegistration: () -> Boolean = { true }, /** * Set this to true if you want to make the ringing notifications as low-priority * in case the application is in foreground. This will prevent the notification from diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/StreamNotificationManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/StreamNotificationManager.kt index 896bfc07a4..949d51215f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/StreamNotificationManager.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/StreamNotificationManager.kt @@ -64,7 +64,9 @@ internal class StreamNotificationManager private constructor( logger.d { "[registerPushDevice] pushDevice gnerated: $generatedDevice" } scope.launch { createDevice(generatedDevice) } } - notificationPermissionManager?.start() + if (notificationConfig.requestPermissionOnDeviceRegistration()) { + notificationPermissionManager?.start() + } } } From b7391b450293928a282333472588300289242c03 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 23 Sep 2024 13:32:04 +0200 Subject: [PATCH 22/22] Improve which participant is shown in PiP, allow for mirroring the `me` video stream for better experience. (#1185) * Better determine the participant to show in PiP, improve video orientation for `me` participant. * Api * Make the `mirror` property a `val` * Spotless and API --- .../api/stream-video-android-ui-compose.api | 54 +++++++++++++++ .../components/call/activecall/CallContent.kt | 36 +++++++--- .../call/renderer/ParticipantVideo.kt | 15 +++- .../ui/components/video/VideoRenderer.kt | 69 +++++++++++++------ .../video/config/VideoRendererConfig.kt | 58 ++++++++++++++++ 5 files changed, 199 insertions(+), 33 deletions(-) create mode 100644 stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig.kt 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 5370b2c7b7..d96f8152d0 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 @@ -1857,6 +1857,7 @@ public final class io/getstream/video/android/compose/ui/components/video/Compos } public final class io/getstream/video/android/compose/ui/components/video/VideoRendererKt { + public static final fun VideoRenderer (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/ParticipantState$Media;Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V public static final fun VideoRenderer (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/ParticipantState$Media;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } @@ -1878,3 +1879,56 @@ public final class io/getstream/video/android/compose/ui/components/video/VideoS public static fun values ()[Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; } +public final class io/getstream/video/android/compose/ui/components/video/config/ComposableSingletons$VideoRendererConfigKt { + public static final field INSTANCE Lio/getstream/video/android/compose/ui/components/video/config/ComposableSingletons$VideoRendererConfigKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function3; +} + +public final class io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig { + public static final field $stable I + public fun ()V + public fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)V + public synthetic fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public final fun component3 ()Lkotlin/jvm/functions/Function3; + public final fun copy (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig;ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getFallbackContent ()Lkotlin/jvm/functions/Function3; + public final fun getMirrorStream ()Z + public final fun getScalingType ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope { + public static final field $stable I + public fun ()V + public fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)V + public synthetic fun (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public final fun component3 ()Lkotlin/jvm/functions/Function3; + public final fun copy (ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope; + public static synthetic fun copy$default (Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope;ZLio/getstream/video/android/compose/ui/components/video/VideoScalingType;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigCreationScope; + public fun equals (Ljava/lang/Object;)Z + public final fun getFallbackContent ()Lkotlin/jvm/functions/Function3; + public final fun getMirrorStream ()Z + public final fun getVideoScalingType ()Lio/getstream/video/android/compose/ui/components/video/VideoScalingType; + public fun hashCode ()I + public final fun setFallbackContent (Lkotlin/jvm/functions/Function3;)V + public final fun setMirrorStream (Z)V + public final fun setVideoScalingType (Lio/getstream/video/android/compose/ui/components/video/VideoScalingType;)V + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfigKt { + public static final fun videoRenderConfig (Lkotlin/jvm/functions/Function1;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; + public static synthetic fun videoRenderConfig$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig; +} + diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt index b54f34d2a7..bff570d7ab 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/activecall/CallContent.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -254,19 +255,36 @@ internal fun DefaultPictureInPictureContent(call: Call) { video = video?.value, ) } else { - val activeSpeakers by call.state.activeSpeakers.collectAsStateWithLifecycle() val me by call.state.me.collectAsStateWithLifecycle() + val participants by call.state.participants.collectAsStateWithLifecycle() + val notMeOfTwo by remember { + // Special case where there are only two participants to take always the other participant, + // regardless of video track. + derivedStateOf { + participants.takeIf { + it.size == 2 + }?.firstOrNull { it.sessionId != me?.sessionId } + } + } + val activeSpeakers by call.state.activeSpeakers.collectAsStateWithLifecycle() + val dominantSpeaker by call.state.dominantSpeaker.collectAsStateWithLifecycle() + val notMeActiveOrDominant by remember { + derivedStateOf { + val activeNotMe = activeSpeakers.firstOrNull { + it.sessionId != me?.sessionId + } + val dominantNotMe = dominantSpeaker?.takeUnless { + it.sessionId == me?.sessionId + } - if (activeSpeakers.isNotEmpty()) { - ParticipantVideo( - call = call, - participant = activeSpeakers.first(), - style = RegularVideoRendererStyle(labelPosition = Alignment.BottomStart), - ) - } else if (me != null) { + activeNotMe ?: dominantNotMe + } + } + val participantToShow = notMeOfTwo ?: notMeActiveOrDominant ?: me + if (participantToShow != null) { ParticipantVideo( call = call, - participant = me!!, + participant = participantToShow, style = RegularVideoRendererStyle(labelPosition = Alignment.BottomStart), ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt index 40849aa16c..53496d75fe 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/renderer/ParticipantVideo.kt @@ -82,7 +82,9 @@ import io.getstream.video.android.compose.ui.components.indicator.GenericIndicat import io.getstream.video.android.compose.ui.components.indicator.NetworkQualityIndicator import io.getstream.video.android.compose.ui.components.indicator.SoundIndicator import io.getstream.video.android.compose.ui.components.video.VideoRenderer +import io.getstream.video.android.compose.ui.components.video.config.videoRenderConfig import io.getstream.video.android.core.Call +import io.getstream.video.android.core.CameraDirection import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.core.model.NetworkQuality import io.getstream.video.android.core.model.Reaction @@ -246,11 +248,20 @@ public fun ParticipantVideoRenderer( } val video by participant.video.collectAsStateWithLifecycle() - + val cameraDirection by call.camera.direction.collectAsStateWithLifecycle() + val me by call.state.me.collectAsStateWithLifecycle() + val mirror by remember { + derivedStateOf { + participant.sessionId == me?.sessionId && cameraDirection == CameraDirection.Front + } + } VideoRenderer( call = call, video = video, - videoFallbackContent = videoFallbackContent, + videoRendererConfig = videoRenderConfig { + mirrorStream = mirror + this.fallbackContent = videoFallbackContent + }, ) } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt index e9f0da894c..4077f9e3c5 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/VideoRenderer.kt @@ -46,6 +46,8 @@ import androidx.compose.ui.viewinterop.AndroidView import io.getstream.log.StreamLog import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.compose.ui.components.video.VideoScalingType.Companion.toCommonScalingType +import io.getstream.video.android.compose.ui.components.video.config.VideoRendererConfig +import io.getstream.video.android.compose.ui.components.video.config.videoRenderConfig import io.getstream.video.android.core.Call import io.getstream.video.android.core.ParticipantState import io.getstream.video.android.core.model.MediaTrack @@ -55,28 +57,12 @@ import io.getstream.video.android.mock.previewCall import io.getstream.video.android.ui.common.renderer.StreamVideoTextureViewRenderer import io.getstream.webrtc.android.ui.VideoTextureViewRenderer -/** - * Renders a single video track based on the call state. - * - * @param call The call state that contains all the tracks and participants. - * @param video A media contains a video track or an audio track to be rendered. - * @param modifier Modifier for styling. - * @param videoScalingType Setup the video scale type of this renderer. - * @param videoFallbackContent Content is shown the video track is failed to load or not available. - * @param onRendered An interface that will be invoked when the video is rendered. - */ @Composable public fun VideoRenderer( + modifier: Modifier = Modifier, call: Call, video: ParticipantState.Media?, - modifier: Modifier = Modifier, - videoScalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, - videoFallbackContent: @Composable (Call) -> Unit = { - DefaultMediaTrackFallbackContent( - modifier, - call, - ) - }, + videoRendererConfig: VideoRendererConfig = videoRenderConfig(), onRendered: (VideoTextureViewRenderer) -> Unit = {}, ) { if (LocalInspectionMode.current) { @@ -94,7 +80,7 @@ public fun VideoRenderer( } // Show avatar always behind the video. - videoFallbackContent.invoke(call) + videoRendererConfig.fallbackContent.invoke(call) if (video?.enabled == true) { val mediaTrack = video.track @@ -125,7 +111,10 @@ public fun VideoRenderer( trackType = trackType, onRendered = onRendered, ) - setScalingType(scalingType = videoScalingType.toCommonScalingType()) + setMirror(videoRendererConfig.mirrorStream) + setScalingType( + scalingType = videoRendererConfig.scalingType.toCommonScalingType(), + ) setupVideo(mediaTrack, this) view = this @@ -139,6 +128,43 @@ public fun VideoRenderer( } } +/** + * Renders a single video track based on the call state. + * + * @param call The call state that contains all the tracks and participants. + * @param video A media contains a video track or an audio track to be rendered. + * @param modifier Modifier for styling. + * @param videoScalingType Setup the video scale type of this renderer. + * @param videoFallbackContent Content is shown the video track is failed to load or not available. + * @param onRendered An interface that will be invoked when the video is rendered. + */ +@Deprecated("Use VideoRenderer which accepts `videoConfig` instead.") +@Composable +public fun VideoRenderer( + call: Call, + video: ParticipantState.Media?, + modifier: Modifier = Modifier, + videoScalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, + videoFallbackContent: @Composable (Call) -> Unit = { + DefaultMediaTrackFallbackContent( + modifier, + call, + ) + }, + onRendered: (VideoTextureViewRenderer) -> Unit = {}, +) { + VideoRenderer( + call = call, + video = video, + modifier = modifier, + videoRendererConfig = videoRenderConfig { + this.videoScalingType = videoScalingType + this.fallbackContent = videoFallbackContent + }, + onRendered = onRendered, + ) +} + private fun cleanTrack( view: VideoTextureViewRenderer?, mediaTrack: MediaTrack?, @@ -154,7 +180,6 @@ private fun cleanTrack( } } } - private fun setupVideo( mediaTrack: MediaTrack?, renderer: VideoTextureViewRenderer, @@ -171,7 +196,7 @@ private fun setupVideo( } @Composable -private fun DefaultMediaTrackFallbackContent( +internal fun DefaultMediaTrackFallbackContent( modifier: Modifier, call: Call, ) { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig.kt new file mode 100644 index 0000000000..5997a627ac --- /dev/null +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/video/config/VideoRendererConfig.kt @@ -0,0 +1,58 @@ +/* + * 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.compose.ui.components.video.config + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import io.getstream.video.android.compose.ui.components.video.DefaultMediaTrackFallbackContent +import io.getstream.video.android.compose.ui.components.video.VideoScalingType +import io.getstream.video.android.core.Call + +@Immutable +public data class VideoRendererConfig( + val mirrorStream: Boolean = false, + val scalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, + val fallbackContent: @Composable (Call) -> Unit = {}, +) + +@Immutable +public data class VideoRendererConfigCreationScope( + public var mirrorStream: Boolean = false, + public var videoScalingType: VideoScalingType = VideoScalingType.SCALE_ASPECT_FILL, + public var fallbackContent: @Composable (Call) -> Unit = { + DefaultMediaTrackFallbackContent( + modifier = Modifier, + call = it, + ) + }, +) + +/** + * A builder method for a video renderer config. + */ +public inline fun videoRenderConfig( + block: VideoRendererConfigCreationScope.() -> Unit = {}, +): VideoRendererConfig { + val scope = VideoRendererConfigCreationScope() + scope.block() + return VideoRendererConfig( + mirrorStream = scope.mirrorStream, + scalingType = scope.videoScalingType, + fallbackContent = scope.fallbackContent, + ) +}