Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle background push notifications with Firebase, supporting Doze

I have an Android app configured to receive push notifications through Firebase and I'm having issues making it work while the phone is in Doze mode.

The application correctly receives push notifications, regardless if it is in foreground or in background. To do this, I am using only the data field in the push notification to be able to handle anything that comes in regardless of the status of the app.

I have implemented my service to receive notifications as follows:

class MyFirebaseMessagingService : FirebaseMessagingService() {

   override fun onMessageReceived(p0: RemoteMessage?) {
        Timber.d("Push notification received")
        super.onMessageReceived(p0)
        when (p0!!.data["ch"]) {
            NotificationType.VoIP.channelType() -> handleVoIPNotification(p0.data)
            NotificationType.Push.channelType() -> handlePushNotification(p0.data)
        }
    }

}

The ch property defines the type of notification and is sent from my backend: since my app has a video-calling function, when someone is calling the backend sends out a notification with ch = voip and sets the message priority to high, as documented in the Firebase guide.

The handleVoIPNotification function contains this:

private fun handleVoIPNotification(data: Map<String, String>) {
        val gson = Gson()
        val jsonElement = gson.toJsonTree(data)

        try {
            val voIPNotification = gson.fromJson(jsonElement, VoIPNotification::class.java)

            Timber.i("VoIP Notification received: %s", voIPNotification.action.name)

            // pass the incoming call data to the Call Manager.
            CallManager.getInstance().handleNotification(voIPNotification)
        } catch (exc: Exception) {
            Timber.e(exc)
            Timber.i("Invalid VoIP notification received: %s", data)
        }
    }

The Call Manager then updates a property named currentCall and terminates:

this.currentCall.apply {
        token = notification.token
        roomName = notification.room
        username = notification.nickname

        status.value = Call.CallStatus.Ringing
    }

The status property is a BehaviorSubject implementation that is observed by another object that reacts to changes of the status of the call:

currentCall.status.observable
                .distinctUntilChanged()
                .subscribe {
                    Timber.d("Call status changed: $it")

                    when (it) {
                        Call.CallStatus.Ringing -> {
                            this.showIncomingCallNotification()
                        }
                        Call.CallStatus.Declined, Call.CallStatus.Ended -> {
                            this.dismissIncomingCallNotification()
                            this.refreshIncomingCallActivity()
                        }
                        Call.CallStatus.Connecting -> {
                            this.dismissIncomingCallNotification()
                            this.presentOngoingCallActivity()
                        }
                        else -> { /* ignored */ }
                    }
                }.disposedBy(this.disposeBag)

The showIncomingCallNotification is the following:

fun showIncomingCallNotification() {
        val intent = Intent(Intent.ACTION_MAIN, null).apply {
            flags = Intent.FLAG_ACTIVITY_NO_USER_ACTION or Intent.FLAG_ACTIVITY_NEW_TASK
            setClass(configuration.context, configuration.incomingCallActivityType.java)
        }

        val pendingIntent = PendingIntent.getActivity(configuration.context, configuration.requestCode, intent, 0)

        val builder = NotificationCompat.Builder(configuration.context, configuration.notificationChannel)
                .setOngoing(true)
                .setContentIntent(pendingIntent)
                .setFullScreenIntent(pendingIntent, true)
                .setSmallIcon(configuration.notificationIcon)
                .setContentTitle(currentCall.username)
                .setContentText(configuration.context.getString(configuration.notificationText))

        val notification = builder.build()
        notification.flags = notification.flags or Notification.FLAG_INSISTENT

        configuration.notificationsManager.getSystemNotificationManager().notify(0, notification)
    }

This code shows a notification which, when pressed, opens the IncomingCallActivity and lets the user accept or reject the call. This notification is also responsible of making the phone ring and vibrate.

All of this works perfectly when the app is opened in foreground or in the background, as long as the screen is on or it has just been turned off. If I wait a while (from 5 minutes to hours, it depends), everything stops working: my Firebase service is not called when a push notification is sent from my backend (and I can see that the push notification has been correctly sent). Turning on the screen, makes the incoming call notification appear correctly.

I have logs in place that clearly show that the Firebase service onMessageReceived function is not even called until I turn on the screen.

I have read a thousand times the Android Developers - Optimize for Doze and App Standby and they clearly state that FCM with High priority messages is the correct approach for waking up an app when the phone is in idle.

FCM is optimized to work with Doze and App Standby idle modes by means of high-priority FCM messages. FCM high-priority messages let you reliably wake your app to access the network, even if the user’s device is in Doze or the app is in App Standby mode. In Doze or App Standby mode, the system delivers the message and gives the app temporary access to network services and partial wakelocks, then returns the device or app to the idle state.

Regardless, this doesn't work. I have tried on different phones and I can say that the behavior is way worse on Android 9, whereas from Android 7 and lower it is not so common.

I have seen and tried the solutions proposed in this other question, but none of them seems to work and, even if in one of the answers they say that this happens only if the user has swiped away the app from multi-tasking, I can say that it happens to me even if the app hasn't been force stopped.

There's also another question, more similar to my issue, here but all the proposed solutions don't work.

I have no idea how to solve this and it is a very bad issue and affects the usability of my app, since the user is unable to answer to video-calls when they're not using their phone. I also found other people online reporting this issue, but all of those conversations seem to die at some point without an actual solution…

Can anyone help me?

like image 805
MrAsterisco Avatar asked Jun 28 '19 13:06

MrAsterisco


People also ask

How do you handle Firebase push notifications?

Handle notification messages in a backgrounded appA user tap on the notification opens the app launcher by default. This includes messages that contain both notification and data payload (and all messages sent from the Notifications console).

Is Firebase good for push notifications?

Firebase Cloud Messaging (FCM) provides a reliable and battery-efficient connection between your server and devices that allows you to deliver and receive messages and notifications on iOS, Android, and the web at no cost.

Can I send push notifications without FCM?

Without FCM, you'd need to build more than three notification systems. You'd need one each for iOS and Android, but then you'd also need to accommodate all the different types of browsers for the web application to deliver push notifications without a hitch.


1 Answers

Turns out that this is not a FCM issue, but it's actually a problem with Azure Notification Hub (which is the system our backend is using to send out notifications).

If you're having issues with High Priority messages, Azure Notification Hub and Doze Mode, refer to the chosen answer of this question: Setting Fcm Notification Priority - Azure Notification Hub

like image 141
MrAsterisco Avatar answered Oct 21 '22 02:10

MrAsterisco