Skip to main content
SDKsAndroid

Rich notification images

FCM notification vs data-only messages, BigPictureStyle, and the MotiSig image URL contract on Android.

Android handles notification banner images differently from iOS. There is no Notification Service Extension: either FCM renders the image for you (system message), or your host app renders its own notification with NotificationCompat.BigPictureStyle (data-only message).

This page covers both paths and the payload contract MotiSig uses to make them interchangeable.

Two delivery modes

ModeWhat FCM doesWhat your app must do
notification message with notification.imageFCM auto-renders the banner with the image when the app is in the background. onMessageReceived is not called.Nothing for the banner. For tap → MotiSig.handleNotificationIntent(intent) from your launcher activity.
Data-only message (or notification message in foreground)Calls MotiSigFirebaseMessagingService.onMessageReceived with data only. No automatic banner.Build a notification yourself with BigPictureStyle to show the image.

You can pick either mode on the server side; the MotiSig backend defaults to the first form so the platform handles rendering when possible.

What the SDK does for you

MotiSigFirebaseMessagingService reads RemoteMessage.notification.imageUrl and lifts it into data["imageUrl"] (when no image* key is already present), then dispatches to your MotiSigNotificationListener with userInfo containing the image URL — regardless of which delivery mode was used. So your in-app handlers always see a single, predictable key.

Image URL resolution order (host app helper)

For data-only messages, your app builds the notification. The example app ships PushImageUrl.kt, which extracts the first usable HTTPS URL from these keys, in order:

  1. _motisig.imageUrl / _motisig.image_url / _motisig.image (MotiSig canonical, supports JSON-string _motisig blob)
  2. _richContent.image (Expo push relay)
  3. fcm_options.image (FCM relay)
  4. Top-level image / imageUrl / image_url
  5. Sorted ios_attachment_*_url (cross-platform payload compatibility)

This matches the iOS and Expo SDKs so a single server payload lights up images on all platforms.

Copy PushImageUrl.kt into your project (or write your own resolver) and use it from a NotificationCompat builder.

Building a notification with BigPictureStyle

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import androidx.core.app.NotificationCompat

class MyMessagingService : MotiSigFirebaseMessagingService() {
    override fun onMessageReceived(message: com.google.firebase.messaging.RemoteMessage) {
        super.onMessageReceived(message) // hand off to the SDK first

        val data = message.data
        val imageUrl = extractPushImageUrl(data.mapValues { it.value as Any })
        val title = message.notification?.title ?: data["title"] ?: return
        val body = message.notification?.body ?: data["body"] ?: ""

        showRichNotification(applicationContext, title, body, imageUrl, data)
    }
}

private fun showRichNotification(
    context: Context,
    title: String,
    body: String,
    imageUrl: String?,
    data: Map<String, String>,
) {
    val channelId = "motisig_default"
    val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && nm.getNotificationChannel(channelId) == null) {
        nm.createNotificationChannel(
            NotificationChannel(channelId, "Default", NotificationManager.IMPORTANCE_DEFAULT)
        )
    }

    val tapIntent = Intent(context, YourLauncherActivity::class.java).apply {
        addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
        for ((k, v) in data) putExtra(k, v)
    }
    val pi = PendingIntent.getActivity(
        context, 0, tapIntent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
    )

    val builder = NotificationCompat.Builder(context, channelId)
        .setSmallIcon(R.drawable.ic_notification)
        .setContentTitle(title)
        .setContentText(body)
        .setAutoCancel(true)
        .setContentIntent(pi)

    if (!imageUrl.isNullOrBlank()) {
        // Off the main thread; use your image loader of choice (Coil, Glide, OkHttp, etc.)
        try {
            val bytes = java.net.URL(imageUrl).readBytes()
            val bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
            builder.setLargeIcon(bmp)
                .setStyle(NotificationCompat.BigPictureStyle().bigPicture(bmp).bigLargeIcon(null as android.graphics.Bitmap?))
        } catch (_: Throwable) {
            // fall back to text-only
        }
    }

    nm.notify(System.currentTimeMillis().toInt(), builder.build())
}

Notes:

  • Run the URL fetch off the main thread. FirebaseMessagingService.onMessageReceived runs on a worker thread, so a synchronous URL.readBytes() is safe inside it (the example above is intentionally minimal — production apps should use a real image loader with caching).
  • Forward the FCM data as Intent extras so your launcher activity's onCreate / onNewIntent can call MotiSig.getInstance().handleNotificationIntent(intent) and trigger click tracking.
  • The MotiSig SDK never posts notifications on your behalf; it only forwards events to listeners. Posting is fully under your control.

Expected payload

For a data-only delivery, the recommended canonical payload is:

{
  "data": {
    "messageId": "f082aa55-6eed-407f-b819-36e858ed7d0a",
    "title": "Hello",
    "body": "world",
    "_motisig": "{\"imageUrl\":\"https://your-cdn.example.com/path/push.jpg\"}"
  }
}

For an FCM notification message that auto-renders, set notification.image directly on the FCM payload — the SDK still copies the URL into data["imageUrl"] for in-app reads.

Foreground

When your app is in the foreground, FCM does not auto-render notifications: every push hits onMessageReceived and your code decides whether to show a notification, an in-app banner, both, or nothing. Use MotiSigNotificationListener (inForeground == true) to update in-app state, and call NotificationManagerCompat.notify(...) if you also want a tray banner.

Troubleshooting

SymptomWhat to check
Banner missing image when app is in backgroundThe push was sent as a data-only message. Either send it as a notification message with notification.image, or build the notification yourself from onMessageReceived (BigPictureStyle).
onMessageReceived never calledYour app is in the background and the push is a notification message — that's normal. Test with the app in the foreground to verify onMessageReceived.
Image URL returns 403 / 401The CDN requires auth headers your app cannot inject from Android's image loader. Sign the URL itself (query-string token) so it's publicly fetchable.
_motisig arrives as a string, not a mapFCM coerces all data values to strings on the wire. The example helper handles this — see appendMotisigImageCandidates in PushImageUrl.kt.
Image loaded but banner still smallUse both setLargeIcon(...) and BigPictureStyle().bigPicture(...) so the expanded view shows the full image. Without BigPictureStyle, only the thumbnail appears.

For broader debugging, see Troubleshooting.