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 23ergibt sich aus den aktuellen Firebase SDKs. Ab Android 13 (API 33) ist für die Anzeige von Benachrichtigungen die LaufzeitberechtigungPOST_NOTIFICATIONSerforderlich.
Wie funktioniert es?
NotiPilot 1.0.0 stellt Benachrichtigungen über den Expo Push Service zu. Ihre App:
- ruft den FCM-Token von Firebase ab,
- tauscht diesen Token beim Expo-Token-Service gegen einen Expo Push Token ein,
- registriert den Expo Push Token per
register-devicebei NotiPilot.
Die Benachrichtigungen erreichen das Gerät weiterhin über FCM; in der App genügt ein standardmäßiger FirebaseMessagingService.
1. Vorbereitung (einmalig)
- Fügen Sie Ihre Android-App in der Firebase-Konsole hinzu und legen Sie die Datei
google-services.jsonim Ordnerapp/ab. - 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.)
- 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.
- 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:
plugins {
id 'com.google.gms.google-services' version '4.4.2' apply false
}
app/build.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
<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
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
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
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.jsonhinzugefügt, Schlüssel des FCM-V1-Dienstkontos ins Expo-Projekt hochgeladen - Expo Project ID im Dashboard =
NOTIPILOT_APP_ID - Android Package im Dashboard =
applicationId(appIdbeim Token-Austausch) - Ab Android 13 wird die Berechtigung
POST_NOTIFICATIONSangefragt - Der Benachrichtigungskanal
defaultwird angelegt -
registerDevicewird inonNewTokenaufgerufen -
countryund nach Möglichkeitcitywerden 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. |