Geliştiriciler · API v1.0.0

Android Entegrasyonu (Java)

Bu rehber Android Studio ile Java kullanılarak geliştirilen native Android uygulamaları içindir. Kotlin kullanıyorsanız Kotlin rehberine bakın. Genel API referansı için Genel bakış.

Desteklenen sürümler

Bileşen Minimum Önerilen
Android 6.0 (API 23, minSdk 23) targetSdk / compileSdk 35+
Java 11 (kaynak uyumluluğu) 17
Android Gradle Plugin 8.0 güncel
Firebase BoM 33.x güncel

minSdk 23 şartı güncel Firebase SDK'larından gelir. Android 13 (API 33) ve üzerinde bildirim göstermek için POST_NOTIFICATIONS çalışma zamanı izni zorunludur.

Nasıl çalışır?

NotiPilot 1.0.0 bildirimleri Expo Push Service üzerinden teslim eder. Uygulamanız:

  1. Firebase'den FCM token'ı alır,
  2. Bu token'ı Expo token servisinde bir Expo Push Token'a dönüştürür,
  3. Expo Push Token'ı NotiPilot'a register-device ile kaydeder.

Bildirimler yine FCM üzerinden cihaza ulaşır; uygulamada standart bir FirebaseMessagingService yeterlidir.

1. Ön hazırlık (bir kez)

  1. Firebase konsolunda Android uygulamanızı ekleyin, google-services.json dosyasını app/ klasörüne koyun.
  2. expo.dev üzerinde bir proje oluşturun ve proje ID'sini (UUID) not edin. (Expo'yu yalnızca teslimat altyapısı olarak kullanırsınız; uygulamanızın Expo ile yazılmış olması gerekmez.)
  3. Firebase konsolu → Proje Ayarları → Hizmet Hesapları → Yeni özel anahtar oluştur ile FCM V1 servis hesabı JSON'unu indirin ve expo.dev → Proje → Credentials → Android → FCM V1 service account key alanına yükleyin.
  4. NotiPilot panelinde uygulamanızı ekleyin: Expo Project ID = Expo proje ID'si, Expo Access Token = expo.dev → Access Tokens, Android Package = applicationId.

2. Gradle

Proje seviyesi build.gradle:

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", "\"EXPO-PROJE-ID-NIZ\""
    }
    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'
}

Sürüm numaraları örnektir; projenizde güncel sürümleri kullanın.

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 bildirimleri default kanalına gönderir.

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;
    }

    /** Uygulama açılışında (izin verildikten sonra) ve onNewToken'da çağırın. fcmToken null olabilir. */
    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);
            }
        });
    }

    /** Kullanıcı giriş yaptığında çağırın. */
    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);
            }
        });
    }

    /** FCM token'ını Expo Push Token'a dönüştürür. */
    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 içeriği `notification` bloğunda veya `data` içinde (title / message / body) gönderebilir.
        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;

        // Panelden gönderilen özel veri `data["body"]` alanında JSON string olarak gelir.
        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, "Genel", 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 — izin ve kayıt

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();
        }

        // Bildirime tıklanarak açıldıysa özel veriler intent extras içindedir
        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);
        });
    }
}

Kullanıcı giriş yaptığında: NotiPilot.identify(context, user.getId(), null);

Kontrol listesi

  • google-services.json eklendi, FCM V1 servis hesabı anahtarı Expo projesine yüklendi
  • Paneldeki Expo Project ID = NOTIPILOT_APP_ID
  • Paneldeki Android Package = applicationId (token dönüşümündeki appId)
  • Android 13+ için POST_NOTIFICATIONS izni isteniyor
  • default bildirim kanalı oluşturuluyor
  • onNewToken içinde registerDevice çağrılıyor
  • country ve mümkünse city attributes olarak gönderiliyor

Sık karşılaşılan sorunlar

Belirti Çözüm
Token dönüşümü 4xx dönüyor projectId Expo proje ID'si mi? appId uygulamanın paket adıyla aynı mı?
Kayıt başarılı, bildirim gelmiyor Expo projesinde FCM V1 kimlik bilgisi yüklü değil. expo.dev → Credentials → Android'i kontrol edin.
Uygulama arka plandayken bildirim gelmiyor Pil optimizasyonu / üretici kısıtlamaları (Xiaomi, Huawei vb.) ve bildirim izni kontrol edilmeli.
404 unknown_app app_id panelde kayıtlı değil.