Docs
grovs.io Start for free

Push notifications

Receive Grovs push campaigns on Android, report opens, follow their links and show campaign images.

Requires Android SDK 3.1.0

The SDK reports push campaign opens and hands every tapped Grovs push to your app like a deep link. Campaigns come from the dashboard, a journey or the server API. To create them, see Push campaigns.

Tap handling, foreground display and GrovsMessaging need Android SDK 3.1 or later. On 3.0, pushes arrive but taps aren't reported.

1. Add Firebase Cloud Messaging

The SDK compiles against Firebase Messaging but doesn't bring it in, so your app adds it. If your app doesn't use Firebase yet:

  1. Add your Android app in the Firebase Console.
  2. Download google-services.json into your app's app/ directory.
  3. Add the Firebase dependencies:
Groovy
// project-level build.gradle
plugins {
    id("com.google.gms.google-services") version "4.4.2" apply false
}
 
// app-level build.gradle
plugins {
    id("com.google.gms.google-services")
}
 
dependencies {
    implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
    implementation("com.google.firebase:firebase-messaging")
}

2. Upload your Firebase credentials to Grovs

Grovs uses the FCM v1 API, which needs a Service Account JSON key and your Firebase Project ID.

1
Generate a Service Account key

In the Firebase Console, go to Project Settings → Service Accounts and click Generate new private key. Keep the downloaded .json file safe.

2
Copy your Firebase Project ID

In Project Settings → General, copy the Project ID (for example my-app-12345).

3
Upload to Grovs

In the Grovs dashboard, open SDK Setup → Android and go to the Push Notifications step. Upload the JSON key, enter the Project ID and save.

3. Ask for permission and pass the token

The SDK doesn't declare POST_NOTIFICATIONS. On Android 13 (API 33) and later, declare it and request it at runtime:

XML
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Kotlin
import android.Manifest
import android.os.Build
 
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0)
}

Pass the FCM token to Grovs:

Kotlin
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
    Grovs.pushToken = token
}

You can set it before configure. The SDK keeps it and sends it once it authenticates. Token refreshes reach Grovs through the SDK's own messaging service, with no app code.

4. Handle taps

Taps need no extra code if you already forward onStart and onNewIntent from your launcher activity (see Quick Start). Tapping a Grovs push opens the launcher activity with the push data as intent extras. The SDK reads them, reports the open and calls your deep link listener.

If your launcher is a splash screen that starts another activity without its extras, forward the extras, or pass the tap intent yourself from the activity that receives it:

Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Grovs.handleNotificationOpen(intent)
}

handleNotificationOpen returns false and does nothing for an intent that isn't a Grovs push. Pass it only intents that come from a tap.

What your app receives

A tapped Grovs push arrives in your GrovsDeeplinkListener and in openedLinkDetails, like any deep link:

  • data: the push's data keys: your campaign's custom data plus grovs_campaign_id, and grovs_link and grovs_image when set. Firebase transport keys (google.*, gcm.*, from, collapse_key, message_type) are removed. Push values are all strings.
  • Campaign with a link: the SDK resolves it like any Grovs link. link and tracking come from the resolved link. data is the link's data merged with the push's keys; on a clash the push's value wins. The link's own values keep their types.
  • Campaign without a link, or a link that can't be resolved: link and tracking are null and data is the push's data. A failed link lookup isn't retried.
Kotlin
Grovs.setOnDeeplinkReceivedListener(this) { details ->
    val screen = details.data?.get("screen") as? String
    if (screen != null) {
        // route to the screen your campaign or link points at
    }
}

A tap on a push with a Grovs link counts once, as a campaign open (push_open). The link resolves with source push, so it isn't also a link open and later events aren't attributed to the link.

A push is a Grovs push when its data has a non-blank grovs_campaign_id. Check without handling it with Grovs.isGrovsNotification(message.data) or Grovs.isGrovsNotification(intent).

Pushes received while the app is open

When the app is in the background, Firebase shows the push. When it's in the foreground, the SDK's messaging service shows it, with its image, and a tap opens your launcher activity the same way.

  • Image: the notification's image, or grovs_image. Loaded over https only, within 5 seconds and up to 1 MB. If it fails, the push shows without it.
  • Channel: the Firebase default channel (com.google.firebase.messaging.default_notification_channel_id) if your app created it. Otherwise the SDK creates its own high-importance channel, named "Grovs Channel".
  • Sound: on Android 8 and later, the channel decides. A custom campaign sound plays only if the channel has that sound.
  • Nothing shows if the user turned notifications off for your app.

Set a default channel so background pushes don't land in "Miscellaneous", and create that channel at startup:

XML
<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="promotions" />

Small icon

For foreground pushes the SDK uses io.grovs.NotificationIconSmall, then the Firebase default icon (com.google.firebase.messaging.default_notification_icon, which Firebase also uses in the background), then its own. Give the icon as a resource or as a drawable name:

XML
<meta-data
    android:name="io.grovs.NotificationIconSmall"
    android:resource="@drawable/ic_notify" />
 
<!-- or -->
<meta-data
    android:name="io.grovs.NotificationIconSmall"
    android:value="ic_notify" />

To stop the SDK showing pushes while the app is open, set GrovsMessaging.showForegroundNotifications = false. If your app shows them itself, a tap is still handled when its intent opens your launcher activity with the push data as string extras.

If your app has its own FirebaseMessagingService

Android delivers FCM messages to one service only. Either extend the Grovs service:

Kotlin
import io.grovs.GrovsMessaging
import io.grovs.handlers.MessagingService
 
class MyMessagingService : MessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        if (GrovsMessaging.handleMessage(this, message)) return
        // your own messages
    }
}

or forward to Grovs from yours:

Kotlin
import com.google.firebase.messaging.FirebaseMessagingService
import io.grovs.GrovsMessaging
 
class MyMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        if (GrovsMessaging.handleMessage(this, message)) return
        // your own messages
    }
 
    override fun onNewToken(token: String) {
        GrovsMessaging.onNewToken(token)
    }
}

handleMessage shows a Grovs push (unless showForegroundNotifications is false) and returns true. It returns false for any other message. onNewToken is the same as setting Grovs.pushToken.

Declare your service, and remove the SDK's declaration so Android doesn't pick the wrong one:

XML
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application>
        <service
            android:name=".MyMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
 
        <service
            android:name="io.grovs.handlers.MessagingService"
            tools:node="remove" />
    </application>
</manifest>

From Java, the members are static: GrovsMessaging.handleMessage(this, message), GrovsMessaging.setShowForegroundNotifications(false).

Delivery guarantees

SituationWhat happens
Before configureThe tap is held (the latest one) and handled once configure runs.
Before authenticationA push without a link is delivered at once. A push with a link is delivered once the SDK authenticates. The open is saved and sent after authentication.
Network or server errorRetried a few times, then kept and retried at the next foreground until it's reported. A push open the server refuses is dropped.
App restartUnreported opens are saved on disk and sent after the next launch authenticates. They're dropped after 7 days, like events.
Same tap seen twiceonStart and onNewIntent seeing the same intent report one open. A push already delivered isn't delivered again when the launcher intent is read again, even after a restart.
No consentA tap while the SDK is disabled delivers nothing and reports nothing. If you call setSDK(true) before the app goes to the background, the tap is handled then. setSDK(false) drops unreported opens.

Grovs counts one open per device per campaign.

Testing

Test on a physical device or an emulator with Google Play Services.

For an end-to-end test, create a campaign whose audience is your own user ID (the Grovs.identifier your app sets) and send it to a build on the matching environment. Tap it with the app in the background, then again with it open. The open shows in the campaign report.

Turn on Grovs.setDebug(LogLevel.INFO) to see each open reported, or kept for retry, in Logcat.

Asking for permission from an in-app message

An in-app message can carry a button that asks for push permission. On Android 13 and later this needs POST_NOTIFICATIONS declared in your manifest; without it, campaigns with this button aren't shown. If the user declined for good, or turned notifications off, the button opens your app's notification settings. Campaigns with this button skip people who already allowed notifications.

Last updated 2026-10-26
Was this page helpful?