Entwickler · API v1.0.0

Android-Integration (Java)

Dieser Leitfaden richtet sich an native Android-Apps, die mit Android Studio in Java entwickelt werden. Wenn Sie Kotlin verwenden, lesen Sie den Kotlin-Leitfaden. Die allgemeine API-Referenz finden Sie in der Überblick.

Unterstützte Versionen

Komponente Minimum Empfohlen
Android 6.0 (API 23, minSdk 23) targetSdk / compileSdk 35+
Java 11 (Quellkompatibilität) 17
Android Gradle Plugin 8.0 aktuell
Firebase BoM 33.x aktuell

Die Anforderung minSdk 23 ergibt sich aus den aktuellen Firebase SDKs. Ab Android 13 (API 33) ist für die Anzeige von Benachrichtigungen die Laufzeitberechtigung POST_NOTIFICATIONS erforderlich.

Wie funktioniert es?

NotiPilot 1.0.0 stellt Benachrichtigungen über den Expo Push Service zu. Ihre App:

  1. ruft den FCM-Token von Firebase ab,
  2. tauscht diesen Token beim Expo-Token-Service gegen einen Expo Push Token ein,
  3. registriert den Expo Push Token per register-device bei NotiPilot.

Die Benachrichtigungen erreichen das Gerät weiterhin über FCM; in der App genügt ein standardmäßiger FirebaseMessagingService.

1. Vorbereitung (einmalig)

  1. Fügen Sie Ihre Android-App in der Firebase-Konsole hinzu und legen Sie die Datei google-services.json im Ordner app/ ab.
  2. Erstellen Sie auf expo.dev ein Projekt und notieren Sie die Projekt-ID (UUID). (Expo dient nur als Zustellinfrastruktur; Ihre App muss nicht mit Expo geschrieben sein.)
  3. Laden Sie in der Firebase-Konsole unter Projekteinstellungen → Dienstkonten → Neuen privaten Schlüssel generieren die JSON-Datei des FCM-V1-Dienstkontos herunter und laden Sie sie unter expo.dev → Projekt → Credentials → Android → FCM V1 service account key hoch.
  4. Fügen Sie Ihre App im NotiPilot-Dashboard hinzu: Expo Project ID = Expo-Projekt-ID, Expo Access Token = expo.dev → Access Tokens, Android Package = applicationId.

2. Gradle

build.gradle auf Projektebene:

Gradle
plugins {
    id 'com.google.gms.google-services' version '4.4.2' apply false
}

app/build.gradle:

Gradle
plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
}

android {
    defaultConfig {
        minSdk 23
        buildConfigField "String", "NOTIPILOT_BASE_URL", "\"https://app.notipilot.com/api/v1\""
        buildConfigField "String", "NOTIPILOT_APP_ID", "\"IHRE-EXPO-PROJEKT-ID\""
    }
    buildFeatures { buildConfig true }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
}

dependencies {
    implementation platform('com.google.firebase:firebase-bom:33.7.0')
    implementation 'com.google.firebase:firebase-messaging'
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

Die Versionsnummern sind Beispiele; verwenden Sie in Ihrem Projekt die aktuellen Versionen.

3. AndroidManifest.xml

XML
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

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

    <application ...>
        <service
            android:name=".push.NotiPilotMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>

        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="default" />
    </application>
</manifest>

NotiPilot sendet Benachrichtigungen an den Kanal default.

4. NotiPilot.java

Java
package com.example.app.push;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;

import com.example.app.BuildConfig;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.messaging.FirebaseMessaging;

import org.json.JSONObject;

import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public final class NotiPilot {
    private static final OkHttpClient HTTP = new OkHttpClient();
    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
    private static final ExecutorService IO = Executors.newSingleThreadExecutor();

    private NotiPilot() {}

    public interface Callback { void onResult(JSONObject response, Exception error); }

    public static String deviceUid(Context context) {
        SharedPreferences prefs = context.getSharedPreferences("notipilot", Context.MODE_PRIVATE);
        String uid = prefs.getString("device_uid", null);
        if (uid == null) {
            uid = UUID.randomUUID().toString();
            prefs.edit().putString("device_uid", uid).apply();
        }
        return uid;
    }

    /** Beim App-Start (nach erteilter Berechtigung) und in onNewToken aufrufen. fcmToken darf null sein. */
    public static void registerDevice(Context context, String fcmToken,
                                      Map<String, Object> extraAttributes, Callback cb) {
        Context app = context.getApplicationContext();
        IO.execute(() -> {
            try {
                String uid = deviceUid(app);
                String token = fcmToken != null ? fcmToken
                        : Tasks.await(FirebaseMessaging.getInstance().getToken());
                String expoToken = exchangeForExpoToken(app, uid, token);

                JSONObject attributes = new JSONObject();
                attributes.put("locale", Locale.getDefault().getLanguage());   // "tr"
                attributes.put("country", Locale.getDefault().getCountry());   // "TR"
                attributes.put("app_version",
                        app.getPackageManager().getPackageInfo(app.getPackageName(), 0).versionName);
                attributes.put("os_version", Build.VERSION.RELEASE);
                if (extraAttributes != null) {
                    for (Map.Entry<String, Object> e : extraAttributes.entrySet()) {
                        attributes.put(e.getKey(), e.getValue() == null ? JSONObject.NULL : e.getValue());
                    }
                }

                JSONObject body = new JSONObject();
                body.put("app_id", BuildConfig.NOTIPILOT_APP_ID);
                body.put("device_uid", uid);
                body.put("token", expoToken);
                body.put("platform", "android");
                body.put("provider", "expo");
                body.put("attributes", attributes);

                deliver(cb, post("/register-device", body, 0), null);
            } catch (Exception e) {
                deliver(cb, null, e);
            }
        });
    }

    /** Aufrufen, wenn sich der Nutzer anmeldet. */
    public static void identify(Context context, String externalId, Callback cb) {
        Context app = context.getApplicationContext();
        IO.execute(() -> {
            try {
                JSONObject body = new JSONObject();
                body.put("app_id", BuildConfig.NOTIPILOT_APP_ID);
                body.put("device_uid", deviceUid(app));
                body.put("external_id", externalId);
                deliver(cb, post("/identify-device", body, 0), null);
            } catch (Exception e) {
                deliver(cb, null, e);
            }
        });
    }

    /** Tauscht den FCM-Token gegen einen Expo Push Token ein. */
    private static String exchangeForExpoToken(Context context, String uid, String fcmToken) throws Exception {
        JSONObject body = new JSONObject();
        body.put("type", "fcm");
        body.put("deviceId", uid.toLowerCase(Locale.ROOT));
        body.put("development", false);
        body.put("appId", context.getPackageName());
        body.put("deviceToken", fcmToken);
        body.put("projectId", BuildConfig.NOTIPILOT_APP_ID);

        Request request = new Request.Builder()
                .url("https://exp.host/--/api/v2/push/getExpoPushToken")
                .post(RequestBody.create(body.toString(), JSON))
                .build();
        try (Response res = HTTP.newCall(request).execute()) {
            String raw = res.body() != null ? res.body().string() : "{}";
            if (!res.isSuccessful()) {
                throw new IllegalStateException("Expo token exchange failed: " + res.code() + " " + raw);
            }
            return new JSONObject(raw).getJSONObject("data").getString("expoPushToken");
        }
    }

    private static JSONObject post(String path, JSONObject body, int attempt) throws Exception {
        Request request = new Request.Builder()
                .url(BuildConfig.NOTIPILOT_BASE_URL + path)
                .post(RequestBody.create(body.toString(), JSON))
                .build();
        int code;
        JSONObject json;
        try (Response res = HTTP.newCall(request).execute()) {
            code = res.code();
            String raw = res.body() != null ? res.body().string() : "";
            json = new JSONObject(raw.isEmpty() ? "{}" : raw);
        }
        if ((code == 429 || code >= 500) && attempt < 3) {
            Thread.sleep(json.optLong("retry_after", 1L << attempt) * 1000L);
            return post(path, body, attempt + 1);
        }
        return json;
    }

    private static void deliver(Callback cb, JSONObject res, Exception err) {
        if (cb != null) cb.onResult(res, err);
    }
}

5. NotiPilotMessagingService.java

Java
package com.example.app.push;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import com.example.app.MainActivity;
import com.example.app.R;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.Iterator;
import java.util.Map;

public class NotiPilotMessagingService extends FirebaseMessagingService {
    public static final String CHANNEL_ID = "default";

    @Override
    public void onNewToken(@NonNull String token) {
        NotiPilot.registerDevice(getApplicationContext(), token, null, null);
    }

    @Override
    public void onMessageReceived(@NonNull RemoteMessage message) {
        // Expo kann den Inhalt im `notification`-Block oder in `data` (title / message / body) senden.
        Map<String, String> data = message.getData();
        String title = message.getNotification() != null ? message.getNotification().getTitle() : data.get("title");
        String body = message.getNotification() != null ? message.getNotification().getBody() : data.get("message");
        if (title == null) return;

        // Aus dem Dashboard gesendete benutzerdefinierte Daten kommen als JSON-String im Feld `data["body"]` an.
        JSONObject custom = null;
        try {
            if (data.get("body") != null) custom = new JSONObject(data.get("body"));
        } catch (JSONException ignored) { }

        show(this, title, body != null ? body : "", custom);
    }

    public static void createChannel(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel =
                    new NotificationChannel(CHANNEL_ID, "Allgemein", NotificationManager.IMPORTANCE_HIGH);
            context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
        }
    }

    public static void show(Context context, String title, String body, JSONObject data) {
        createChannel(context);

        Intent intent = new Intent(context, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        if (data != null) {
            for (Iterator<String> it = data.keys(); it.hasNext(); ) {
                String key = it.next();
                intent.putExtra(key, data.optString(key));
            }
        }
        int id = (int) System.currentTimeMillis();
        PendingIntent pending = PendingIntent.getActivity(context, id, intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(title)
                .setContentText(body)
                .setStyle(new NotificationCompat.BigTextStyle().bigText(body))
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setAutoCancel(true)
                .setContentIntent(pending);

        NotificationManagerCompat manager = NotificationManagerCompat.from(context);
        if (manager.areNotificationsEnabled()) {
            manager.notify(id, builder.build());
        }
    }
}

6. MainActivity.java — Berechtigung und Registrierung

Java
public class MainActivity extends AppCompatActivity {

    private final ActivityResultLauncher<String> permissionLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
                if (granted) register();
            });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        NotiPilotMessagingService.createChannel(this);

        if (Build.VERSION.SDK_INT >= 33 &&
                checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
        } else {
            register();
        }

        // Wurde die App durch Tippen auf eine Benachrichtigung geöffnet, stehen die benutzerdefinierten Daten in den Intent-Extras
        String screen = getIntent().getStringExtra("screen");
    }

    private void register() {
        Map<String, Object> extra = new HashMap<>();
        extra.put("city", "Istanbul");
        NotiPilot.registerDevice(this, null, extra, (res, err) -> {
            if (err != null) Log.w("NotiPilot", "register failed", err);
        });
    }
}

Wenn sich der Nutzer anmeldet: NotiPilot.identify(context, user.getId(), null);

Checkliste

  • google-services.json hinzugefügt, Schlüssel des FCM-V1-Dienstkontos ins Expo-Projekt hochgeladen
  • Expo Project ID im Dashboard = NOTIPILOT_APP_ID
  • Android Package im Dashboard = applicationId (appId beim Token-Austausch)
  • Ab Android 13 wird die Berechtigung POST_NOTIFICATIONS angefragt
  • Der Benachrichtigungskanal default wird angelegt
  • registerDevice wird in onNewToken aufgerufen
  • country und nach Möglichkeit city werden als Attributes gesendet

Häufige Probleme

Symptom Lösung
Token-Austausch liefert 4xx Ist projectId die Expo-Projekt-ID? Stimmt appId mit dem Paketnamen der App überein?
Registrierung erfolgreich, aber keine Benachrichtigung Im Expo-Projekt sind keine FCM-V1-Credentials hinterlegt. Prüfen Sie expo.dev → Credentials → Android.
Keine Benachrichtigung, wenn die App im Hintergrund ist Akku-Optimierung / Herstellerbeschränkungen (Xiaomi, Huawei usw.) und die Benachrichtigungsberechtigung prüfen.
404 unknown_app app_id ist im Dashboard nicht registriert.