The Pushix Android SDK (com.pushix:pushix-android) is for native Android apps written in Kotlin or Java. It registers the phone with Pushix, shows notifications, tells your app when one is tapped, and shows in-app messages.
For Flutter apps use the Flutter SDK instead (see Flutter SDK).
Requirements
- Android Studio with Android Gradle Plugin 8.x, Kotlin 1.9 or newer (Java apps work too)
minSdk21 andcompileSdk34 or newer. If your app uses Firebase BoM 34 or newer,minSdkmust be 23.- A Firebase project with your Android app in it. Use the project whose service account you upload to Pushix.
1. Add Firebase to the app
In the Firebase console open your project → Project settings → Your apps, add the Android app if it isn't there (same package name as your app), and download google-services.json into the app/ folder.
Add the Google services plugin.
settings.gradle.kts or the root build.gradle.kts:
plugins {
id("com.google.gms.google-services") version "4.4.2" apply false
}app/build.gradle.kts:
plugins {
id("com.android.application")
id("com.google.gms.google-services")
}Use the Firebase project whose service account you upload to Pushix. A different project makes every send fail with SENDER_ID_MISMATCH.
2. Add the SDK
The SDK is in sdks/android/pushix in the Pushix download. Pick one of these.
As a module (simplest). Copy the pushix folder into your project, next to app:
// settings.gradle.kts
include(":pushix")
// app/build.gradle.kts
dependencies {
implementation(project(":pushix"))
}From a local Maven repository. In sdks/android run ./gradlew :pushix:publishToMavenLocal once (or ./gradlew :pushix:publishReleasePublicationToFolderRepository, which writes a repository to pushix/build/repo you can copy to a shared folder or web server). Then:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
mavenLocal() // or: maven { url = uri("/path/to/repo") }
}
}
// app/build.gradle.kts
dependencies {
implementation("com.pushix:pushix-android:1.0.0")
}Use the Maven artifact if your project is on Android Gradle Plugin 9.
The SDK brings firebase-messaging with it, so you don't need to add it yourself. If the app already uses another push SDK, remove it (and its Gradle plugin, if it has one): two push SDKs in one app fight over Firebase messages.
3. Android setup
AndroidManifest.xml — inside <application>:
<!-- Channel and icon for notifications Android shows while the app is closed -->
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="pushix_default" />
<meta-data android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_notification" />The SDK's own manifest already adds the INTERNET and POST_NOTIFICATIONS permissions, its FirebaseMessagingService and two invisible activities. You don't need to declare them.
Notification icon — Android draws the small icon as a white silhouette, so a full-colour launcher icon shows up as a white square. Create a white-on-transparent icon (Android Studio → New → Image Asset → Notification Icons) and name it ic_stat_notification. The SDK uses it automatically. You can also set an icon for all sends in the dashboard (App settings → Android defaults) or per message with small_icon.
Keep icons and sounds in release builds — icons and sounds named only in messages (small_icon, large_icon, android_sound, button icons) look unused to the build and get removed by resource shrinking. Create app/src/main/res/raw/keep.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/ic_stat_*,@raw/*" />If a message names an icon or sound that doesn't exist, the SDK falls back to the default icon and sound instead of dropping the notification.
You don't need ProGuard or R8 rules. The SDK ships its own.
4. Initialize
Call initWithContext once, in your Application class. If you don't have one, create it and add android:name=".App" to <application> in the manifest.
import android.app.Application
import com.pushix.sdk.LogLevel
import com.pushix.sdk.Pushix
class App : Application() {
override fun onCreate() {
super.onCreate()
Pushix.Debug.logLevel = LogLevel.WARN // VERBOSE while you set things up
Pushix.initWithContext(this, "YOUR_APP_ID", "https://pushix.in")
Pushix.Notifications.addClickListener { event ->
val data = event.notification.additionalData // your "data" object, or null
if (data?.optString("screen") == "orders") {
startActivity(Intent(this, OrdersActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
}
}The same in Java:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
Pushix.initWithContext(this, "YOUR_APP_ID", "https://pushix.in");
Pushix.getNotifications().addClickListener(event -> {
JSONObject data = event.getNotification().getAdditionalData();
});
}
}The App ID is in the dashboard under App settings. The server URL is https://pushix.in. A malformed App ID or URL throws IllegalArgumentException at start-up, so you notice it right away.
Then ask for permission at a sensible moment, while an Activity is visible (Android 13+ shows the system dialog):
Pushix.Notifications.requestPermission(true) { granted ->
// runs on the main thread
}When the user logs in to your app, link the device to your user id so your backend can target them:
Pushix.login(user.id.toString()) // on login
Pushix.logout() // on logout (also clears tags)That's it. Open the app once and the device appears in the dashboard under Subscribers, marked Android.
API overview
| Call | What it does |
|---|---|
Pushix.initWithContext(context, appId, serverUrl) | Starts the SDK. Call it once, in Application.onCreate() |
Pushix.Debug.logLevel = LogLevel.VERBOSE | How much the SDK writes to Logcat (NONE … VERBOSE) |
Pushix.login(id) / Pushix.logout() | Links the device to your user id / unlinks it (also clears tags) |
Pushix.consentRequired = true / Pushix.consentGiven = true | Registers only after the user agrees |
Pushix.Notifications.requestPermission(true) { granted -> } / .permission / addPermissionObserver | Notification permission |
Pushix.Notifications.addClickListener / addForegroundLifecycleListener / addSilentDataListener (and remove…) | Taps, notifications arriving while the app is open, data-only messages |
Pushix.Notifications.clearAllNotifications() / removeNotification(id) | Removes notifications from the shade |
Pushix.User.addTag(k, v) / addTags / removeTag / removeTags / getTags() | Tags |
Pushix.User.setLanguage("hi") / externalId | Message language; the id passed to login |
Pushix.User.pushSubscription.id / .token / .optedIn / optIn() / optOut() / addObserver | Player id, FCM token and opt-in state of this device |
Pushix.InAppMessages.addTrigger / addTriggers / removeTrigger / removeTriggers / clearTriggers / paused | In-app message triggers, and holding messages back |
Pushix.InAppMessages.addClickListener / addLifecycleListener | In-app message events |
Pushix.handleMessage(context, message) / Pushix.onNewToken(context, token) / Pushix.isPushixMessage(message) | For apps with their own FirebaseMessagingService |
From Java, use the getters: Pushix.getNotifications(), getUser(), getInAppMessages(), getDebug().
The listener types (PushixNotificationClickListener, PushixNotificationLifecycleListener, PushixPermissionObserver, PushixSilentDataListener, PushixPushSubscriptionObserver, PushixInAppMessageClickListener) are fun interfaces, so Kotlin lambdas work as well as object : implementations. PushixInAppMessageLifecycleListener has four optional methods (onWillDisplay, onDidDisplay, onWillDismiss, onDidDismiss).
Permission
Pushix.Notifications.permission // true if notifications can be shown now
Pushix.Notifications.requestPermission(true) { granted -> }
Pushix.Notifications.addPermissionObserver { granted -> }- Android 13+ shows the system dialog. Older Android versions have no dialog; notifications are on unless the user turned them off.
- With
true(fallback to settings), when Android won't show the dialog any more (the user said no twice, or switched notifications off in Settings), the SDK asks "Notifications are off. Open Settings?" and opens the app's notification settings. Withfalse, nothing is shown and the callback getsfalse. - Call it while one of your Activities is visible. From the background the callback gets the current value and nothing is shown.
- The dialog texts are string resources (
pushix_permission_title,pushix_permission_message,pushix_permission_open_settings,pushix_permission_not_now). Override them in yourres/values*/strings.xmlto change or translate them.
Pushix.User.pushSubscription.optOut() stops push to this device without removing the permission (the server marks it unsubscribed); optIn() turns it back on and asks for permission if needed.
User, login and tags
Tags are key–value strings used for segments, filters and {{ personalisation }}.
Pushix.User.addTags(mapOf("level" to 7, "plan" to "gold", "last_bonus" to System.currentTimeMillis() / 1000))
Pushix.User.removeTag("plan")
val tags = Pushix.User.getTags() // tags set from this device; tags your backend set are not included
Pushix.User.setLanguage("hi") // pick the Hindi translation of messagesNumbers are sent as text. Your backend can also set tags by user id without the app: PUT /api/v1/apps/{app_id}/users/{external_user_id}.
Changes made while offline are saved on the phone and sent when the server can be reached, with retries.
Pushix.User.pushSubscription.id // player id, "" until registered
Pushix.User.pushSubscription.token // FCM token
Pushix.User.pushSubscription.addObserver { state ->
// state.previous / state.current: id, token, optedIn
}Calling login, logout or the User methods before initWithContext throws IllegalStateException.
Handling taps
Pushix.Notifications.addClickListener { event ->
val n = event.notification
n.notificationId // id from the REST API
n.additionalData // your "data" object (JSONObject), or null
n.launchURL // your "url"
event.result.actionId // button id, or null for the notification itself
event.result.url // the button's url, else the notification's url
}- Listeners run on the main thread.
- Taps that open the app from closed are delivered as soon as you add the first click listener, so add it in
Application.onCreate(). - Each tap is reported to the server and shows up as "Clicked" in the dashboard. A tap on the very first launch is reported once the device has registered.
- A button tap dismisses its notification.
What opens. When the notification (or button) has a URL, the SDK opens it: http:// and https:// links in the browser, anything else (myshop://orders) as a deep link, which opens the screen in your app that declares that scheme in an <intent-filter>. Without a URL, tapping opens your app's launcher Activity, like tapping the app icon. Change this with manifest meta-data inside <application>:
<!-- Don't open URLs. Open the app instead; your click listener gets event.result.url. -->
<meta-data android:name="com.pushix.suppressLaunchURLs" android:value="true" />
<!-- Open nothing at all, not even the app. Your click listener decides what to start. -->
<meta-data android:name="com.pushix.NotificationOpened.DEFAULT" android:value="DISABLE" />If your click listener navigates to a screen itself, set suppressLaunchURLs so the deep link isn't opened twice. Note that the Flutter SDK never opens deep links by itself; the Android SDK does.
Notifications Android draws itself. Plain messages are shown by Android while your app is closed (see How notifications are drawn). Tapping one starts your launcher Activity, and the SDK finds the tap in that Activity's intent. This works without any code as long as your Activities extend ComponentActivity (AppCompatActivity, FragmentActivity). With a plain android.app.Activity that uses launchMode="singleTop" or singleTask, call setIntent(intent) in onNewIntent so the SDK sees the tap.
While the app is open
Notifications are shown even when the app is in the foreground. To hide or delay one:
Pushix.Notifications.addForegroundLifecycleListener { event ->
if (currentScreen == "orders") {
event.preventDefault() // don't show it
// event.notification.display() // or show it later
}
}The listener runs on the main thread. Keep it short; the notification waits for it.
Notification icon and channel
- Small icon. The SDK uses, in order: the message's
small_icon(a drawable name in your app), thecom.google.firebase.messaging.default_notification_iconmeta-data, a drawable calledic_stat_notification, and finally a white bell built into the SDK. - Channel. The SDK creates the
pushix_defaultchannel ("Notifications", high importance) on start-up, so notifications pop up on screen. To rename it, override thepushix_default_channel_nameandpushix_default_channel_descriptionstring resources. - Your own channels. Create them in your app (for example one with a custom sound) and send their id as
android_channel_id. If a message names a channel the app hasn't created, the SDK usespushix_default. - Sound. On Android 8+ the channel decides the sound;
android_soundonly applies to Android 7 and older. Put sound files inres/raw. - Accent colour, large icon, big picture, group, lock-screen visibility, collapse id all work as described in the REST API. Messages in the same
android_groupare bundled with a summary on Android 7+.
In-app messages
Create them in the dashboard under In-app messages: a pop-up, card, top or bottom banner, or full screen, with your colours, picture and up to three buttons. Tick the Android platform. The SDK downloads the ones meant for this device each time a session starts and shows them by their trigger:
| Trigger | Shows |
|---|---|
| When the app opens | On each new session (launch, or back after 30+ seconds) |
| From the Nth session | When the app opens and the device has had at least N sessions |
| After some time in the app | N seconds into the session |
| When my code sets a trigger | When your code calls addTrigger with a matching key and value |
Pushix.InAppMessages.addTrigger("cart_value", 1200)
Pushix.InAppMessages.paused = true // e.g. during a game round
Pushix.InAppMessages.paused = false
Pushix.InAppMessages.addClickListener { event ->
// event.result.actionId is the button id, event.result.url its link, event.result.action its type
if (event.result.action == "deep_link" && event.result.url == "myshop://account") {
startActivity(Intent(this, KycActivity::class.java))
}
}
Pushix.InAppMessages.addLifecycleListener(object : PushixInAppMessageLifecycleListener {
override fun onDidDisplay(event: PushixInAppMessageDidDisplayEvent) { /* pause a video */ }
override fun onDidDismiss(event: PushixInAppMessageDidDismissEvent) { /* resume it */ }
})Messages are drawn on top of the Activity that is in front, so no layout changes are needed. Pop-ups and full-screen messages close with the back button when they are dismissible. Frequency rules (show at most N times, wait between showings) are kept on the device. "Open a link" buttons open the browser; "Deep link" buttons are handed to your click listener, or opened by Android if you have none. Seen, clicked and closed counts appear in the dashboard and in Analytics.
Data-only messages
Messages sent with content_available: true and no contents don't show anything. You get them here, on the main thread, whenever your app's process receives them:
Pushix.Notifications.addSilentDataListener { n ->
if (n.additionalData?.optString("refresh") == "balance") reloadBalance()
}Android may delay them while the phone is idle, and doesn't deliver them at all after the user force-stops the app.
Using your own FirebaseMessagingService
The SDK declares PushixMessagingService in its manifest, so most apps need nothing. If your app already has its own FirebaseMessagingService (for another push provider or for your own data messages), Android delivers messages to yours instead. Forward them to Pushix:
class MyMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
if (Pushix.handleMessage(this, message)) return // a Pushix message, handled
// ...your own messages
}
override fun onNewToken(token: String) {
Pushix.onNewToken(this, token)
// ...your own code
}
}Or extend PushixMessagingService and call super.onMessageReceived(message). Pushix.isPushixMessage(message) tells you whether a message came from Pushix without handling it.
To be sure only your service is used, remove the SDK's from your merged manifest:
<service android:name="com.pushix.sdk.PushixMessagingService" tools:node="remove" />(add xmlns:tools="http://schemas.android.com/tools" to the <manifest> tag).
Privacy consent
To register only after the user agrees:
Pushix.consentRequired = true // before initWithContext
Pushix.initWithContext(this, appId, serverUrl)
// later, when the user agrees:
Pushix.consentGiven = trueBoth values are remembered across launches. Until consent is given the SDK sends nothing to Pushix.
How notifications are drawn
- Plain messages (title, text, picture) are sent as normal notifications. Android shows them even if the app was swiped away. While the app is open the SDK draws them, so your foreground listener can hide them.
- Messages with buttons, a large icon or
android_groupare sent as data messages and drawn by the SDK. These need the app process to start, which some phone makers (Xiaomi, Oppo, Vivo, Realme) block when battery saving is aggressive. Prefer plain messages for anything important.
Testing
- Run the app on a phone or an emulator with Google Play, and allow notifications.
- In the dashboard, open Subscribers, click your device and tick Test device.
- New message → Send to test devices. Try once with the app open, once in the background and once swiped away.
The example app in sdks/android shows initialization, login, tags, permission, the click listener and an in-app trigger. Put your google-services.json in sdks/android/example/ and your App ID and server in sdks/android/gradle.properties to run it.
Troubleshooting
| Problem | Fix |
|---|---|
| Device never appears in Subscribers | Check the server URL (must be reachable from the phone over HTTPS) and the App ID. Set Pushix.Debug.logLevel = LogLevel.VERBOSE and filter Logcat by Pushix. |
| Log says "Firebase is not set up" or "Could not get an FCM token" | Add google-services.json and the com.google.gms.google-services plugin. Test on a device or emulator with Google Play services. |
| Every send fails with SENDER_ID_MISMATCH | The service account in the dashboard is from a different Firebase project than google-services.json. |
IllegalStateException: Call Pushix.initWithContext() | Call initWithContext in Application.onCreate(), before any other Pushix call. |
| Notification shows as a white square | Use a white-on-transparent small icon named ic_stat_notification (see Android setup). |
| No pop-up, only an icon in the status bar | The channel isn't high importance. Use the default pushix_default channel, or create your own with IMPORTANCE_HIGH. If the user lowered it in system settings, only they can raise it again. |
| Permission dialog never appears | It needs Android 13+, targetSdk 33+, and a visible Activity. After two refusals Android stops showing it; pass true to requestPermission to offer Settings instead. |
| Works when open, nothing when closed (Xiaomi etc.) | Ask users to allow Autostart / disable battery optimisation for the app, and send plain messages (no buttons or large icon). |
| Nothing at all after adding Pushix to an app with its own FirebaseMessagingService | Forward messages with Pushix.handleMessage (see Using your own FirebaseMessagingService). |
| Tap opens the browser and the app's screen twice | Your click listener and the SDK both open the URL. Add the com.pushix.suppressLaunchURLs meta-data. |
| Taps on plain notifications aren't reported | Use ComponentActivity/AppCompatActivity, or call setIntent(intent) in onNewIntent. |
| Custom sound doesn't play | On Android 8+ the channel decides the sound. Create a channel with that sound in your app and send its id as android_channel_id. |
| Icon or sound works in debug but not release | Add the keep.xml from Android setup so resource shrinking keeps them. |
| In-app message never shows | Check its status is Live, Android is ticked, and its frequency hasn't been used up on this device. Messages are fetched when a session starts, and wait while paused is true. |
Send your first notification
Create an app in Pushix to get your App ID and REST API key, then follow this guide. The free plan needs no card.