Skip to content

Commit a3ec855

Browse files
Updated way of handling notifications to not use AsyncTask
1 parent 3a0ac5c commit a3ec855

9 files changed

+1805
-13
lines changed

iterableapi/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ dependencies {
6363
api 'com.google.firebase:firebase-messaging:20.3.0'
6464
implementation 'com.google.code.gson:gson:2.10.1'
6565
implementation "androidx.security:security-crypto:1.1.0-alpha06"
66+
implementation 'androidx.work:work-runtime:2.9.0'
6667

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

8082
androidTestImplementation 'androidx.test:runner:1.6.2'

iterableapi/src/main/java/com/iterable/iterableapi/IterableFirebaseMessagingService.java

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.google.firebase.messaging.RemoteMessage;
1212

1313
import java.util.Map;
14+
import java.util.UUID;
1415
import java.util.concurrent.ExecutionException;
1516

1617
public class IterableFirebaseMessagingService extends FirebaseMessagingService {
@@ -56,12 +57,13 @@ public static boolean handleMessageReceived(@NonNull Context context, @NonNull R
5657
return false;
5758
}
5859

59-
if (!IterableNotificationHelper.isGhostPush(extras)) {
60+
boolean isGhostPush = IterableNotificationHelper.isGhostPush(extras);
61+
62+
if (!isGhostPush) {
6063
if (!IterableNotificationHelper.isEmptyBody(extras)) {
6164
IterableLogger.d(TAG, "Iterable push received " + messageData);
62-
IterableNotificationBuilder notificationBuilder = IterableNotificationHelper.createNotification(
63-
context.getApplicationContext(), extras);
64-
new IterableNotificationManager().execute(notificationBuilder);
65+
66+
enqueueNotificationWork(context, extras, false);
6567
} else {
6668
IterableLogger.d(TAG, "Iterable OS notification push received");
6769
}
@@ -105,9 +107,7 @@ public static String getFirebaseToken() {
105107
String registrationToken = null;
106108
try {
107109
registrationToken = Tasks.await(FirebaseMessaging.getInstance().getToken());
108-
} catch (ExecutionException e) {
109-
IterableLogger.e(TAG, e.getLocalizedMessage());
110-
} catch (InterruptedException e) {
110+
} catch (ExecutionException | InterruptedException e) {
111111
IterableLogger.e(TAG, e.getLocalizedMessage());
112112
} catch (Exception e) {
113113
IterableLogger.e(TAG, "Failed to fetch firebase token");
@@ -122,17 +122,65 @@ public static String getFirebaseToken() {
122122
* @return Boolean indicating whether the message is an Iterable ghost push or silent push
123123
*/
124124
public static boolean isGhostPush(RemoteMessage remoteMessage) {
125-
Map<String, String> messageData = remoteMessage.getData();
125+
try {
126+
Map<String, String> messageData = remoteMessage.getData();
126127

127-
if (messageData == null || messageData.isEmpty()) {
128+
if (messageData.isEmpty()) {
129+
return false;
130+
}
131+
132+
Bundle extras = IterableNotificationHelper.mapToBundle(messageData);
133+
return IterableNotificationHelper.isGhostPush(extras);
134+
} catch (Exception e) {
135+
IterableLogger.e(TAG, e.getMessage());
128136
return false;
129137
}
138+
}
130139

131-
Bundle extras = IterableNotificationHelper.mapToBundle(messageData);
132-
return IterableNotificationHelper.isGhostPush(extras);
140+
private static void enqueueNotificationWork(@NonNull final Context context, @NonNull final Bundle extras, boolean isGhostPush) {
141+
IterableNotificationWorkScheduler scheduler = new IterableNotificationWorkScheduler(context);
142+
143+
scheduler.scheduleNotificationWork(
144+
extras,
145+
isGhostPush,
146+
new IterableNotificationWorkScheduler.SchedulerCallback() {
147+
@Override
148+
public void onScheduleSuccess(UUID workId) {
149+
IterableLogger.d(TAG, "Notification work scheduled successfully: " + workId);
150+
}
151+
152+
@Override
153+
public void onScheduleFailure(Exception exception, Bundle notificationData) {
154+
IterableLogger.e(TAG, "Failed to schedule notification work", exception);
155+
IterableLogger.e(TAG, "Attempting FALLBACK to direct processing...");
156+
handleFallbackNotification(context, notificationData);
157+
}
158+
}
159+
);
160+
}
161+
162+
private static void handleFallbackNotification(@NonNull Context context, @NonNull Bundle extras) {
163+
try {
164+
IterableNotificationBuilder notificationBuilder = IterableNotificationHelper.createNotification(
165+
context.getApplicationContext(), extras);
166+
if (notificationBuilder != null) {
167+
IterableNotificationHelper.postNotificationOnDevice(context, notificationBuilder);
168+
IterableLogger.d(TAG, "✓ FALLBACK succeeded - notification posted directly");
169+
} else {
170+
IterableLogger.w(TAG, "✗ FALLBACK: Notification builder is null");
171+
}
172+
} catch (Exception fallbackException) {
173+
IterableLogger.e(TAG, "✗ CRITICAL: FALLBACK also failed!", fallbackException);
174+
IterableLogger.e(TAG, "NOTIFICATION WILL NOT BE DISPLAYED");
175+
}
133176
}
134177
}
135178

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

138186
@Override

iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationHelper.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ static Bundle mapToBundle(Map<String, String> map) {
9898
static class IterableNotificationHelperImpl {
9999

100100
public IterableNotificationBuilder createNotification(Context context, Bundle extras) {
101+
if (extras == null) {
102+
IterableLogger.w(IterableNotificationBuilder.TAG, "Notification extras is null. Skipping.");
103+
return null;
104+
}
105+
101106
String applicationName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
102107
String title = null;
103108
String notificationBody = null;
@@ -436,7 +441,7 @@ boolean isIterablePush(Bundle extras) {
436441

437442
boolean isGhostPush(Bundle extras) {
438443
boolean isGhostPush = false;
439-
if (extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
444+
if (extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
440445
String iterableData = extras.getString(IterableConstants.ITERABLE_DATA_KEY);
441446
IterableNotificationData data = new IterableNotificationData(iterableData);
442447
isGhostPush = data.getIsGhostPush();
@@ -447,7 +452,7 @@ boolean isGhostPush(Bundle extras) {
447452

448453
boolean isEmptyBody(Bundle extras) {
449454
String notificationBody = "";
450-
if (extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
455+
if (extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
451456
notificationBody = extras.getString(IterableConstants.ITERABLE_DATA_BODY, "");
452457
}
453458

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.iterable.iterableapi;
2+
3+
import android.content.Context;
4+
import android.os.Bundle;
5+
6+
import androidx.annotation.NonNull;
7+
import androidx.annotation.Nullable;
8+
import androidx.annotation.VisibleForTesting;
9+
import androidx.work.OneTimeWorkRequest;
10+
import androidx.work.OutOfQuotaPolicy;
11+
import androidx.work.WorkManager;
12+
13+
import java.util.UUID;
14+
15+
/**
16+
* Manages scheduling of notification processing work using WorkManager.
17+
* This class is responsible for:
18+
* - Creating WorkManager requests for notification processing
19+
* - Enqueueing work with expedited execution for high-priority notifications
20+
* - Providing callback interface for success/failure handling
21+
* - Comprehensive logging of scheduling operations
22+
*/
23+
class IterableNotificationWorkScheduler {
24+
25+
private static final String TAG = "IterableNotificationWorkScheduler";
26+
27+
private final Context context;
28+
private final WorkManager workManager;
29+
30+
/**
31+
* Callback interface for work scheduling results.
32+
* Allows caller to handle success/failure appropriately.
33+
*/
34+
public interface SchedulerCallback {
35+
/**
36+
* Called when work is successfully scheduled.
37+
* @param workId UUID of the scheduled work
38+
*/
39+
void onScheduleSuccess(UUID workId);
40+
41+
/**
42+
* Called when work scheduling fails.
43+
* @param exception The exception that caused the failure
44+
* @param notificationData The original notification data (for fallback)
45+
*/
46+
void onScheduleFailure(Exception exception, Bundle notificationData);
47+
}
48+
49+
/**
50+
* Constructor for production use.
51+
* Initializes with application context and default WorkManager instance.
52+
*
53+
* @param context Application or service context
54+
*/
55+
public IterableNotificationWorkScheduler(@NonNull Context context) {
56+
this(context, WorkManager.getInstance(context));
57+
}
58+
59+
/**
60+
* Constructor for testing.
61+
* Allows injection of mock WorkManager for unit testing.
62+
*
63+
* @param context Application or service context
64+
* @param workManager WorkManager instance (can be mocked for tests)
65+
*/
66+
@VisibleForTesting
67+
IterableNotificationWorkScheduler(@NonNull Context context, @NonNull WorkManager workManager) {
68+
this.context = context.getApplicationContext();
69+
this.workManager = workManager;
70+
}
71+
72+
/**
73+
* Schedules notification processing work using WorkManager.
74+
*
75+
* Creates an expedited OneTimeWorkRequest and enqueues it with WorkManager.
76+
* Expedited execution ensures high-priority notifications are processed promptly,
77+
* with quota exemption when called from FCM onMessageReceived.
78+
*
79+
* @param notificationData Bundle containing notification data
80+
* @param isGhostPush Whether this is a ghost/silent push
81+
* @param callback Optional callback for success/failure (can be null)
82+
*/
83+
public void scheduleNotificationWork(
84+
@NonNull Bundle notificationData,
85+
boolean isGhostPush,
86+
@Nullable SchedulerCallback callback) {
87+
88+
IterableLogger.d(TAG, "========================================");
89+
IterableLogger.d(TAG, "Scheduling notification work");
90+
IterableLogger.d(TAG, "Bundle keys: " + notificationData.keySet().size());
91+
IterableLogger.d(TAG, "Is ghost push: " + isGhostPush);
92+
93+
try {
94+
IterableLogger.d(TAG, "Step 1: Creating Worker input data");
95+
androidx.work.Data inputData = IterableNotificationWorker.createInputData(
96+
notificationData,
97+
isGhostPush
98+
);
99+
IterableLogger.d(TAG, " ✓ Worker input data created successfully");
100+
101+
IterableLogger.d(TAG, "Step 2: Building expedited WorkRequest");
102+
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(IterableNotificationWorker.class)
103+
.setInputData(inputData)
104+
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
105+
.build();
106+
IterableLogger.d(TAG, " ✓ WorkRequest built with expedited execution");
107+
108+
IterableLogger.d(TAG, "Step 3: Enqueueing work with WorkManager");
109+
workManager.enqueue(workRequest);
110+
111+
UUID workId = workRequest.getId();
112+
IterableLogger.d(TAG, " ✓ Work enqueued successfully");
113+
IterableLogger.d(TAG, "");
114+
IterableLogger.d(TAG, "✓ NOTIFICATION WORK SCHEDULED");
115+
IterableLogger.d(TAG, " Work ID: " + workId);
116+
IterableLogger.d(TAG, " Priority: EXPEDITED (high-priority notification)");
117+
IterableLogger.d(TAG, " Worker: " + IterableNotificationWorker.class.getSimpleName());
118+
IterableLogger.d(TAG, "========================================");
119+
120+
if (callback != null) {
121+
callback.onScheduleSuccess(workId);
122+
}
123+
124+
} catch (Exception e) {
125+
IterableLogger.e(TAG, "========================================");
126+
IterableLogger.e(TAG, "✗ FAILED TO SCHEDULE NOTIFICATION WORK");
127+
IterableLogger.e(TAG, "Error type: " + e.getClass().getSimpleName());
128+
IterableLogger.e(TAG, "Error message: " + e.getMessage());
129+
IterableLogger.e(TAG, "========================================");
130+
131+
if (callback != null) {
132+
callback.onScheduleFailure(e, notificationData);
133+
}
134+
}
135+
}
136+
137+
@VisibleForTesting
138+
Context getContext() {
139+
return context;
140+
}
141+
142+
@VisibleForTesting
143+
WorkManager getWorkManager() {
144+
return workManager;
145+
}
146+
}

0 commit comments

Comments
 (0)