Skip to content

Commit

Permalink
[AND-153] ForegroundServiceStartNotAllowedException will now show t…
Browse files Browse the repository at this point in the history
…he notification instead of failing the whole process of starting a service (#1239)

* When startForegroundService crashes, just send the notification without a service

* Wrap most `startForeground` calls with results and simply show the notification if the service start fails.

* Do not show setup notification eagerly if something fails

* Move config into the `NotificationConfig`

* Api

* Clean up

---------

Co-authored-by: Liviu Timar <65943217+liviu-timar@users.noreply.github.com>
  • Loading branch information
aleksandar-apostolov and liviu-timar authored Dec 4, 2024
1 parent c1bc2a2 commit bb9c515
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 59 deletions.
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) {
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
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

0 comments on commit bb9c515

Please sign in to comment.