Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android.core.notifications.internal.service

import android.app.Notification
import io.getstream.video.android.model.StreamCallId

internal data class IncomingCallRequest(
val callId: StreamCallId,
val callDisplayName: String?,
val callServiceConfiguration: CallServiceConfig,
val isVideo: Boolean,
val payload: Map<String, Any?>,
val notification: Notification?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,19 @@ package io.getstream.video.android.core.notifications.internal.service
import android.annotation.SuppressLint
import android.app.Notification
import android.os.Bundle
import androidx.core.app.NotificationManagerCompat
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.StreamVideoClient
import io.getstream.video.android.core.notifications.NotificationType
import io.getstream.video.android.core.notifications.internal.Throttler
import io.getstream.video.android.core.notifications.internal.VideoPushDelegate.Companion.DEFAULT_CALL_TEXT
import io.getstream.video.android.core.notifications.internal.service.CallService.Companion.TRIGGER_REMOVE_INCOMING_CALL
import io.getstream.video.android.core.notifications.internal.service.incomingcallcoordinator.IncomingCallCoordinator
import io.getstream.video.android.core.notifications.internal.service.incomingcallcoordinator.PreAndroid17IncomingCallCoordinator
import io.getstream.video.android.core.notifications.internal.telecom.TelecomHelper
import io.getstream.video.android.core.notifications.internal.telecom.TelecomPermissions
import io.getstream.video.android.core.notifications.internal.telecom.jetpack.TelecomCall
import io.getstream.video.android.core.notifications.internal.telecom.jetpack.TelecomCallAction
import io.getstream.video.android.core.utils.safeCallWithResult
import io.getstream.video.android.model.StreamCallId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
Expand All @@ -66,6 +64,16 @@ internal class ServiceLauncher(private val client: StreamVideoClient) {
private val telecomPermissions = TelecomPermissions()
private val jetpackTelecomRepositoryProvider = JetpackTelecomRepositoryProvider(client)
private val throttler = Throttler()
private val incomingCallCoordinator: IncomingCallCoordinator =
PreAndroid17IncomingCallCoordinator(
context = context,
client = client,
incomingCallPresenter = incomingCallPresenter,
serviceIntentBuilder = serviceIntentBuilder,
telecomPermissions = telecomPermissions,
telecomHelper = telecomHelper,
jetpackTelecomRepositoryProvider = jetpackTelecomRepositoryProvider,
)

@SuppressLint("MissingPermission", "NewApi")
fun showIncomingCall(
Expand All @@ -76,43 +84,16 @@ internal class ServiceLauncher(private val client: StreamVideoClient) {
payload: Map<String, Any?>,
notification: Notification?,
) {
val result = incomingCallPresenter.showIncomingCall(
context,
callId,
callDisplayName,
callServiceConfiguration,
notification,
incomingCallCoordinator.showIncomingCall(
IncomingCallRequest(
callId = callId,
callDisplayName = callDisplayName,
callServiceConfiguration = callServiceConfiguration,
isVideo = isVideo,
payload = payload,
notification = notification,
),
)
logger.d { "[showIncomingCall] service start result: $result" }
if (telecomPermissions.canUseTelecom(callServiceConfiguration, context)) {
if (telecomHelper.canUseJetpackTelecom()) {
when (result) {
ShowIncomingCallResult.FG_SERVICE -> {
updateIncomingCallNotification(notification, callId)

val jetpackTelecomRepository = jetpackTelecomRepositoryProvider.get(callId)

val appSchema = client.telecomConfig?.schema
val addressUri = "$appSchema:${callId.id}".toUri()
val formattedCallDisplayName = callDisplayName?.takeIf { it.isNotBlank() } ?: DEFAULT_CALL_TEXT

val call = client.call(callId.type, callId.id)

call.state.jetpackTelecomRepository = (jetpackTelecomRepository)

call.scope.launch {
jetpackTelecomRepository.registerCall(
formattedCallDisplayName,
addressUri,
true,
isVideo,
)
}
}
else -> {}
}
}
}
}

fun showOnGoingCall(call: Call, trigger: String) {
Expand Down Expand Up @@ -187,45 +168,11 @@ internal class ServiceLauncher(private val client: StreamVideoClient) {
}
}

/**
* Because we need to retrieve the notification
* in [io.getstream.video.android.core.notifications.internal.telecom.connection.SuccessIncomingTelecomConnection]
*/
private fun updateIncomingCallNotification(
notification: Notification?,
callId: StreamCallId,
) {
notification?.let {
val notificationId = callId.getNotificationId(NotificationType.Incoming)
client.call(callId.type, callId.id)
.state.updateNotification(notificationId, notification)
}
}

fun removeIncomingCall(
callId: StreamCallId,
config: CallServiceConfig = DefaultCallConfigurations.default,
) {
safeCallWithResult {
context.startService(
serviceIntentBuilder.buildStartIntent(
context,
StartServiceParam(
callId,
TRIGGER_REMOVE_INCOMING_CALL,
callServiceConfiguration = config,
),
),
)!!
}.onError {
logger.d {
"[removeIncomingCall] notificationId: ${callId.getNotificationId(
NotificationType.Incoming,
)}"
}
NotificationManagerCompat.from(context)
.cancel(callId.getNotificationId(NotificationType.Incoming))
}
incomingCallCoordinator.dismissIncomingCall(callId, config)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android.core.notifications.internal.service.incomingcallcoordinator

import io.getstream.video.android.core.notifications.internal.service.CallServiceConfig
import io.getstream.video.android.core.notifications.internal.service.IncomingCallRequest
import io.getstream.video.android.model.StreamCallId

internal interface IncomingCallCoordinator {

fun showIncomingCall(request: IncomingCallRequest)

fun dismissIncomingCall(
callId: StreamCallId,
config: CallServiceConfig,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android.core.notifications.internal.service.incomingcallcoordinator

import android.annotation.SuppressLint
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import io.getstream.log.taggedLogger
import io.getstream.video.android.core.StreamVideoClient
import io.getstream.video.android.core.notifications.NotificationType
import io.getstream.video.android.core.notifications.internal.VideoPushDelegate.Companion.DEFAULT_CALL_TEXT
import io.getstream.video.android.core.notifications.internal.service.CallService.Companion.TRIGGER_REMOVE_INCOMING_CALL
import io.getstream.video.android.core.notifications.internal.service.CallServiceConfig
import io.getstream.video.android.core.notifications.internal.service.IncomingCallPresenter
import io.getstream.video.android.core.notifications.internal.service.IncomingCallRequest
import io.getstream.video.android.core.notifications.internal.service.JetpackTelecomRepositoryProvider
import io.getstream.video.android.core.notifications.internal.service.ServiceIntentBuilder
import io.getstream.video.android.core.notifications.internal.service.ShowIncomingCallResult
import io.getstream.video.android.core.notifications.internal.service.StartServiceParam
import io.getstream.video.android.core.notifications.internal.telecom.TelecomHelper
import io.getstream.video.android.core.notifications.internal.telecom.TelecomPermissions
import io.getstream.video.android.core.utils.safeCallWithResult
import io.getstream.video.android.model.StreamCallId
import kotlinx.coroutines.launch

/** Coordinates the existing incoming-call service and optional Telecom registration path. */
internal class PreAndroid17IncomingCallCoordinator(
private val context: Context,
private val client: StreamVideoClient,
private val incomingCallPresenter: IncomingCallPresenter,
private val serviceIntentBuilder: ServiceIntentBuilder,
private val telecomPermissions: TelecomPermissions,
private val telecomHelper: TelecomHelper,
private val jetpackTelecomRepositoryProvider: JetpackTelecomRepositoryProvider,
) : IncomingCallCoordinator {

private val logger by taggedLogger("PreAndroid17IncomingCallCoordinator")

@SuppressLint("MissingPermission", "NewApi")
override fun showIncomingCall(request: IncomingCallRequest) {
val result = incomingCallPresenter.showIncomingCall(
context = context,
callId = request.callId,
callDisplayName = request.callDisplayName,
callServiceConfiguration = request.callServiceConfiguration,
notification = request.notification,
)
logger.d { "[showIncomingCall] service start result: $result" }

if (result != ShowIncomingCallResult.FG_SERVICE ||
!telecomPermissions.canUseTelecom(request.callServiceConfiguration, context) ||
!telecomHelper.canUseJetpackTelecom()
) {
return
}

updateIncomingCallNotification(request, client)
val jetpackTelecomRepository = jetpackTelecomRepositoryProvider.get(request.callId)
val addressUri = "${client.telecomConfig?.schema}:${request.callId.id}".toUri()
val formattedCallDisplayName = request.callDisplayName
?.takeIf { it.isNotBlank() }
?: DEFAULT_CALL_TEXT
val call = client.call(request.callId.type, request.callId.id)

call.state.jetpackTelecomRepository = jetpackTelecomRepository
call.scope.launch {
jetpackTelecomRepository.registerCall(
displayName = formattedCallDisplayName,
address = addressUri,
isIncoming = true,
isVideoCall = request.isVideo,
)
}
}

override fun dismissIncomingCall(
callId: StreamCallId,
config: CallServiceConfig,
) {
safeCallWithResult {
context.startService(
serviceIntentBuilder.buildStartIntent(
context,
StartServiceParam(
callId = callId,
trigger = TRIGGER_REMOVE_INCOMING_CALL,
callServiceConfiguration = config,
),
),
)!!
}.onError {
val notificationId = callId.getNotificationId(NotificationType.Incoming)
logger.d { "[dismissIncomingCall] notificationId: $notificationId" }
NotificationManagerCompat.from(context).cancel(notificationId)
}
}

private fun updateIncomingCallNotification(
request: IncomingCallRequest,
client: StreamVideoClient,
) {
request.notification?.let { notification ->
val notificationId = request.callId.getNotificationId(NotificationType.Incoming)
client.call(request.callId.type, request.callId.id)
.state.updateNotification(notificationId, notification)
}
}
}
Loading