Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions iterableapi/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ dependencies {
api 'com.google.firebase:firebase-messaging:20.3.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation "androidx.security:security-crypto:1.1.0-alpha06"
implementation 'androidx.work:work-runtime:2.9.0'

testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:runner:1.6.2'
Expand All @@ -75,6 +76,7 @@ dependencies {
testImplementation 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.3'
testImplementation 'org.skyscreamer:jsonassert:1.5.0'
testImplementation 'androidx.work:work-testing:2.9.0'
testImplementation project(':iterableapi')

androidTestImplementation 'androidx.test:runner:1.6.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.google.firebase.messaging.RemoteMessage;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;

public class IterableFirebaseMessagingService extends FirebaseMessagingService {
Expand Down Expand Up @@ -56,12 +57,13 @@ public static boolean handleMessageReceived(@NonNull Context context, @NonNull R
return false;
}

if (!IterableNotificationHelper.isGhostPush(extras)) {
boolean isGhostPush = IterableNotificationHelper.isGhostPush(extras);

if (!isGhostPush) {
if (!IterableNotificationHelper.isEmptyBody(extras)) {
IterableLogger.d(TAG, "Iterable push received " + messageData);
IterableNotificationBuilder notificationBuilder = IterableNotificationHelper.createNotification(
context.getApplicationContext(), extras);
new IterableNotificationManager().execute(notificationBuilder);

enqueueNotificationWork(context, extras, false);
} else {
IterableLogger.d(TAG, "Iterable OS notification push received");
}
Expand Down Expand Up @@ -105,9 +107,7 @@ public static String getFirebaseToken() {
String registrationToken = null;
try {
registrationToken = Tasks.await(FirebaseMessaging.getInstance().getToken());
} catch (ExecutionException e) {
IterableLogger.e(TAG, e.getLocalizedMessage());
} catch (InterruptedException e) {
} catch (ExecutionException | InterruptedException e) {
IterableLogger.e(TAG, e.getLocalizedMessage());
} catch (Exception e) {
IterableLogger.e(TAG, "Failed to fetch firebase token");
Expand All @@ -122,17 +122,65 @@ public static String getFirebaseToken() {
* @return Boolean indicating whether the message is an Iterable ghost push or silent push
*/
public static boolean isGhostPush(RemoteMessage remoteMessage) {
Map<String, String> messageData = remoteMessage.getData();
try {
Map<String, String> messageData = remoteMessage.getData();

if (messageData == null || messageData.isEmpty()) {
if (messageData.isEmpty()) {
return false;
}

Bundle extras = IterableNotificationHelper.mapToBundle(messageData);
return IterableNotificationHelper.isGhostPush(extras);
} catch (Exception e) {
IterableLogger.e(TAG, e.getMessage());
return false;
}
}

Bundle extras = IterableNotificationHelper.mapToBundle(messageData);
return IterableNotificationHelper.isGhostPush(extras);
private static void enqueueNotificationWork(@NonNull final Context context, @NonNull final Bundle extras, boolean isGhostPush) {
IterableNotificationWorkScheduler scheduler = new IterableNotificationWorkScheduler(context);

scheduler.scheduleNotificationWork(
extras,
isGhostPush,
new IterableNotificationWorkScheduler.SchedulerCallback() {
@Override
public void onScheduleSuccess(UUID workId) {
IterableLogger.d(TAG, "Notification work scheduled successfully: " + workId);
}

@Override
public void onScheduleFailure(Exception exception, Bundle notificationData) {
IterableLogger.e(TAG, "Failed to schedule notification work", exception);
IterableLogger.e(TAG, "Attempting FALLBACK to direct processing...");
handleFallbackNotification(context, notificationData);
}
}
);
}

private static void handleFallbackNotification(@NonNull Context context, @NonNull Bundle extras) {
try {
IterableNotificationBuilder notificationBuilder = IterableNotificationHelper.createNotification(
context.getApplicationContext(), extras);
if (notificationBuilder != null) {
IterableNotificationHelper.postNotificationOnDevice(context, notificationBuilder);
IterableLogger.d(TAG, "✓ FALLBACK succeeded - notification posted directly");
} else {
IterableLogger.w(TAG, "✗ FALLBACK: Notification builder is null");
}
} catch (Exception fallbackException) {
IterableLogger.e(TAG, "✗ CRITICAL: FALLBACK also failed!", fallbackException);
IterableLogger.e(TAG, "NOTIFICATION WILL NOT BE DISPLAYED");
}
}
}

/**
* @deprecated This class is no longer used. Notification processing now uses WorkManager
* to comply with Firebase best practices. This class is kept for backwards compatibility only.
*/
@Deprecated
class IterableNotificationManager extends AsyncTask<IterableNotificationBuilder, Void, Void> {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ static Bundle mapToBundle(Map<String, String> map) {
static class IterableNotificationHelperImpl {

public IterableNotificationBuilder createNotification(Context context, Bundle extras) {
if (extras == null) {
IterableLogger.w(IterableNotificationBuilder.TAG, "Notification extras is null. Skipping.");
return null;
}

String applicationName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
String title = null;
String notificationBody = null;
Expand Down Expand Up @@ -436,7 +441,7 @@ boolean isIterablePush(Bundle extras) {

boolean isGhostPush(Bundle extras) {
boolean isGhostPush = false;
if (extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
if (extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
String iterableData = extras.getString(IterableConstants.ITERABLE_DATA_KEY);
IterableNotificationData data = new IterableNotificationData(iterableData);
isGhostPush = data.getIsGhostPush();
Expand All @@ -447,7 +452,7 @@ boolean isGhostPush(Bundle extras) {

boolean isEmptyBody(Bundle extras) {
String notificationBody = "";
if (extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
if (extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
notificationBody = extras.getString(IterableConstants.ITERABLE_DATA_BODY, "");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.iterable.iterableapi;

import android.content.Context;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.work.OneTimeWorkRequest;
import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkManager;

import java.util.UUID;

/**
* Manages scheduling of notification processing work using WorkManager.
* This class is responsible for:
* - Creating WorkManager requests for notification processing
* - Enqueueing work with expedited execution for high-priority notifications
* - Providing callback interface for success/failure handling
* - Comprehensive logging of scheduling operations
*/
class IterableNotificationWorkScheduler {

private static final String TAG = "IterableNotificationWorkScheduler";

private final Context context;
private final WorkManager workManager;

/**
* Callback interface for work scheduling results.
* Allows caller to handle success/failure appropriately.
*/
public interface SchedulerCallback {
/**
* Called when work is successfully scheduled.
* @param workId UUID of the scheduled work
*/
void onScheduleSuccess(UUID workId);

/**
* Called when work scheduling fails.
* @param exception The exception that caused the failure
* @param notificationData The original notification data (for fallback)
*/
void onScheduleFailure(Exception exception, Bundle notificationData);
}

/**
* Constructor for production use.
* Initializes with application context and default WorkManager instance.
*
* @param context Application or service context
*/
public IterableNotificationWorkScheduler(@NonNull Context context) {
this(context, WorkManager.getInstance(context));
}

/**
* Constructor for testing.
* Allows injection of mock WorkManager for unit testing.
*
* @param context Application or service context
* @param workManager WorkManager instance (can be mocked for tests)
*/
@VisibleForTesting
IterableNotificationWorkScheduler(@NonNull Context context, @NonNull WorkManager workManager) {
this.context = context.getApplicationContext();
this.workManager = workManager;
}

/**
* Schedules notification processing work using WorkManager.
*
* Creates an expedited OneTimeWorkRequest and enqueues it with WorkManager.
* Expedited execution ensures high-priority notifications are processed promptly,
* with quota exemption when called from FCM onMessageReceived.
*
* @param notificationData Bundle containing notification data
* @param isGhostPush Whether this is a ghost/silent push
* @param callback Optional callback for success/failure (can be null)
*/
public void scheduleNotificationWork(
@NonNull Bundle notificationData,
boolean isGhostPush,
@Nullable SchedulerCallback callback) {

IterableLogger.d(TAG, "========================================");
IterableLogger.d(TAG, "Scheduling notification work");
IterableLogger.d(TAG, "Bundle keys: " + notificationData.keySet().size());
IterableLogger.d(TAG, "Is ghost push: " + isGhostPush);

try {
IterableLogger.d(TAG, "Step 1: Creating Worker input data");
androidx.work.Data inputData = IterableNotificationWorker.createInputData(
notificationData,
isGhostPush
);
IterableLogger.d(TAG, " ✓ Worker input data created successfully");

IterableLogger.d(TAG, "Step 2: Building expedited WorkRequest");
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(IterableNotificationWorker.class)
.setInputData(inputData)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();
IterableLogger.d(TAG, " ✓ WorkRequest built with expedited execution");

IterableLogger.d(TAG, "Step 3: Enqueueing work with WorkManager");
workManager.enqueue(workRequest);

UUID workId = workRequest.getId();
IterableLogger.d(TAG, " ✓ Work enqueued successfully");
IterableLogger.d(TAG, "");
IterableLogger.d(TAG, "✓ NOTIFICATION WORK SCHEDULED");
IterableLogger.d(TAG, " Work ID: " + workId);
IterableLogger.d(TAG, " Priority: EXPEDITED (high-priority notification)");
IterableLogger.d(TAG, " Worker: " + IterableNotificationWorker.class.getSimpleName());
IterableLogger.d(TAG, "========================================");

if (callback != null) {
callback.onScheduleSuccess(workId);
}

} catch (Exception e) {
IterableLogger.e(TAG, "========================================");
IterableLogger.e(TAG, "✗ FAILED TO SCHEDULE NOTIFICATION WORK");
IterableLogger.e(TAG, "Error type: " + e.getClass().getSimpleName());
IterableLogger.e(TAG, "Error message: " + e.getMessage());
IterableLogger.e(TAG, "========================================");

if (callback != null) {
callback.onScheduleFailure(e, notificationData);
}
}
}

@VisibleForTesting
Context getContext() {
return context;
}

@VisibleForTesting
WorkManager getWorkManager() {
return workManager;
}
}
Loading
Loading