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+).Prerequisites
Before using push notifications, configure FCM for your app:- Add
google-services-appdna.json(or your project’sgoogle-services.json) to yourapp/src/main/assets/directory. - Apply the
com.google.gms.google-servicesGradle plugin in your app module. - Enable Firebase Cloud Messaging in the Firebase Console.
- 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_idin that JSON), and click Save. It must be a service-account key ("type": "service_account", withclient_emailandprivate_key); other Google credential types, such as a workload identity federation configuration, are refused.
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 ownAppDNAMessagingService 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 SDK1.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:
-
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 ownres/values/: -
Pass the FCM token after
configureand on every token refresh:AppDNA.push.handleMessageData(data)tracks delivery (with itsdelivery_id) and firesonPushReceived; it never posts a notification — display stays with you. Server pushes carry anotificationblock, 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 withAppDNA.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 anotificationblock: 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. -
Taps: call
AppDNA.handlePushTap(intent)from your launch activity (onCreateandonNewIntent), or, when you build the notification yourself,AppDNA.push.handleTapData(data, actionId). Both trackpush_tapped(withdelivery_id), fireonPushTappedand route the AppDNA action; they share one dedup, so calling both for one tap is safe. Both returnfalseand do nothing unless the data carries the AppDNA marker. After a tap it handled,handlePushTap(intent)removes theappdna,push_idanddelivery_idextras 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 callinghandlePushTap. Other extras are left in place.
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 thePOST_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:
onRequestPermissionsResult or call setPushPermission afterwards. On API < 33 the call is a no-op and returns true.
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 ownFirebaseMessagingService — then also remove or disable the SDK’s service and forward AppDNA messages, as shown in If your app already uses Firebase Messaging:
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
Track when a push notification is delivered to the device: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.
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 orAppDNA.push.handleMessagedisplays (a data-only message, or a push with anotificationblock while the app is in the foreground). A push that FCM displays itself — anotificationblock 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_idand the button’saction_type/action_valueextras.AppDNA.handlePushTap(intent)trackspush_tappedwith the button’s id asaction, firesonPushTapped(notification, actionId)and routes the button’s action.deep_linkandopen_url(the Console’s URL type) go to your deep-link handler;show_screenopens that screen. dismiss, and any button with Open app in foreground switched off, go to the SDK’sPushActionReceiverand do not open the app, as on iOS. The receiver tracks the tap, firesonPushTappedwith the button’s id and routes the button’s action.dismissroutes 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 asshow_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 forAppDNA.configure(called from yourApplication.onCreate) before it handles the tap, so the tap is still tracked.- A
text_replybutton carries aRemoteInputwhose hint is the button’s value. The typed text reachesonPushTappedasnotification.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).
- A button that opens the app launches it with
-
Custom sound. The Console value is a file name with its extension. Android plays
res/raw/<name>: lower-case, extension dropped, and any character outsidea-z,0-9and_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 withhandleMessageData. The server names the same channel inandroid.notification.channel_id(andsoundfor 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 ofdefaultis not a custom sound). The server sends it asandroid.notification.channel_id, which FCM uses when it displays the push, and aschannel_idin 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 aschannel_importance,channel_nameandchannel_descriptionin 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” forappdna_pushor “Push Notifications (<id>)” for any other channel id (for example “Push Notifications (orders)”). It ignores the push’schannel_importance,channel_nameandchannel_descriptionthere, so a campaign sent at Low or Min importance never changes the channel every later push uses. Achannel_groupin the message data still puts a channel other thanappdna_pushin that group (the SDK creates the group if needed);appdna_pushjoins no group. The campaign’s Android Channel Group sets it: the server sends it aschannel_groupin 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 ares/raw/keep.xmlin 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 asimage_urlandcolor, 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(normalorhigh;highwhen the campaign leaves it unset), which tells FCM how urgently to deliver the message. When the campaign sets it, it is also sent asdata.priority. A push the SDK builds itself is posted withNotificationCompat.PRIORITY_HIGHwhendata.priorityishigh, and withPRIORITY_DEFAULTwhen it isnormalor 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 containinggifdecodes the media’s first GIF frame,videoorvideo/…a frame near the start of the video, andimage/…the image. Anything else, includingimageand no value at all, falls back to the URL’s file extension:.gifis a GIF,.mp4,.mov,.webm,.m4vand.3gpare 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
ThePushPayload data class contains the notification content:
PushAction
ThePushAction 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, fromTimeZone.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:
- the
timezonetrait your app passes toidentify(an IANA name such asEurope/Berlin), if you set one — it always takes precedence, and the device’s zone never overwrites it; - the zone the user’s device last reported;
- the app’s time zone;
- UTC.
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
- Set up Billing to handle in-app purchases
- Configure Onboarding flows
- Present Paywalls with purchase handling
- Learn about Offline Support for event resilience

