From 62e41d15c7fd563e1c59ea52b214306256a5eed9 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Tue, 27 Aug 2024 15:04:17 +0200 Subject: [PATCH] [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 @@ + + + + +