Skip to content
Merged
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
3 changes: 3 additions & 0 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -13866,6 +13866,8 @@ public final class io/getstream/video/android/core/notifications/NotificationCon
public fun <init> ()V
public fun <init> (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/jvm/functions/Function0;ZZLjava/util/Set;Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/jvm/functions/Function0;ZZLjava/util/Set;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/jvm/functions/Function0;ZZLjava/util/Set;Lkotlin/jvm/functions/Function1;J)V
public synthetic fun <init> (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/jvm/functions/Function0;ZZLjava/util/Set;Lkotlin/jvm/functions/Function1;JILkotlin/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;
Expand All @@ -13881,6 +13883,7 @@ public final class io/getstream/video/android/core/notifications/NotificationCon
public final fun getAutoRegisterPushDevice ()Z
public final fun getEnableCallNotificationUpdates ()Z
public final fun getHideRingingNotificationInForeground ()Z
public final fun getIncomingRingingNotificationUpdateDelayMillis ()J
public final fun getMediaNotificationCallTypes ()Ljava/util/Set;
public final fun getNotificationHandler ()Lio/getstream/video/android/core/notifications/NotificationHandler;
public final fun getNotificationUpdateTriggers ()Lkotlin/jvm/functions/Function1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
import io.getstream.video.android.core.notifications.IncomingNotificationData
import io.getstream.video.android.core.notifications.NotificationType
import io.getstream.video.android.core.notifications.internal.service.CallServiceConfig
import io.getstream.video.android.core.notifications.internal.service.models.ServiceRoute
import io.getstream.video.android.core.notifications.internal.telecom.jetpack.JetpackTelecomRepository
import io.getstream.video.android.core.notifications.internal.telecom.jetpack.TelecomCall
import io.getstream.video.android.core.permission.PermissionRequest
Expand Down Expand Up @@ -836,6 +837,13 @@
@Volatile
internal var callJoinInterceptorProvider: (() -> CallJoinInterceptor?)? = null

internal val incomingRingtoneOwner = MutableStateFlow<IncomingRingtoneOwner>(
IncomingRingtoneOwner.Legacy,
)

Check warning on line 842 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Don't expose mutable flow types.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AaCGw9ZKCF_4VX1ctKvQ&open=AaCGw9ZKCF_4VX1ctKvQ&pullRequest=1809

private val _serviceRoute = MutableStateFlow(ServiceRoute.UNDECIDED)
internal val serviceRoute: StateFlow<ServiceRoute> = _serviceRoute.asStateFlow()

fun handleEvent(event: VideoEvent) {
logger.d { "[handleEvent] ${event::class.java.name.split(".").last()}" }

Expand Down Expand Up @@ -1906,6 +1914,10 @@
this.atomicNotification.set(notification)
}

internal fun updateIncomingRingtoneOwner(incomingRingtoneOwner: IncomingRingtoneOwner) {
this.incomingRingtoneOwner.value = incomingRingtoneOwner
}

@InternalStreamVideoApi
fun setOwnCapabilities(ownCapability: List<OwnCapability>) {
this._ownCapabilities.value = ownCapability
Expand Down Expand Up @@ -1939,6 +1951,10 @@
activeStateGate.cleanup()
cancelTimeout()
}

internal fun updateServiceRoute(route: ServiceRoute) {
_serviceRoute.value = route
}
}

private fun MemberResponse.toMemberState(): MemberState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ 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.telecom.TelecomCallController
import io.getstream.video.android.model.StreamCallId

internal class ExternalCallRejectionHandler() {
private val logger by taggedLogger("CallRejectionHandler")
Expand Down Expand Up @@ -51,7 +50,7 @@ internal class ExternalCallRejectionHandler() {
logger.d { "[onRejectCall] source:$source, #ringing; callId: ${call.id}, action: ${intent.action}" }

StreamVideo.instance().state.serviceLauncher.removeIncomingCall(
StreamCallId.fromCallCid(call.cid),
call,
StreamVideo.instance().state.callConfigRegistry.get(call.type),
)
when (source) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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

/** Identifies which component owns incoming-call ringing. */
internal sealed interface IncomingRingtoneOwner {

/** The incoming-call notification produces the ringtone and vibration. */
data object Notification : IncomingRingtoneOwner

/** The SDK sound and vibration player produces the ringtone and vibration. */
data object Legacy : IncomingRingtoneOwner
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import io.getstream.video.android.core.sounds.defaultResourcesRingingConfig
import io.getstream.video.android.core.sounds.disableVibrationConfig
import io.getstream.video.android.core.sounds.toSounds
import io.getstream.video.android.core.user.StreamUserRepositoryImpl
import io.getstream.video.android.core.utils.isAndroid17OrHigher
import io.getstream.video.android.model.ApiKey
import io.getstream.video.android.model.User
import io.getstream.video.android.model.UserToken
Expand Down Expand Up @@ -318,7 +319,11 @@ public class StreamVideoBuilder @JvmOverloads constructor(
enableStatsCollection = enableStatsReporting,
vibrationConfig = vibrationConfig,
enableStereoForSubscriber = enableStereoForSubscriber,
telecomConfig = telecomConfig,
telecomConfig = telecomConfig ?: if (isAndroid17OrHigher()) {
TelecomConfig(context.packageName)
} else {
null
},
tokenRepository = tokenRepository,
rejectCallWhenBusy = rejectCallWhenBusy,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,15 @@ public open class DefaultNotificationHandler(
streamVideo.state.callConfigRegistry.get(callId.type),
isVideo = isVideoCall(callId, payload),
payload = payload,
notification = getRingingCallNotification(
RingingState.Incoming(),
callId,
callDisplayName,
shouldHaveContentIntent = true,
payload,
),
notificationProvider = {
getRingingCallNotification(
RingingState.Incoming(),
callId,
callDisplayName,
shouldHaveContentIntent = true,
payload,
)
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,35 @@
)
}

/**
* Creates the incoming-call full-screen intent without the notification-dismiss trampoline.
* Android can launch this intent without user interaction, so launching it must not dismiss a
* notification that owns the incoming ringtone.
*/
internal fun searchIncomingCallFullScreenPendingIntent(
callId: StreamCallId,
notificationId: Int = callId.getNotificationId(NotificationType.Incoming),
payload: Map<String, Any?>,
): PendingIntent? {
val intent = Intent(NotificationHandler.ACTION_INCOMING_CALL)
.putExtras(
notificationIntentBundleResolver.getIncomingCallBundle(
callId,
notificationId,
payload,
),
)
return searchResolveInfo { context.packageManager.queryIntentActivities(intent, 0) }
?.let { resolveInfo ->
PendingIntent.getActivity(
context,
0,
buildComponentIntent(intent, resolveInfo, callId),
DefaultNotificationHandler.PENDING_INTENT_FLAG,

Check warning on line 89 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultStreamIntentResolver.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Deprecated code should not be used.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AaCfEBFfmY9F3uphHxJk&open=AaCfEBFfmY9F3uphHxJk&pullRequest=1809
)
}
}

/**
* Search for an activity that is used for outgoing calls.
* Calls are considered outgoing until the call is accepted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,37 @@ public data class NotificationConfig(
*/
@ExperimentalStreamVideoApi
val notificationUpdateTriggers: (Call) -> Flow<*>? = { _ -> null },
)
) {
/**
* Delay before replacing an unaccepted incoming-call notification whose channel owns ringing.
* Android may stop an insistent alert when that notification is immediately replaced.
* Set this to `0` to disable the delay.
*/
public var incomingRingingNotificationUpdateDelayMillis: Long = 1_300L
private set

public constructor(
pushDeviceGenerators: List<PushDeviceGenerator> = emptyList(),
requestPermissionOnAppLaunch: () -> Boolean = { true },
notificationHandler: NotificationHandler = NoOpNotificationHandler,
autoRegisterPushDevice: Boolean = true,
requestPermissionOnDeviceRegistration: () -> Boolean = { true },
hideRingingNotificationInForeground: Boolean = false,
enableCallNotificationUpdates: Boolean = true,
mediaNotificationCallTypes: Set<String> = hashSetOf(CallType.Livestream.name),
notificationUpdateTriggers: (Call) -> Flow<*>? = { _ -> null },
incomingRingingNotificationUpdateDelayMillis: Long,
) : this(
pushDeviceGenerators = pushDeviceGenerators,
requestPermissionOnAppLaunch = requestPermissionOnAppLaunch,
notificationHandler = notificationHandler,
autoRegisterPushDevice = autoRegisterPushDevice,
requestPermissionOnDeviceRegistration = requestPermissionOnDeviceRegistration,
hideRingingNotificationInForeground = hideRingingNotificationInForeground,
enableCallNotificationUpdates = enableCallNotificationUpdates,
mediaNotificationCallTypes = mediaNotificationCallTypes,
notificationUpdateTriggers = notificationUpdateTriggers,
) {
this.incomingRingingNotificationUpdateDelayMillis = incomingRingingNotificationUpdateDelayMillis
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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

import android.app.Notification
import android.app.PendingIntent
import android.os.Bundle
import androidx.core.app.NotificationCompat
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.RingingState

internal fun interface NotificationUpdateComparator {
fun areEquivalent(
call: Call,
ringingState: RingingState,
existing: Notification,
updated: Notification,
): Boolean
}

internal object DefaultNotificationUpdateComparator : NotificationUpdateComparator {
override fun areEquivalent(
call: Call,
ringingState: RingingState,
existing: Notification,
updated: Notification,
): Boolean {
if (existing.hasCustomRemoteViews() || updated.hasCustomRemoteViews()) return false
return existing.toContentFingerprint() == updated.toContentFingerprint()
}

private fun Notification.hasCustomRemoteViews(): Boolean =
contentView != null || bigContentView != null || headsUpContentView != null

Check warning on line 47 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationUpdateComparator.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Deprecated code should not be used.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AaCfEBF5mY9F3uphHxJn&open=AaCfEBF5mY9F3uphHxJn&pullRequest=1809

Check warning on line 47 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationUpdateComparator.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Deprecated code should not be used.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AaCfEBF5mY9F3uphHxJp&open=AaCfEBF5mY9F3uphHxJp&pullRequest=1809

Check warning on line 47 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationUpdateComparator.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Deprecated code should not be used.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AaCfEBF5mY9F3uphHxJo&open=AaCfEBF5mY9F3uphHxJo&pullRequest=1809

@Suppress("DEPRECATION")
private fun Notification.toContentFingerprint(): List<Any?> = listOf(
NotificationCompat.getChannelId(this),
icon,
iconLevel,
tickerText?.toString(),
number,
contentIntent,
deleteIntent,
fullScreenIntent,
sound?.toString(),
audioAttributes?.usage,
audioAttributes?.contentType,
vibrate?.toList(),
ledARGB,
ledOnMS,
ledOffMS,
defaults,
flags,
priority,
category,
group,
sortKey,
visibility,
color,
actions(),
extras.toComparableMap(),
)

private fun Notification.actions(): List<List<Any?>> = buildList {
repeat(NotificationCompat.getActionCount(this@actions)) { index ->
NotificationCompat.getAction(this@actions, index)?.let { action ->
add(
listOf(
action.icon,

Check warning on line 83 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationUpdateComparator.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Deprecated code should not be used.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AaCfEBF5mY9F3uphHxJm&open=AaCfEBF5mY9F3uphHxJm&pullRequest=1809
action.title?.toString(),
action.actionIntent as PendingIntent?,

Check warning on line 85 in stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationUpdateComparator.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless cast.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-video-android&issues=AaCfEBF5mY9F3uphHxJl&open=AaCfEBF5mY9F3uphHxJl&pullRequest=1809
action.semanticAction,
action.showsUserInterface,
action.allowGeneratedReplies,
),
)
}
}
}

@Suppress("DEPRECATION")
private fun Bundle.toComparableMap(): Map<String, Any?> =
keySet().sorted().associateWith { key -> get(key).toComparableValue() }

private fun Any?.toComparableValue(): Any? = when (this) {
null -> null
is CharSequence -> toString()
is Bundle -> toComparableMap()
is Array<*> -> map { it.toComparableValue() }
is BooleanArray -> toList()
is ByteArray -> toList()
is CharArray -> toList()
is DoubleArray -> toList()
is FloatArray -> toList()
is IntArray -> toList()
is LongArray -> toList()
is ShortArray -> toList()
is Collection<*> -> map { it.toComparableValue() }
else -> this
}
}
Loading
Loading