# Push notifications

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

Source: https://docs.grovs.io/docs/sdk/android/push-notifications

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](https://docs.grovs.io/docs/engagement/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](https://console.firebase.google.com).
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](https://console.firebase.google.com), 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](https://app.grovs.io), 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](https://docs.grovs.io/docs/sdk/android/quick-start#2-forward-lifecycle-events)). 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`](https://docs.grovs.io/docs/sdk/android/deep-linking#receiving-deep-link-events) 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

| 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](https://docs.grovs.io/docs/sdk/android/in-app-messages) 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.
