Appearance
Android SDK (Kotlin)
Deliver sponsored push notifications in your Android app. The SDK handles device registration, notification display, click tracking and impression measurement — with the ad source fully concealed.
Current version: 0.2.0 — ai.dlserve:dlserve-android:0.2.0.
What you need first
- minSdk 23 (Android 6.0+) and a Java 17 toolchain.
- A Firebase project for your app with:
google-services.jsonin your app module (app/google-services.json), and- the
com.google.gms.google-servicesGradle plugin applied. Without the plugin the JSON is never read, the app gets no FCM token, and no device ever registers.
- Your Firebase service-account JSON uploaded to your dlserve placement — in the dashboard, open your Android placement and upload it there. We send through your own Firebase project, so until that file is uploaded devices register fine and not a single notification is ever delivered.
The Firebase plugin
Root build.gradle.kts:
kotlin
plugins {
id("com.google.gms.google-services") version "4.4.2" apply false
}App module build.gradle.kts:
kotlin
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.gms.google-services")
}The SDK depends on com.google.firebase:firebase-messaging:24.0.0 and exposes it on your compile classpath, so you do not have to declare it yourself. If you already use the Firebase BOM, keep it and pin 33.1.0 or newer — an older BOM pins an older firebase-messaging, and mixing the two is the usual cause of a duplicate-class or NoSuchMethodError build failure.
1. Add the dlserve repository
The SDK is served from https://maven.dlserve.ai. Declare it at settings level — current Android templates run FAIL_ON_PROJECT_REPOS, where a repository declared inside a module fails the build.
settings.gradle.kts:
kotlin
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.dlserve.ai")
content { includeGroup("ai.dlserve") }
}
}
}Older projects that still resolve per project declare it in the root build.gradle instead:
groovy
allprojects {
repositories {
google()
mavenCentral()
maven {
url 'https://maven.dlserve.ai'
content { includeGroup 'ai.dlserve' }
}
}
}content { includeGroup(...) } keeps every other dependency lookup away from our host, so your build stays as fast as it was.
2. Add the dependency
In your app module's build.gradle.kts:
kotlin
dependencies {
implementation("ai.dlserve:dlserve-android:0.2.0")
}3. Initialize
Call init once from Application.onCreate(), and register the notification listener there too — a push can wake your process with no Activity alive, and the listener is what tells you a notification was shown or tapped.
kotlin
import ai.dlserve.sdk.Dlserve
import ai.dlserve.sdk.NotificationListener
import android.app.Application
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Dlserve.setNotificationListener(object : NotificationListener {
override fun onDisplayed(title: String, body: String) {
// A sponsored notification was posted to the tray.
}
override fun onOpened(title: String, body: String, clickUrl: String) {
// The user tapped it; the browser is opening.
}
})
Dlserve.init(this, appId = "your-app-id")
}
}appId is the App ID shown on your placement in the dashboard. init does its network work on a background thread and never blocks the main thread.
4. Route FCM messages
Option A — you already have a FirebaseMessagingService (recommended)
kotlin
class MyMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(msg: RemoteMessage) {
if (Dlserve.handleMessageIfDlserve(this, msg)) return
// Your own message handling.
}
override fun onNewToken(token: String) {
Dlserve.onNewToken(token)
// Forward to your own backend too, if you have one.
}
}handleMessageIfDlserve returns true and renders the notification when the message is ours, false otherwise. It never throws into your service: a render failure is logged and swallowed.
If you have already deserialized the payload into a plain map (a game engine or a cross-platform runtime typically has), call Dlserve.handleMessageFromData(context, data) instead — same contract.
Option B — you have no messaging service
The SDK ships ai.dlserve.sdk.DlserveMessagingService disabled, so it can never collide with an app that has its own. Enable it with a manifest-merge override in your app's AndroidManifest.xml — note the xmlns:tools declaration, which tools:replace requires:
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<service
android:name="ai.dlserve.sdk.DlserveMessagingService"
android:enabled="true"
tools:replace="android:enabled" />
</application>
</manifest>5. Ask for the notification permission (Android 13+)
Android 13 and newer will not post a notification until the user grants POST_NOTIFICATIONS. The SDK declares the permission; your app asks for it:
kotlin
if (Dlserve.needsNotificationPermission(context)) {
// Launch your runtime permission request for POST_NOTIFICATIONS.
}Sponsored notifications post to their own "Sponsored offers" channel, which the user can turn off without touching your app's other notifications. While the permission is missing or that channel is off, nothing is posted — and nothing is counted as shown.
6. Choose the status-bar icon
Android masks the small icon to its alpha channel, so it must be a flat white-on-transparent silhouette. The SDK ships one (ic_dlserve_notification). To use your own:
kotlin
Dlserve.setSmallIcon(R.drawable.ic_stat_myapp)Users and tags
kotlin
Dlserve.login("user-123") // associate this device with your user id
Dlserve.logout() // clear the association — local only, see below
Dlserve.setTags(mapOf(
"plan" to "premium",
"trial" to null, // a null value deletes the key
))Limits — exceeding one throws IllegalArgumentException on the calling thread, so a bad value is caught in development instead of vanishing:
| Field | Limit |
|---|---|
externalId | ≤ 256 characters |
| number of tags | ≤ 20 |
| tag key | ≤ 64 characters |
| tag value | ≤ 128 characters |
logout() is local-only. It clears the stored user id on the device and sends nothing. It does not stop notifications and is not an opt-out.
Letting users turn notifications off
kotlin
Dlserve.setPushEnabled(context, false) // unregisters the device and stops rendering
Dlserve.setPushEnabled(context, true) // registers again
Dlserve.isPushEnabled(context) // current state, default trueThe choice is stored on the device and survives restarts. While it is off, init, onNewToken and the message handlers do nothing at all. Give users this switch somewhere in your settings screen — the system-level "Sponsored offers" channel toggle is a fallback, not a substitute.
API reference
| Method | Description |
|---|---|
Dlserve.init(context, appId) | Initialize the SDK and register the device. Call once, from Application.onCreate(). |
Dlserve.login(externalId) | Associate a user id (≤ 256 chars). Throws IllegalArgumentException if longer. |
Dlserve.logout() | Clear the user association. Local only — not an opt-out. |
Dlserve.setTags(Map<String, String?>) | Set tags; a null value deletes the key. Throws IllegalArgumentException over the limits. |
Dlserve.setPushEnabled(context, enabled) | Turn sponsored notifications on or off for this device. Persisted. |
Dlserve.isPushEnabled(context) | Whether they are on. Default true. |
Dlserve.onNewToken(token) | Forward an FCM token rotation. |
Dlserve.handleMessageIfDlserve(context, msg) | Route a RemoteMessage; true = handled by us. |
Dlserve.handleMessageFromData(context, data) | Same, for an already-deserialized data map. |
Dlserve.needsNotificationPermission(context) | Whether POST_NOTIFICATIONS still has to be requested. |
Dlserve.setNotificationListener(listener) | Observe onDisplayed / onOpened. Set it in Application.onCreate(). |
Dlserve.setSmallIcon(resId) | Override the status-bar icon. |
Troubleshooting
| What you see | Why | Fix |
|---|---|---|
Could not find ai.dlserve:dlserve-android:0.2.0 | The repository is missing | Step 1 — and check it is in settings.gradle.kts, not a module |
Build was configured to prefer settings repositories over project repositories | The repository was declared inside a module | Move it to settings.gradle.kts |
Logcat: Dlserve: no FCM token — apply the com.google.gms.google-services plugin and put google-services.json in your app module | The google-services plugin or google-services.json is missing | "What you need first" |
| Devices register, no notification ever arrives | The service-account JSON is not on the placement | Upload it in the dashboard |
Logcat: Dlserve: config unavailable for appId '…' — dropping this push | The App ID is wrong, or it is not an Android placement — the push was dropped | Copy the App ID from the placement again, step 3 |
Nothing on Android 13+, logcat: Dlserve: notify blocked: POST_NOTIFICATIONS not granted | POST_NOTIFICATIONS was never granted | Step 5 |
| Nothing although the permission is granted | The user turned off "Sponsored offers" in system settings | Nothing to fix — that is the opt-out working |
IllegalArgumentException from login / setTags | A value is over a limit | "Users and tags" |
Duplicate class / NoSuchMethodError around Firebase | An old Firebase BOM | Pin BOM 33.1.0 or newer |
What changed in 0.2.0
0.1.0 was never published to a repository — 0.2.0 is the first version you can resolve. If you were building against a local copy, note:
- The impression pixel and
onDisplayednow fire only when the notification was really shown. - Tapping a notification now calls
onOpenedbefore the browser opens; the callback used to be inert. loginandsetTagsnow throw on invalid input instead of dropping the call silently.- New:
setPushEnabled/isPushEnabled(opt-out) andsetSmallIcon. - The render path can no longer throw into your
FirebaseMessagingService.
Next: Flutter · React Native · Troubleshooting