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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
- Replaced the deprecated `AsyncTask`-based push notification handling with `WorkManager` for improved reliability and compatibility with modern Android versions. No action is required.

## [3.6.5]
### Fixed
- Fixed IterableEmbeddedView not having an empty constructor and causing crashes
Expand Down
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
@@ -1,8 +1,8 @@
package com.iterable.iterableapi;

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

import androidx.annotation.NonNull;

import com.google.android.gms.tasks.Tasks;
Expand All @@ -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,17 @@ 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);

if (IterableNotificationHelper.hasAttachmentUrl(extras)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified that only the image attachment needs to schedule the NotificationWork.
Sounds are set as part of the notification category so it doesn't need to be scheduled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah from what i checked we don't need to do the sound async, only images

enqueueNotificationWork(context, extras);
} else {
handleNow(context, extras);
}
} else {
IterableLogger.d(TAG, "Iterable OS notification push received");
}
Expand Down Expand Up @@ -105,9 +111,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,25 +126,66 @@ 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.isEmpty()) {
return false;
}

if (messageData == null || messageData.isEmpty()) {
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) {
IterableNotificationWorkScheduler scheduler = new IterableNotificationWorkScheduler(context);

scheduler.scheduleNotificationWork(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the scheduler actually create the notification on device?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it know the format of how to render the notification since it doesn't call the IterableNotificationHelper.createNotification function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you check IterableNotificationWorker.doWork() you can see we are building the notification there, the unused createNotification apparently is there for backwards compatibility since 2019, that actually should be deprecated in my opinion, we can use this PR to do that

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

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

class IterableNotificationManager extends AsyncTask<IterableNotificationBuilder, Void, Void> {
private static void handleNow(@NonNull Context context, @NonNull Bundle extras) {
try {
IterableNotificationBuilder notificationBuilder = IterableNotificationHelper.createNotification(
context.getApplicationContext(), extras);
if (notificationBuilder != null) {
IterableNotificationHelper.postNotificationOnDevice(context, notificationBuilder);
}
} catch (Exception e) {
IterableLogger.e(TAG, "Failed to post notification directly", e);
}
}

@Override
protected Void doInBackground(IterableNotificationBuilder... params) {
if (params != null && params[0] != null) {
IterableNotificationBuilder notificationBuilder = params[0];
IterableNotificationHelper.postNotificationOnDevice(notificationBuilder.context, notificationBuilder);
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");
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ static boolean isEmptyBody(Bundle extras) {
return instance.isEmptyBody(extras);
}

/**
* Returns whether the notification payload includes an image attachment URL,
* meaning display requires a network image download (long-running work).
* @param extras what is inside the bundle
* @return if it has an attachment url
*/
static boolean hasAttachmentUrl(Bundle extras) {
return instance.hasAttachmentUrl(extras);
}

static Bundle mapToBundle(Map<String, String> map) {
Bundle bundle = new Bundle();
for (Map.Entry<String, String> entry : map.entrySet()) {
Expand All @@ -98,6 +108,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 +451,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,12 +462,26 @@ 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, "");
}

return notificationBody.isEmpty();
}

boolean hasAttachmentUrl(Bundle extras) {
if (extras == null || !extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
return false;
}
try {
String iterableData = extras.getString(IterableConstants.ITERABLE_DATA_KEY);
JSONObject iterableJson = new JSONObject(iterableData);
String attachmentUrl = iterableJson.optString(IterableConstants.ITERABLE_DATA_PUSH_IMAGE, "");
return !attachmentUrl.isEmpty();
} catch (Exception e) {
return false;
}
}
}

@Nullable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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;

class IterableNotificationWorkScheduler {

private static final String TAG = "IterableNotificationWorkScheduler";

private final Context context;
private final WorkManager workManager;

interface SchedulerCallback {
void onScheduleSuccess(UUID workId);
void onScheduleFailure(Exception exception, Bundle notificationData);
}

IterableNotificationWorkScheduler(@NonNull Context context) {
this(context, WorkManager.getInstance(context));
}

@VisibleForTesting
IterableNotificationWorkScheduler(@NonNull Context context, @NonNull WorkManager workManager) {
this.context = context.getApplicationContext();
this.workManager = workManager;
}

void scheduleNotificationWork(
@NonNull Bundle notificationData,
@Nullable SchedulerCallback callback) {

try {
androidx.work.Data inputData = IterableNotificationWorker.createInputData(
notificationData
);

OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(IterableNotificationWorker.class)
.setInputData(inputData)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();

workManager.enqueue(workRequest);

UUID workId = workRequest.getId();
IterableLogger.d(TAG, "Notification work scheduled: " + workId);

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

} catch (Exception e) {
IterableLogger.e(TAG, "Failed to schedule notification work", e);

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

@VisibleForTesting
Context getContext() {
return context;
}

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