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 a3a4726aaa..ee9fd5320a 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -4231,16 +4231,18 @@ 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;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 fun (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/jvm/functions/Function0;Z)V + public synthetic fun (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/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 ()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 final fun component4 ()Z + public final fun component5 ()Lkotlin/jvm/functions/Function0; + public final fun component6 ()Z + public final fun copy (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/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;ZLkotlin/jvm/functions/Function0;ZILjava/lang/Object;)Lio/getstream/video/android/core/notifications/NotificationConfig; public fun equals (Ljava/lang/Object;)Z + public final fun getAutoRegisterPushDevice ()Z public final fun getHideRingingNotificationInForeground ()Z public final fun getNotificationHandler ()Lio/getstream/video/android/core/notifications/NotificationHandler; public final fun getPushDeviceGenerators ()Ljava/util/List; 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 4548cf4d6f..32ba5443c1 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 @@ -235,6 +235,9 @@ public class StreamVideoBuilder @JvmOverloads constructor( scope.launch { try { val result = client.connectAsync().await() + if (notificationConfig.autoRegisterPushDevice) { + client.registerPushDevice() + } result.onSuccess { streamLog { "Connection succeeded! (duration: ${result.getOrNull()})" } }.onError { 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 4027d2d355..a3132b2f5f 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 @@ -94,7 +94,17 @@ public open class DefaultNotificationHandler( override fun onRingingCall(callId: StreamCallId, callDisplayName: String) { logger.d { "[onRingingCall] #ringing; callId: ${callId.id}" } - CallService.showIncomingCall(application, callId, callDisplayName) + CallService.showIncomingCall( + application, + callId, + callDisplayName, + notification = getRingingCallNotification( + RingingState.Incoming(), + callId, + callDisplayName, + shouldHaveContentIntent = true, + ), + ) } override fun onMissedCall(callId: StreamCallId, callDisplayName: 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 79370a887f..9afc05948d 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 autoRegisterPushDevice: Boolean = true, val requestPermissionOnDeviceRegistration: () -> Boolean = { true }, /** * Set this to true if you want to make the ringing notifications as low-priority 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 7b52ad73a8..6d1af4606a 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 @@ -16,13 +16,16 @@ package io.getstream.video.android.core.notifications.internal.service +import android.Manifest import android.annotation.SuppressLint import android.app.ActivityManager import android.app.Notification import android.app.Service +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.content.pm.ServiceInfo import android.media.AudioAttributes import android.media.AudioFocusRequest @@ -34,6 +37,7 @@ import android.net.Uri import android.os.Build import android.os.IBinder import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import io.getstream.log.StreamLog @@ -47,6 +51,7 @@ import io.getstream.video.android.core.notifications.NotificationHandler.Compani 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.safeCallWithDefault +import io.getstream.video.android.core.utils.safeCallWithResult import io.getstream.video.android.core.utils.startForegroundWithServiceType import io.getstream.video.android.model.StreamCallId import io.getstream.video.android.model.streamCallDisplayName @@ -164,60 +169,89 @@ internal open class CallService : Service() { intent ?: Intent(context, CallService::class.java) } - fun showIncomingCall(context: Context, callId: StreamCallId, callDisplayName: String?, callServiceConfiguration: CallServiceConfig = callServiceConfig()) { + fun showIncomingCall( + context: Context, + callId: StreamCallId, + callDisplayName: String?, + callServiceConfiguration: CallServiceConfig = callServiceConfig(), + notification: Notification?, + ) { val hasActiveCall = StreamVideo.instanceOrNull()?.state?.activeCall?.value != null - - if (!hasActiveCall) { - ContextCompat.startForegroundService( - context, - buildStartIntent( + safeCallWithResult { + val result = if (!hasActiveCall) { + ContextCompat.startForegroundService( context, - callId, - TRIGGER_INCOMING_CALL, - callDisplayName, - callServiceConfiguration, - ), - ) - } else { + buildStartIntent( + context, + callId, + TRIGGER_INCOMING_CALL, + callDisplayName, + callServiceConfiguration, + ), + ) + ComponentName(context, CallService::class.java) + } else { + context.startService( + buildStartIntent( + context, + callId, + TRIGGER_INCOMING_CALL, + callDisplayName, + callServiceConfiguration, + ), + ) + } + result!! + }.onError { + // Show notification + StreamLog.e(TAG) { "Could not start service, showing notification only: $it" } + val hasPermission = ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + StreamLog.i(TAG) { "Has permission: $hasPermission" } + StreamLog.i(TAG) { "Notification: $notification" } + if (hasPermission && notification != null) { + NotificationManagerCompat.from(context) + .notify(INCOMING_CALL_NOTIFICATION_ID, notification) + } + } + } + + fun removeIncomingCall( + context: Context, + callId: StreamCallId, + config: CallServiceConfig = callServiceConfig(), + ) { + safeCallWithResult { context.startService( buildStartIntent( context, callId, - TRIGGER_INCOMING_CALL, - callDisplayName, - callServiceConfiguration, + TRIGGER_REMOVE_INCOMING_CALL, + callServiceConfiguration = config, ), - ) + )!! + }.onError { + NotificationManagerCompat.from(context).cancel(INCOMING_CALL_NOTIFICATION_ID) } } - 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 = safeCallWithDefault( - 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 + private fun isServiceRunning(context: Context, serviceClass: Class<*>): Boolean = + safeCallWithDefault(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 } - StreamLog.w(TAG) { "Service is NOT running: $serviceClass" } - return false - } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -347,7 +381,11 @@ internal open class CallService : Service() { } } - private fun maybePromoteToForegroundService(videoClient: StreamVideoClient, notificationId: Int, trigger: String) { + private fun maybePromoteToForegroundService( + videoClient: StreamVideoClient, + notificationId: Int, + trigger: String, + ) { val hasActiveCall = videoClient.state.activeCall.value != null val not = if (hasActiveCall) " not" else "" @@ -356,12 +394,23 @@ internal open class CallService : Service() { } if (!hasActiveCall) { - videoClient.getSettingUpCallNotification()?.let { - startForegroundWithServiceType(notificationId, it, trigger, serviceType) + videoClient.getSettingUpCallNotification()?.let { notification -> + startForegroundWithServiceType( + notificationId, + notification, + trigger, + serviceType, + ) } } } + private fun justNotify(notificationId: Int, notification: Notification) { + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + NotificationManagerCompat.from(this).notify(notificationId, notification) + } + } + @SuppressLint("MissingPermission") private fun showIncomingCall(notificationId: Int, notification: Notification) { if (callId == null) { // If there isn't another call in progress (callId is set in onStartCommand()) @@ -371,12 +420,12 @@ internal open class CallService : Service() { notification, TRIGGER_INCOMING_CALL, serviceType, - ) + ).onError { + justNotify(notificationId, notification) + } } else { // Else, we show a simple notification (the service was already started as a foreground service). - NotificationManagerCompat - .from(this) - .notify(notificationId, notification) + justNotify(notificationId, notification) } } @@ -631,7 +680,11 @@ internal open class CallService : Service() { } } - private fun handleIncomingCallAcceptedByMeOnAnotherDevice(acceptedByUserId: String, myUserId: String, callRingingState: RingingState) { + 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) { // So stop ringing on this device @@ -639,7 +692,12 @@ internal open class CallService : Service() { } } - private fun handleIncomingCallRejectedByMeOrCaller(rejectedByUserId: String, myUserId: String, createdByUserId: String?, activeCallExists: Boolean) { + private fun handleIncomingCallRejectedByMeOrCaller( + rejectedByUserId: String, + myUserId: String, + createdByUserId: String?, + activeCallExists: Boolean, + ) { // If rejected event was received (even from another device), with event user being me OR the caller, remove incoming call / stop service. if (rejectedByUserId == myUserId || rejectedByUserId == createdByUserId) { if (activeCallExists) { 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 7b91b1e030..a284078367 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 @@ -189,7 +189,7 @@ internal fun Service.startForegroundWithServiceType( notification: Notification, trigger: String, foregroundServiceType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL, -) { +) = safeCallWithResult { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { startForeground(notificationId, notification) } else {