Skip to main content
Supported on: Android SDK 1.0.33+. Forwarding from your own FirebaseMessagingService (AppDNA.push.isAppDNAMessage / handleMessage / handleMessageData / handleTapData) and the appdna_messaging_service_enabled switch need Android SDK 1.0.54+ (Flutter 1.0.20+, React Native 1.0.19+).
The AppDNA SDK provides a complete push notification module for registering device tokens via Firebase Cloud Messaging (FCM), handling notification delivery, and tracking user interactions.

Prerequisites

Before using push notifications, configure FCM for your app:
  1. Add google-services-appdna.json (or your project’s google-services.json) to your app/src/main/assets/ directory.
  2. Apply the com.google.gms.google-services Gradle plugin in your app module.
  3. Enable Firebase Cloud Messaging in the Firebase Console.
  4. Upload your FCM service-account JSON to the AppDNA Console under Settings > Push, in the FCM (Android) card (paste it into Service Account JSON), enter your Firebase Project ID (the project_id in that JSON), and click Save. It must be a service-account key ("type": "service_account", with client_email and private_key); other Google credential types, such as a workload identity federation configuration, are refused.
Push notifications will not be delivered to your users without google-services-appdna.json in your project AND FCM credentials uploaded to the AppDNA Console.
Requires an AppDNA server from this release or later. Delivered / tapped tracking and handlePushTap rely on the push_id, delivery_id and appdna marker the server now puts into every campaign, journey and direct push (a console test push carries no delivery_id); older servers sent no push_id.

How the SDK handles messages

The SDK merges its own AppDNAMessagingService into your manifest. It handles only AppDNA pushes — messages whose data carries the marker appdna = "1". Any other FCM message is ignored entirely (no tracking, no onPushReceived, no notification). An AppDNA push is shown in the foreground too: a message with a notification block is displayed by FCM itself in the background, and by the SDK (same channel and look as a data-only message) when it arrives while the app is in the foreground, where FCM shows nothing. An AppDNA message with no title and no body posts no notification.

If your app already uses Firebase Messaging

Requires Android SDK 1.0.54+ (Flutter 1.0.20+, React Native 1.0.19+). Android delivers MESSAGING_EVENT to one service only. If your app has its own FirebaseMessagingService, keep yours as the only handler and forward AppDNA messages to the SDK:
  1. Remove the SDK’s service in your app manifest (with xmlns:tools="http://schemas.android.com/tools" on the <manifest> element):
    Or disable it with a resource in your own res/values/:
  2. Pass the FCM token after configure and on every token refresh:
    AppDNA.push.handleMessageData(data) tracks delivery (with its delivery_id) and fires onPushReceived; it never posts a notification — display stays with you. Server pushes carry a notification block, which FCM displays itself while the app is in the background; in the foreground FCM displays nothing and hands the message to your service, so showing it then is up to you. AppDNA.push.isAppDNAMessage(data) tells you whether a message is AppDNA’s without handling it. If you would rather let the SDK display AppDNA messages, forward the whole message with AppDNA.push.handleMessage(message) instead: it does the same tracking and, for an AppDNA message that has a title or body, posts the notification as the SDK’s own service would. That includes a message with a notification block: your service receives one only while the app is in the foreground, where FCM does not display it (in the background FCM displays it and your service is not called), so it is shown once either way.
  3. Taps: call AppDNA.handlePushTap(intent) from your launch activity (onCreate and onNewIntent), or, when you build the notification yourself, AppDNA.push.handleTapData(data, actionId). Both track push_tapped (with delivery_id), fire onPushTapped and route the AppDNA action; they share one dedup, so calling both for one tap is safe. Both return false and do nothing unless the data carries the AppDNA marker. After a tap it handled, handlePushTap(intent) removes the appdna, push_id and delivery_id extras from the intent, so a re-created Activity does not track or route the same tap twice. If your code reads any of those extras, read them before calling handlePushTap. Other extras are left in place.
Do not also call the manual trackPushDelivered / trackPushTapped for messages you forward — they would count twice. Routing order for a tap: the tapped button’s own action, then the push’s action, then flat action_type / action_value, then screen_id, then deep_link.

Request Permission

On Android 13+ (API 33), push notifications require the POST_NOTIFICATIONS runtime permission. Request it from a coroutine — requestPermission is a suspend function that uses the Activity Result API internally and returns a Boolean once the user responds:
The SDK handles the runtime prompt and updates internal permission state automatically — you do not need to override onRequestPermissionsResult or call setPushPermission afterwards. On API < 33 the call is a no-op and returns true.
The activity parameter must be a ComponentActivity (or subclass like AppCompatActivity / FragmentActivity). The Activity Result API the SDK uses requires ComponentActivity.
Java-friendly variant: AppDNA.push.requestPermissionFuture(activity): CompletableFuture<Boolean>.

Set Token Manually

Register the device push token when it is received from FCM. This is needed when your app has its own FirebaseMessagingService — then also remove or disable the SDK’s service and forward AppDNA messages, as shown in If your app already uses Firebase Messaging:
You can also use the alternative method name:
Both setPushToken and onNewPushToken perform the same operation. The token registration payload sent to the backend includes: token, platform="android", device_id, app_version, sdk_version, and os_version.

Set Permission Status

Manually update the SDK with the current push permission status:
On Android 13+ (API 33), push notifications require the POST_NOTIFICATIONS runtime permission. Call setPushPermission after the user responds to the permission request.

Track Delivery and Taps

Use these only for pushes the SDK does not see (neither its own service nor your forwarding through AppDNA.push.handleMessage / handleTapData). They carry no delivery_id, so combined with forwarding or the SDK’s service they double-count.
Track when a push notification is delivered to the device:
Track when a user taps on a push notification:
The action parameter is optional. Pass null if no specific action is associated with the tap.

Push Module

Access the push module directly for advanced usage:

PushModule Methods

Pass the marker as a string. AppDNA’s servers send appdna and push_id as strings ("1", "…"). FCM data values are always strings. If you build or rewrite a payload yourself, keep them strings: iOS reads only string values, so a numeric appdna: 1 is not an AppDNA push there. On Android the push APIs take a Map<String, String>, and handlePushTap(intent) drops every extra that is not a String, so a numeric appdna extra is not an AppDNA tap either.

AppDNAPushDelegate

All 3 methods on this delegate fire from the SDK’s push pipeline:
  • onPushTokenRegistered(token) — fires whenever the SDK registers a new FCM token (or detects a change). Token registration with the AppDNA backend continues regardless of whether you implement this method.
  • onPushReceived(notification, inForeground) — fires when an AppDNA push arrives (not for your own FCM messages; use your own handler for those).
  • onPushTapped(notification, actionId) — fires when the user taps an AppDNA notification (appdna: "1") or one of its action buttons; not for your own FCM messages.
Implement the AppDNAPushDelegate interface to receive push notification lifecycle callbacks:

Example Implementation

Action buttons and custom sound

Buttons and a sound set on the Console’s push compose page reach the device:
  • Buttons arrive as data.actions, a JSON array of {id, label, action_type, action_value, foreground}. The SDK adds them to every notification it builds itself: every AppDNA push its own service or AppDNA.push.handleMessage displays (a data-only message, or a push with a notification block while the app is in the foreground). A push that FCM displays itself — a notification block while the app is in the background — shows no buttons: Android gives FCM’s own display none.
    • A button that opens the app launches it with action_id and the button’s action_type / action_value extras. AppDNA.handlePushTap(intent) tracks push_tapped with the button’s id as action, fires onPushTapped(notification, actionId) and routes the button’s action. deep_link and open_url (the Console’s URL type) go to your deep-link handler; show_screen opens that screen.
    • dismiss, and any button with Open app in foreground switched off, go to the SDK’s PushActionReceiver and do not open the app, as on iOS. The receiver tracks the tap, fires onPushTapped with the button’s id and routes the button’s action. dismiss routes nowhere. Android does not let a notification button start an activity from the background (Android 10+ background-start and Android 12+ trampoline limits), so a route that needs the app on screen, such as show_screen, takes effect only if the app is already in front; keep Open app in foreground on for a button that must show something. If the app was not running, the receiver waits up to 8 seconds for AppDNA.configure (called from your Application.onCreate) before it handles the tap, so the tap is still tracked.
    • A text_reply button carries a RemoteInput whose hint is the button’s value. The typed text reaches onPushTapped as notification.data["reply_text"].
    • The notification is removed when one of its buttons is tapped: by the receiver for a button that does not open the app, and by AppDNA.handlePushTap(intent) for one that does (Android does not remove it on a button tap by itself).
  • Custom sound. The Console value is a file name with its extension. Android plays res/raw/<name>: lower-case, extension dropped, and any character outside a-z, 0-9 and _ replaced by _ (Chime.wav → res/raw/chime). On Android 8+ a notification plays its channel’s sound, so each sound gets its own channel, appdna_sound_<name>. The SDK creates that channel the first time it sees the sound, from any AppDNA message it receives, including one forwarded with handleMessageData. The server names the same channel in android.notification.channel_id (and sound for Android 7 and older). Until the channel exists on a device, FCM falls back to the app’s default channel and its sound. A sound that is not bundled leaves the usual channel and the default sound. A Channel ID set in the campaign is used only for a push without a custom sound (a sound of default is not a custom sound). The server sends it as android.notification.channel_id, which FCM uses when it displays the push, and as channel_id in the message data, which the SDK uses when it displays the push itself. The channel the SDK creates for a sound takes the campaign’s Sound Channel Importance (default high), Name (the sound is appended) and Description, sent as channel_importance, channel_name and channel_description in the message data. The server sends them only with a custom sound; a push without one carries none of them. A channel that already exists on the device keeps its settings. A push without a custom sound that the SDK displays posts on the campaign’s Channel ID, or on the SDK’s default channel, appdna_push, when the campaign sets none. If that channel does not exist on the device yet, the SDK creates it with its own settings: importance high, no description, and the name “Push Notifications” for appdna_push or “Push Notifications (<id>)” for any other channel id (for example “Push Notifications (orders)”). It ignores the push’s channel_importance, channel_name and channel_description there, so a campaign sent at Low or Min importance never changes the channel every later push uses. A channel_group in the message data still puts a channel other than appdna_push in that group (the SDK creates the group if needed); appdna_push joins no group. The campaign’s Android Channel Group sets it: the server sends it as channel_group in the message data together with the campaign’s Channel ID, and only for a push without a custom sound. The SDK looks the sound up by name at runtime, so resource shrinking (shrinkResources true) cannot see that it is used and removes it from a release build. Keep your push sounds with a res/raw/keep.xml in the app:
  • Image, colour and priority. The campaign’s Media URL and Android Color are sent where FCM’s own display reads them (notification.image, android.notification.color) and also in the message data as image_url and color, where the SDK reads them when it displays the push itself, so a push the SDK builds shows the same image and accent colour.
  • Android Priority is FCM’s delivery priority. It is sent as the message’s android.priority (normal or high; high when the campaign leaves it unset), which tells FCM how urgently to deliver the message. When the campaign sets it, it is also sent as data.priority. A push the SDK builds itself is posted with NotificationCompat.PRIORITY_HIGH when data.priority is high, and with PRIORITY_DEFAULT when it is normal or not sent. Android reads that value only on 7.1 and lower. On 8.0 and later the notification channel’s importance decides how the push is shown.
  • Media Type. It is sent as data.media_type, only together with a Media URL. When the SDK builds the push itself, a value containing gif decodes the media’s first GIF frame, video or video/… a frame near the start of the video, and image/… the image. Anything else, including image and no value at all, falls back to the URL’s file extension: .gif is a GIF, .mp4, .mov, .webm, .m4v and .3gp are videos, and every other URL is decoded as an image. When FCM displays the push (notification.image), FCM shows the image itself and Media Type is not used.

PushPayload

The PushPayload data class contains the notification content:

PushAction

The PushAction data class defines the action associated with a notification tap:

Auto-Tracked Events

The SDK automatically tracks the following push-related events:
Auto-tracked events are sent alongside any manually tracked events. You do not need to track these events yourself.

Full Example

Background messages in Flutter / React Native apps: a message that arrives while the app is in the background is handled by FlutterFire’s onBackgroundMessage / React Native Firebase’s setBackgroundMessageHandler; FCM still displays the notification. When the user opens the app from it, both wrappers hand the tap to AppDNA.handlePushTap(intent) themselves, for every kind of tap: a tap that reaches the running app (onNewIntent), the tap that launched it (the launch intent, at configure), and a tap that starts a new activity while the Flutter engine or React Native instance keeps running (that activity’s launch intent: Flutter when the plugin attaches to the activity, React Native when the app resumes). Each intent is handed over once, as a copy, so the activity’s intent keeps its extras. Forwarding the tap yourself is optional and safe — AppDNAPush.handlePushTap() (Flutter) or AppDNA.push.handleTap(data) (both) returns true for a tap already handled and tracks and routes nothing again. Forwarding delivered from those background handlers is not supported in this release.
With the SDK’s own AppDNAMessagingService, token refresh is automatic. If your app uses its own FirebaseMessagingService (see If your app already uses Firebase Messaging), call AppDNA.setPushToken(token) from your onNewToken.

Scheduled pushes in the user’s time zone

A campaign scheduled with Send in user’s timezone reaches each user when their own clock shows the scheduled date and time. Each user gets it once, at the first check (every minute) that finds their local time has reached it — also when a daylight-saving change or a move to another zone happens while the campaign is going out. The audience is fixed when the campaign starts going out, in the first zone to reach the time. From Android SDK 1.0.54, every event the SDK sends carries the device’s time zone (its IANA name, from TimeZone.getDefault()), and AppDNA keeps the latest one on the identified user’s profile — so you need no code for this. Push quiet hours and journey waits that end at a time of day use the same zone. The zone is resolved in this order:
  1. the timezone trait your app passes to identify (an IANA name such as Europe/Berlin), if you set one — it always takes precedence, and the device’s zone never overwrites it;
  2. the zone the user’s device last reported;
  3. the app’s time zone;
  4. UTC.
A value that is not an IANA zone name is ignored at each step. The device’s zone is recorded for identified users only (after identify), from their next event. Set the trait only when the zone your app knows for the user should win over the device’s — for example a zone the user picked in your settings:

Next Steps