Push & Web Notifications

How notifications reach the Android shade from a Cefrium app -- what works out of the box, what is standards-correct but delivery-gated, and the production path that is validated end-to-end.

PathStatusUse it for
Web Notifications
showNotification()
Works out of the box Showing a notification from page or service-worker code. No Firebase, no setup.
Web Push
PushManager.subscribe()
API present | delivery Google-gated Standards code that registers with FCM. Production delivery needs Google's allowlist or your own push service.
Native FCM
Firebase Cloud Messaging
Production | validated Delivering push to your app in production, via your own Firebase project.

Does Cefrium require Google Play Services?

No. The engine - browsing, rendering, the ad-blocker, and Web Notifications display - runs without Google Play Services, including on de-Googled / AOSP devices. GMS is touched only by opt-in feature paths (native FCM push, WebOTP SMS autofill, passkey verification), and each degrades gracefully when GMS is absent.

One honest detail: the SDK declares a single com.google.android.gms.version manifest value - a manifest-value check that stops Chromium's WebOTP path from throwing, not a runtime dependency on Play Services being installed. The fat AAR also vendors the play-services / Firebase classes (they arrive with Chromium), but they stay inert unless your app opts into FCM with a google-services.json. Native push (section 3) is the only path that needs GMS on the device - and only your app turns it on.

1. Web Notifications - works out of the box

A page asks for permission and posts a notification through its service worker. No Firebase, no extra dependency - the display path lives entirely in the bundled engine.

// page.js
const reg = await navigator.serviceWorker.register('/sw.js');
if (await Notification.requestPermission() === 'granted') {
  // `new Notification(...)` is an illegal constructor on Android --
  // always go through the service-worker registration:
  reg.showNotification('Hello from Cefrium', { body: 'It works.' });
}

Grant the permission natively - answer the prompt, or pre-grant per origin:

// Java / Kotlin (View API)
browser.setPermissionHandler((origin, permissions, cb) -> cb.respond(true));
// or, up front for a known origin:
browser.setContentSetting("https://example.com",
        CefriumBrowser.PERMISSION_NOTIFICATIONS, CefriumBrowser.SETTING_ALLOW);

In Compose, the same call is on the navigator: navigator.setContentSetting(origin, permission, value).

That is the whole integration. The Cefrium Gradle plugin already generates the R classes Chromium's notification code reads, so a standard SDK setup shows web notifications with nothing else added.

2. Web Push - present, standards-correct, delivery is Google-gated

The W3C Push API is exposed and wired through Blink. subscribe() returns a real subscription (with p256dh + auth keys), so the engine registers with FCM:

const sub = await reg.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: <your VAPID public key>,
});
sub.endpoint; // https://fcm.googleapis.com/preprod/wp/<iid>:<token>

A VAPID-signed Web Push to that endpoint is accepted by FCM (HTTP 201) - but does not deliver. The endpoint is on the preprod channel.

Why - and why no code change fixes it

Chromium derives a GCM "product category" from the build's product name. Any non-Chrome Chromium build resolves to org.chromium.android. FCM returns production Web Push only for com.chrome.android (real Google Chrome) and the pre-production channel for everything else. Cefrium is a Chromium build, not Chrome, so it is structurally on preprod. Overriding the product category to impersonate Chrome would violate Google's Terms (and still fails the production allowlist), so Cefrium does not do it - this project ships no hacks.

This is the same wall every Chromium fork hits - Brave, for instance, runs its own push service for this exact reason. The honest options for production Web Push are Google granting the project access, or running your own push relay. Neither is a Cefrium code gap. For production push today, use native FCM below - the production transport on Android anyway.

3. Native FCM - the production path

Native FCM uses the device's Google Play Services to register and deliver, so it is on production GCM regardless of the engine's identity.

Validated end-to-end. On a Play-certified emulator, a Cefrium app's Firebase setup returned a real FCM token, and a push sent via FCM HTTP v1 was received and shown in the notification shade.

The one non-obvious part

A normal app adds com.google.firebase:firebase-messaging. Do not add it here - the Cefrium AAR already vendors the Firebase classes (they come in with Chromium), so the dependency causes Duplicate class errors. The AAR vendors the firebase classes but not the firebase-messaging dependency's manifest entries, so you declare those yourself. That is the entire integration.

Gradle - plugin + google-services only

plugins {
    id 'com.android.application'
    id 'com.cefrium'
    id 'com.google.gms.google-services'   // injects google-services.json
}
dependencies {
    implementation 'com.cefrium:cefrium-sdk:0.5.0'
    // NO firebase-messaging - it is vendored in the AAR.
    configurations.all {
        exclude group: 'com.google.guava', module: 'listenablefuture'
        exclude group: 'org.jspecify'
        exclude group: 'com.squareup.okio'   // the AAR vendors okio
    }
}

Add your google-services.json to the app module; its package_name must equal your applicationId.

Manifest - what firebase-messaging's manifest would have contributed

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<application ...>
  <meta-data android:name="com.google.android.gms.version"
      android:value="@integer/google_play_services_version" />

  <service android:name="com.google.firebase.components.ComponentDiscoveryService"
      android:exported="false" android:directBootAware="true">
    <meta-data android:name="com.google.firebase.components:com.google.firebase.messaging.FirebaseMessagingRegistrar"
        android:value="com.google.firebase.components.ComponentRegistrar" />
    <meta-data android:name="com.google.firebase.components:com.google.firebase.installations.FirebaseInstallationsRegistrar"
        android:value="com.google.firebase.components.ComponentRegistrar" />
    <meta-data android:name="com.google.firebase.components:com.google.firebase.FirebaseCommonKtxRegistrar"
        android:value="com.google.firebase.components.ComponentRegistrar" />
    <meta-data android:name="com.google.firebase.components:com.google.firebase.datatransport.TransportRegistrar"
        android:value="com.google.firebase.components.ComponentRegistrar" />
  </service>

  <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
      android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter><action android:name="com.google.android.c2dm.intent.RECEIVE" /></intent-filter>
  </receiver>
  <service android:name="com.google.firebase.messaging.FirebaseMessagingService"
      android:exported="false" android:directBootAware="true">
    <intent-filter android:priority="-500">
      <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
  </service>
</application>

Get the token, send a push

com.google.firebase.messaging.FirebaseMessaging.getInstance().getToken()
    .addOnCompleteListener(t -> {
        if (t.isSuccessful()) Log.i(TAG, "FCM_TOKEN=" + t.getResult());
    });
# Send via FCM HTTP v1 (service-account OAuth)
ACCESS=$(gcloud auth print-access-token)
curl -s -X POST -H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" \
  "https://fcm.googleapis.com/v1/projects/<PROJECT_ID>/messages:send" \
  -d '{"message":{"token":"<FCM_TOKEN>",
        "notification":{"title":"Cefrium FCM","body":"Native delivery works"}}}'

Play-certified device required. getToken() goes through Play Services' GCM registration, which only completes on a certified device. On uncertified hardware (and on emulator images without the Play Store) it hangs - while FirebaseInstallations.getId() still works. If getToken() never returns, that is your device's GMS, not Cefrium. Use a certified phone or a google_apis_playstore emulator image. On Android 13+, also grant the runtime POST_NOTIFICATIONS permission or the message is dropped silently.

Full working sample

The push-demo in cefrium-sample wires all three paths, with a complete manifest, the Gradle exclusions, and token retrieval. Its README is the in-depth reference.