Skip to main content

Android SDK (Kotlin)

Kotlin AAR, minSdk 23, zero dependencies beyond Firebase Messaging and AndroidX core. Firebase provides the token and the transport; the SDK draws every notification (the server sends data-only messages) and reports delivered / opened / clicked / dismissed.

Before the app side works you need the service account uploaded and google-services.json in place: Android platform setup.

1 Dependencies

// app/build.gradle.kts
plugins {
id("com.google.gms.google-services")
}
dependencies {
implementation("com.opennotification:sdk:0.1.0")
implementation(platform("com.google.firebase:firebase-bom:33.1.0"))
implementation("com.google.firebase:firebase-messaging")
}

Until the artifact is on Maven Central, build and publish it locally:

cd packages/sdk-android && gradle :sdk:publishToMavenLocal

and add mavenLocal() to your repositories.

2 Initialise

In your Application subclass — it must run before any push can arrive:

class App : Application() {
override fun onCreate() {
super.onCreate()
OpenNotification.init(
this,
"https://push.example.com",
"pk_live_…",
OpenNotification.Config(
channelId = "default",
channelName = "Notifications",
smallIcon = R.drawable.ic_notification, // monochrome, white on transparent
launchActivity = MainActivity::class.java, // optional; launcher activity otherwise
),
)
OpenNotification.onOpen = { message -> Router.open(message.url, message.data) }
OpenNotification.onReceive = { message -> /* foreground arrival */ }
}
}

init creates the notification channel, registers a token the service may already have handed over (cold start via FCM), and asks FirebaseMessaging for the current token. Registration happens automatically from then on, including token refreshes.

The library manifest registers OpenNotificationMessagingService. Nothing to add to yours.

3 Permission (Android 13+)

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
OpenNotification.requestPermission(this) // no-op below API 33
}
}

The request code is OpenNotification.PERMISSION_REQUEST_CODE if you want to observe the result in onRequestPermissionsResult. Declare the permission if your manifest does not already:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

4 Identity and tags

OpenNotification.login("user_123") { error -> }
OpenNotification.setTags(mapOf("plan" to "platinum", "streak" to 42))
OpenNotification.removeTags(listOf("streak"))
OpenNotification.setLanguage("tr")
OpenNotification.logout()
OpenNotification.unsubscribe()
OpenNotification.trackSession() // on foreground (ProcessLifecycleOwner / Activity.onStart); server dedupes

All callbacks are ((Throwable?) -> Unit)? and run off the main thread. login before registration is stored and sent with the token. Semantics: Identity.

Because HttpURLConnection cannot send PATCH, the SDK uses POST /v1/subscriptions/:id/user — the API exposes the same handler on both verbs.

Silent pushes and channels

A campaign with Silent push arrives as a data message with silent = "1": the SDK draws nothing and calls onReceive with message.silent == true and message.data.

OpenNotification.onReceive = { message -> if (message.silent) Sync.run(message.data) }

Channels defined in the dashboard (Android channels) travel with the push: the SDK creates the channel on first use from the definition it carries and posts to it. A channel the device does not have and the push does not describe falls back to Config.channelId. Android never changes an existing channel's importance — a change in the dashboard needs a new channel id.

Sessions

Registration counts the first session. Call OpenNotification.trackSession() whenever the app comes to the foreground (a ProcessLifecycleOwner observer is the usual place); the server applies the 30-minute idle rule, so calling it often is fine.

5 Opens and cold starts

onOpen fires on the main thread for a press on the body or on a button (message.actionId set, message.url replaced by the button's URL if it had one). Opens are reported to the server before your lambda runs.

When the press launches the app, the intent carries the message. Read it in the activity that receives it:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
OpenNotification.messageFrom(intent)?.let { Router.open(it.url, it.data) }
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
OpenNotification.messageFrom(intent)?.let { Router.open(it.url, it.data) }
}

messageFrom returns null for intents that did not come from a notification.

What the SDK draws

From the data message the renderer builds a NotificationCompat notification with:

Data keyRendered as
title, bodyText; BigTextStyle for long bodies
imageBigPictureStyle and large icon
iconLarge icon when no image
actions (JSON)Up to 3 buttons; each press is tracked as clicked
sounddefault, a raw resource name, or none for silent
groupNotification group key
badgeApp badge count via setNumber
url, msgId, campaignId, everything elseCarried in the intent extras

Dismissals are reported via NotificationReceiver on the delete intent.

Already have a FirebaseMessagingService?

Only one service can receive messages. Forward from yours:

override fun onNewToken(token: String) = OpenNotification.register(token)
override fun onMessageReceived(message: RemoteMessage) {
OpenNotificationMessagingService.handle(message) // ignores messages without msgId/title
}

Testing

  • gradle :sdk:testReleaseUnitTest (needs ANDROID_HOME).
  • On a device or emulator with Google Play services: Send a test in the dashboard's schedule step, or POST /v1/notifications targeting your externalId.
  • Watch adb logcat -s OpenNotification for registration and tracking calls.