Push notifications
Receive Grovs push campaigns on Android, report opens, follow their links and show campaign images.
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:
- Add your Android app in the Firebase Console.
- Download
google-services.jsoninto your app'sapp/directory. - Add the Firebase dependencies:
// 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.
In the Firebase Console, go to Project Settings → Service Accounts and click Generate new private key. Keep the downloaded .json file safe.
In Project Settings → General, copy the Project ID (for example my-app-12345).
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:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />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:
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:
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 customdataplusgrovs_campaign_id, andgrovs_linkandgrovs_imagewhen 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.
linkandtrackingcome from the resolved link.datais 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:
linkandtrackingarenullanddatais the push's data. A failed link lookup isn't retried.
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 overhttpsonly, 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:
<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:
<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:
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:
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:
<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
| Situation | What happens |
|---|---|
Before configure | The tap is held (the latest one) and handled once configure runs. |
| Before authentication | A 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 error | Retried a few times, then kept and retried at the next foreground until it's reported. A push open the server refuses is dropped. |
| App restart | Unreported opens are saved on disk and sent after the next launch authenticates. They're dropped after 7 days, like events. |
| Same tap seen twice | onStart 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 consent | A 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.