Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AND-153] ForegroundServiceStartNotAllowedException will now show the notification instead of failing the whole process of starting a service #1239

Merged
merged 10 commits into from
Dec 4, 2024
Merged
14 changes: 8 additions & 6 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun <init> (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;Lkotlin/jvm/functions/Function0;Z)V
public synthetic fun <init> (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 <init> (Ljava/util/List;Lkotlin/jvm/functions/Function0;Lio/getstream/video/android/core/notifications/NotificationHandler;ZLkotlin/jvm/functions/Function0;Z)V
public synthetic fun <init> (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ public class StreamVideoBuilder @JvmOverloads constructor(
scope.launch {
try {
val result = client.connectAsync().await()
if (notificationConfig.autoRegisterPushDevice) {
aleksandar-apostolov marked this conversation as resolved.
Show resolved Hide resolved
client.registerPushDevice()
}
result.onSuccess {
streamLog { "Connection succeeded! (duration: ${result.getOrNull()})" }
}.onError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public data class NotificationConfig(
val pushDeviceGenerators: List<PushDeviceGenerator> = 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
liviu-timar marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -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 ""

Expand All @@ -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())
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -631,15 +680,24 @@ 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
stopService()
}
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading