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 key | Rendered as |
|---|---|
title, body | Text; BigTextStyle for long bodies |
image | BigPictureStyle and large icon |
icon | Large icon when no image |
actions (JSON) | Up to 3 buttons; each press is tracked as clicked |
sound | default, a raw resource name, or none for silent |
group | Notification group key |
badge | App badge count via setNumber |
url, msgId, campaignId, everything else | Carried 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(needsANDROID_HOME).- On a device or emulator with Google Play services: Send a test in the dashboard's schedule step, or
POST /v1/notificationstargeting yourexternalId. - Watch
adb logcat -s OpenNotificationfor registration and tracking calls.