Skip to main content
SDKsAndroid

Push notifications

FCM integration, listeners, delivered notifications, and click tracking on Android.

This document describes how the MotiSig Android SDK participates in FCM. Your app still needs the standard Firebase setup (google-services.json, firebase-messaging dependency, manifest entries, runtime POST_NOTIFICATIONS permission on Android 13+).

What initialize installs

On Application.onCreate (or the first MotiSig.initialize call):

  • An HTTP client bound to sdkKey / projectId / baseURL.
  • An Application.ActivityLifecycleCallbacks that runs on every activity start/resume:
    • First foreground activity → calls ping() and starts the foreground heartbeat.
    • Each resume → reads NotificationManagerCompat.areNotificationsEnabled(context) and patches the push subscription permission field if it changed.
  • A best-effort fetch of the current FCM token (FirebaseMessaging.getInstance().token) so the push subscription can be upserted as soon as a user is set.

FCM messaging service

Register MotiSigFirebaseMessagingService in your manifest (or subclass it and chain super):

<service
    android:name="ai.motisig.sdk.MotiSigFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

MotiSigFirebaseMessagingService translates FCM events into SDK calls:

  • onNewToken(token)MotiSig.setPushToken(token).
  • onMessageReceived(message)MotiSig.handleNotificationReceived(title, body, data, inForeground, openedFromTap = false) when initialize was called with skipNotificationListeners = false (default). If skipNotificationListeners is true, this service returns without calling the SDK (iOS parity); you may still forward payloads manually. Before dispatch, the service lifts RemoteMessage.notification.imageUrl into data["imageUrl"] (when no image* key already exists) so listeners always see the image URL.

Payload suppressForeground (boolean / 1 / "true") skips foreground listener delivery and foreground analytics for that message. Foreground receives also send POST /track/click with isForeground: true (matching iOS willPresent). Tap opens set MotiSigNotification.wasForeground when the same correlation key was seen in the foreground first.

Device token and push subscription

When an FCM token arrives, the SDK stores it. If a user is already set, it upserts a push subscription (POST …/push-subscriptions) with devicePlatform: "android", pushType: "fcm", the token, the current permission (granted / declined), and the customer-enabled flag (see below). If setUser completes after the token exists, the SDK runs the same upsert at that point. If the token string changes, the SDK removes the old subscription then upserts the new one.

setNotificationEnabled(enabled)

This is the customer-controlled server flag for whether this device's subscription is enabled. It is persisted locally (survives logout()). Call it when your app UI lets the user turn MotiSig-delivered pushes on or off; the SDK sends PATCH …/push-subscriptions with enabled. It does not change OS notification permission.

MotiSig.getInstance().isNotificationEnabled returns the current value.

Permission vs enabled

When the user changes notification permission in Android Settings and returns to the app, the SDK patches the push subscription permission field only (it does not infer enabled from permission). The mapping is binary on Android: granted/declined; unknown is not produced.

Notification listeners

Register a listener that implements MotiSigNotificationListener:

class NotificationRouter : MotiSigNotificationListener {
    override fun motiSigDidReceiveNotification(
        notification: MotiSigNotification,
        inForeground: Boolean,
    ) {
        // handle title, body, userInfo, requestIdentifier
    }
}

private val router = NotificationRouter()
private var subscription: MotiSigNotificationSubscription? = null

subscription = MotiSig.getInstance().addNotificationListener(router, order = 0)
// later:
subscription?.remove()

Behavior:

  • Listeners are held weakly. Retain your listener object as long as you need callbacks.
  • order controls sort priority: lower values are notified first. null behaves like 0. For the same order, registration order (FIFO) applies.
  • Callbacks are delivered on the main thread via motiSigDidReceiveNotification(notification, inForeground).
  • If no listeners are registered when an event arrives, the SDK buffers a bounded set of events; when you add a listener, buffered events are replayed on the main thread before new live events.
  • MotiSigNotificationSubscription.remove() is idempotent and safe from any thread. Dropping the subscription object without calling remove() does not unregister the listener.
  • logout() and removeAllNotificationListeners() clear listeners and the buffer.

MotiSigNotification

public data class MotiSigNotification(
    public val messageId: String?,
    public val title: String?,
    public val body: String?,
    public val userInfo: Map<String, Any>,
    public val requestIdentifier: String? = null,
)

requestIdentifier is set when the event came from the system shade (fetchDeliveredNotifications) — same key as StatusBarNotification.key. Live events from onMessageReceived typically leave it null.

Activity intent forwarding (taps)

When the user taps a notification posted by FCM (background/notification message) and your launcher activity opens, FCM puts the data extras on the Intent. Forward it to the SDK:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    MotiSig.getInstance().handleNotificationIntent(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    MotiSig.getInstance().handleNotificationIntent(intent)
}

handleNotificationIntent extracts title / body (or the gcm.notification.* fallback keys) from the Intent extras, treats it as a tap (openedFromTap = true), dispatches to listeners, and runs click tracking when messageId and a current user are present.

Fetching delivered notifications (explicit)

MotiSig.getInstance().fetchDeliveredNotifications { delivered: List<MotiSigDeliveredNotification> ->
    // runs on the main thread
}

Returns notifications still posted in the system shade for this app (NotificationManager.getActiveNotifications, API 23+). Each entry exposes requestIdentifier, postTimeMillis, and notification (MotiSigNotification).

Call this on process resume (or Activity.onResume) so pushes the user already saw — but did not tap — appear in your in-app history. On API levels below 23, the callback receives an empty list.

Entries can overlap with live MotiSigNotificationListener callbacks until the user dismisses them from the shade. Deduplicate using requestIdentifier and/or messageId, same idea as the iOS SDK's delivered fetch.

Click tracking

When the SDK is asked to dispatch with openedFromTap = true (currently only via handleNotificationIntent), it sends POST /track/click with userId, messageId, and isForeground if:

  • messageId is present in the payload, and
  • a user is set.

Otherwise the call is skipped and logged at DEBUG.

Rich images (banner)

Image rendering is handled by FCM (background notification messages with notification.image work out of the box) or by your host app's own BigPictureStyle notification (data-only messages). The SDK pre-resolves the image URL into userInfo["imageUrl"] so your listener sees a single field. Full recipe: Rich notification images.