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 a380701253b..7df7ebe46c9 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 @@ -86,7 +86,7 @@ class ClientState(private val client: StreamVideo) { public val activeCall: StateFlow = _activeCall public val callConfigRegistry = (client as StreamVideoClient).callServiceConfigRegistry - private val serviceLauncher = ServiceLauncher(client.context) + internal val serviceLauncher = ServiceLauncher(client.context, streamVideoClient) internal val clientEventReporter = (client as StreamVideoClient).analytics.clientEventReporter @@ -293,13 +293,11 @@ class ClientState(private val client: StreamVideo) { CallService.TRIGGER_ONGOING_CALL -> serviceLauncher.showOnGoingCall( call, trigger, - streamVideoClient, ) CallService.TRIGGER_OUTGOING_CALL -> serviceLauncher.showOutgoingCall( call, trigger, - streamVideoClient, ) else -> {} @@ -312,10 +310,7 @@ class ClientState(private val client: StreamVideo) { internal fun maybeStopForegroundService(call: Call) { val callConfig = streamVideoClient.callServiceConfigRegistry.get(call.type) if (callConfig.runCallServiceInForeground) { - val context = streamVideoClient.context - logger.d { "Building stop intent for call_id: ${call.cid}" } - val serviceLauncher = ServiceLauncher(context) serviceLauncher.stopService(call) } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ExternalCallRejectionHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ExternalCallRejectionHandler.kt index 23a9fa83953..f2a45e4af02 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ExternalCallRejectionHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ExternalCallRejectionHandler.kt @@ -22,7 +22,6 @@ import android.os.Bundle import io.getstream.log.taggedLogger import io.getstream.result.Result import io.getstream.video.android.core.model.RejectReason -import io.getstream.video.android.core.notifications.internal.service.ServiceLauncher import io.getstream.video.android.core.notifications.internal.telecom.TelecomCallController import io.getstream.video.android.model.StreamCallId @@ -51,9 +50,7 @@ internal class ExternalCallRejectionHandler() { } logger.d { "[onRejectCall] source:$source, #ringing; callId: ${call.id}, action: ${intent.action}" } - val serviceLauncher = ServiceLauncher(context) - serviceLauncher.removeIncomingCall( - context, + StreamVideo.instance().state.serviceLauncher.removeIncomingCall( StreamCallId.fromCallCid(call.cid), StreamVideo.instance().state.callConfigRegistry.get(call.type), ) 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 26fafce4db7..16b3ecb4d11 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 @@ -48,7 +48,6 @@ import io.getstream.video.android.core.notifications.NotificationHandler.Compani import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_NOTIFICATION import io.getstream.video.android.core.notifications.dispatchers.DefaultNotificationDispatcher import io.getstream.video.android.core.notifications.dispatchers.NotificationDispatcher -import io.getstream.video.android.core.notifications.internal.service.ServiceLauncher import io.getstream.video.android.core.notifications.medianotifications.MediaNotificationConfig import io.getstream.video.android.core.notifications.medianotifications.MediaNotificationContent import io.getstream.video.android.core.notifications.medianotifications.MediaNotificationVisuals @@ -88,7 +87,6 @@ public open class DefaultNotificationHandler( private val logger by taggedLogger("Call:NotificationHandler") val intentResolver = DefaultStreamIntentResolver(application, DefaultNotificationIntentBundleResolver()) - private val serviceLauncher = ServiceLauncher(application) protected val notificationManager: NotificationManagerCompat by lazy { NotificationManagerCompat.from(application).also { @@ -114,14 +112,12 @@ public open class DefaultNotificationHandler( ) { logger.d { "[onRingingCall] #ringing; callId: ${callId.id}" } val streamVideo = StreamVideo.instance() - serviceLauncher.showIncomingCall( - application, + streamVideo.state.serviceLauncher.showIncomingCall( callId, callDisplayName, streamVideo.state.callConfigRegistry.get(callId.type), isVideo = isVideoCall(callId, payload), payload = payload, - streamVideo, notification = getRingingCallNotification( RingingState.Incoming(), callId, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandler.kt index f6b3c1553d0..4897e25d35b 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandler.kt @@ -57,7 +57,6 @@ import io.getstream.video.android.core.notifications.dispatchers.DefaultNotifica import io.getstream.video.android.core.notifications.dispatchers.NotificationDispatcher import io.getstream.video.android.core.notifications.extractor.DefaultNotificationContentExtractor import io.getstream.video.android.core.notifications.internal.service.CallService.Companion.TRIGGER_INCOMING_CALL -import io.getstream.video.android.core.notifications.internal.service.ServiceLauncher import io.getstream.video.android.core.notifications.style.StyleProvider import io.getstream.video.android.core.utils.BackgroundRestrictions import io.getstream.video.android.core.utils.isAppInForeground @@ -149,7 +148,6 @@ constructor( NotificationPermissionHandler by notificationPermissionHandler { private val logger by taggedLogger("Video:StreamNotificationHandler") - private val serviceLauncher = ServiceLauncher(application) private val styleProvider = StyleProvider(application) private val batteryRestrictions = BackgroundRestrictions(application) @@ -177,14 +175,12 @@ constructor( val canRunService = streamVideo.callServiceConfigRegistry.get(callId.type).runCallServiceInForeground if (canRunService) { - serviceLauncher.showIncomingCall( - application, + streamVideo.state.serviceLauncher.showIncomingCall( callId, callDisplayName, streamVideo.state.callConfigRegistry.get(callId.type), isVideo = isVideoCall(callId, payload), payload = payload, - streamVideo, notification = getRingingCallNotification( RingingState.Incoming(), callId, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/JetpackTelecomRepositoryProvider.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/JetpackTelecomRepositoryProvider.kt index a0482bd97cd..7909366bd84 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/JetpackTelecomRepositoryProvider.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/JetpackTelecomRepositoryProvider.kt @@ -16,7 +16,6 @@ package io.getstream.video.android.core.notifications.internal.service -import android.content.Context import android.os.Build import androidx.annotation.RequiresApi import androidx.core.telecom.CallsManager @@ -25,20 +24,30 @@ import io.getstream.video.android.core.notifications.internal.telecom.IncomingCa import io.getstream.video.android.core.notifications.internal.telecom.jetpack.JetpackTelecomRepository import io.getstream.video.android.model.StreamCallId -internal class JetpackTelecomRepositoryProvider(private val context: Context) { +internal class JetpackTelecomRepositoryProvider(private val streamVideo: StreamVideo) { + + private val context = streamVideo.context.applicationContext ?: streamVideo.context + private val registrationLock = Any() + private lateinit var callsManager: CallsManager @RequiresApi(Build.VERSION_CODES.O) fun get(callId: StreamCallId): JetpackTelecomRepository { - val callsManager = CallsManager(context).apply { - registerAppWithTelecom( - capabilities = CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING and - CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING, - ) - } - - val streamVideo = StreamVideo.instance() + val callsManager = getOrRegisterCallsManager() val incomingCallTelecomAction = IncomingCallTelecomAction(streamVideo) return JetpackTelecomRepository(callsManager, callId, incomingCallTelecomAction) } + + @RequiresApi(Build.VERSION_CODES.O) + private fun getOrRegisterCallsManager(): CallsManager = synchronized(registrationLock) { + if (!::callsManager.isInitialized) { + callsManager = CallsManager(context).also { manager -> + manager.registerAppWithTelecom( + capabilities = CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING or + CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING, + ) + } + } + callsManager + } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt index 28d4f641213..0ef5abb6566 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt @@ -41,7 +41,6 @@ import androidx.core.content.ContextCompat import androidx.core.net.toUri import io.getstream.log.taggedLogger import io.getstream.video.android.core.Call -import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.core.StreamVideoClient import io.getstream.video.android.core.notifications.NotificationType import io.getstream.video.android.core.notifications.internal.Throttler @@ -57,25 +56,26 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -internal class ServiceLauncher(val context: Context) { +internal class ServiceLauncher( + val context: Context, + private val client: StreamVideoClient, +) { private val logger by taggedLogger("ServiceTriggers") private val serviceIntentBuilder = ServiceIntentBuilder() private val incomingCallPresenter = IncomingCallPresenter(serviceIntentBuilder) private val telecomHelper = TelecomHelper() private val telecomPermissions = TelecomPermissions() - private val jetpackTelecomRepositoryProvider = JetpackTelecomRepositoryProvider(context) + private val jetpackTelecomRepositoryProvider = JetpackTelecomRepositoryProvider(client) private val throttler = Throttler() @SuppressLint("MissingPermission", "NewApi") fun showIncomingCall( - context: Context, callId: StreamCallId, callDisplayName: String?, callServiceConfiguration: CallServiceConfig, isVideo: Boolean, payload: Map, - streamVideo: StreamVideo, notification: Notification?, ) { val result = incomingCallPresenter.showIncomingCall( @@ -90,15 +90,15 @@ internal class ServiceLauncher(val context: Context) { if (telecomHelper.canUseJetpackTelecom()) { when (result) { ShowIncomingCallResult.FG_SERVICE -> { - updateIncomingCallNotification(notification, streamVideo, callId) + updateIncomingCallNotification(notification, callId) val jetpackTelecomRepository = jetpackTelecomRepositoryProvider.get(callId) - val appSchema = (streamVideo as StreamVideoClient).telecomConfig?.schema + val appSchema = client.telecomConfig?.schema val addressUri = "$appSchema:${callId.id}".toUri() val formattedCallDisplayName = callDisplayName?.takeIf { it.isNotBlank() } ?: DEFAULT_CALL_TEXT - val call = streamVideo.call(callId.type, callId.id) + val call = client.call(callId.type, callId.id) call.state.jetpackTelecomRepository = (jetpackTelecomRepository) @@ -117,14 +117,12 @@ internal class ServiceLauncher(val context: Context) { } } - fun showOnGoingCall(call: Call, trigger: String, streamVideo: StreamVideo) { - val client = streamVideo as StreamVideoClient + fun showOnGoingCall(call: Call, trigger: String) { val callConfig = client.callServiceConfigRegistry.get(call.type) if (!callConfig.runCallServiceInForeground) { return } val callId = StreamCallId.fromCallCid(call.cid) - val context = client.context val serviceIntent = ServiceIntentBuilder().buildStartIntent( context, StartServiceParam( @@ -137,8 +135,8 @@ internal class ServiceLauncher(val context: Context) { } @SuppressLint("NewApi") - fun showOutgoingCall(call: Call, trigger: String, streamVideo: StreamVideo) { - val callConfig = (streamVideo as StreamVideoClient).callServiceConfigRegistry.get(call.type) + fun showOutgoingCall(call: Call, trigger: String) { + val callConfig = client.callServiceConfigRegistry.get(call.type) if (!callConfig.runCallServiceInForeground) { return } @@ -165,7 +163,7 @@ internal class ServiceLauncher(val context: Context) { if (telecomHelper.canUseJetpackTelecom()) { val jetpackTelecomRepository = jetpackTelecomRepositoryProvider.get(callId) - val appSchema = streamVideo.telecomConfig?.schema + val appSchema = client.telecomConfig?.schema val addressUri = "$appSchema:${callId.id}".toUri() val formattedCallDisplayName = callDisplayName?.takeIf { it.isNotBlank() } ?: DEFAULT_CALL_TEXT @@ -199,18 +197,16 @@ internal class ServiceLauncher(val context: Context) { */ private fun updateIncomingCallNotification( notification: Notification?, - streamVideo: StreamVideo, callId: StreamCallId, ) { notification?.let { val notificationId = callId.getNotificationId(NotificationType.Incoming) - streamVideo.call(callId.type, callId.id) + client.call(callId.type, callId.id) .state.updateNotification(notificationId, notification) } } fun removeIncomingCall( - context: Context, callId: StreamCallId, config: CallServiceConfig = DefaultCallConfigurations.default, ) { @@ -249,25 +245,18 @@ internal class ServiceLauncher(val context: Context) { private fun stopCallServiceInternal(call: Call) { logger.d { "[stopCallServiceInternal]" } - val streamVideo = StreamVideo.instanceOrNull() as? StreamVideoClient - streamVideo?.let { streamVideoClient -> - val callConfig = streamVideoClient.callServiceConfigRegistry.get(call.type) - if (callConfig.runCallServiceInForeground) { - val context = streamVideoClient.context - - val serviceIntent = serviceIntentBuilder.buildStopIntent( - context, - StopServiceParam(call, callConfig), - ) - serviceIntent?.let { - logger.d { - "Building stop intent, class: ${serviceIntent.component?.className} for call_id: ${call.cid}" - } - serviceIntent.extras?.let { - logBundle(it) - } - context.startService(serviceIntent) + val callConfig = client.callServiceConfigRegistry.get(call.type) + if (callConfig.runCallServiceInForeground) { + val serviceIntent = serviceIntentBuilder.buildStopIntent( + context, + StopServiceParam(call, callConfig), + ) + serviceIntent?.let { + logger.d { + "Building stop intent, class: ${serviceIntent.component?.className} for call_id: ${call.cid}" } + serviceIntent.extras?.let { logBundle(it) } + context.startService(serviceIntent) } } } diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncherTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncherTest.kt index dd4d797acaa..46ab43c0044 100644 --- a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncherTest.kt +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncherTest.kt @@ -123,7 +123,7 @@ class ServiceLauncherTest { every { StreamVideo.instance() } returns streamVideo every { jetpackTelecomRepositoryProvider.get(any()) } returns jetpackTelecomRepository - serviceLauncher = ServiceLauncher(context) + serviceLauncher = ServiceLauncher(context, streamVideo) } @After @@ -153,13 +153,11 @@ class ServiceLauncherTest { } returns mockk() serviceLauncher.showIncomingCall( - context = context, callId = callId, callDisplayName = "Test Caller", callServiceConfiguration = callServiceConfig, isVideo = true, payload = emptyMap(), - streamVideo = streamVideo, notification = notification, ) testScheduler.advanceUntilIdle() @@ -172,13 +170,11 @@ class ServiceLauncherTest { every { anyConstructed().canUseTelecom(any(), any()) } returns false serviceLauncher.showIncomingCall( - context, callId, "Test Caller", callServiceConfig, isVideo = false, payload = emptyMap(), - streamVideo = streamVideo, notification = notification, ) @@ -204,7 +200,7 @@ class ServiceLauncherTest { every { call.cid } returns "default:cid-123" every { call.isVideoEnabled() } returns true - serviceLauncher.showOutgoingCall(call, "outgoing_call", streamVideo) + serviceLauncher.showOutgoingCall(call, "outgoing_call") verify { ContextCompat.startForegroundService(context, any()) } @@ -229,7 +225,7 @@ class ServiceLauncherTest { every { call.isVideoEnabled() } returns true every { anyConstructed().canUseTelecom(any(), any()) } returns false - serviceLauncher.showOutgoingCall(call, "outgoing_call", streamVideo) + serviceLauncher.showOutgoingCall(call, "outgoing_call") coVerify(exactly = 0) { jetpackTelecomRepository.registerCall(any(), any(), any(), any()) } }