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çinPOST_NOTIFICATIONSçalışma zamanı izni zorunludur.
Nasıl çalışır?
NotiPilot 1.0.0 bildirimleri Expo Push Service üzerinden teslim eder. Uygulamanız:
- Firebase'den FCM token'ı alır,
- Bu token'ı Expo token servisinde bir Expo Push Token'a dönüştürür,
- Expo Push Token'ı NotiPilot'a
register-deviceile kaydeder.
Bildirimler yine FCM üzerinden cihaza ulaşır; uygulamada standart bir FirebaseMessagingService yeterlidir.
1. Ön hazırlık (bir kez)
- Firebase konsolunda Android uygulamanızı ekleyin,
google-services.jsondosyasınıapp/klasörüne koyun. - 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.)
- 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.
- 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:
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", "\"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
<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
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
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
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.jsoneklendi, FCM V1 servis hesabı anahtarı Expo projesine yüklendi - Paneldeki Expo Project ID =
NOTIPILOT_APP_ID - Paneldeki Android Package =
applicationId(token dönüşümündekiappId) - Android 13+ için
POST_NOTIFICATIONSizni isteniyor -
defaultbildirim kanalı oluşturuluyor -
onNewTokeniçinderegisterDeviceçağrılıyor -
countryve mümkünsecityattributes 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. |