Skip to content

Commit de2b8af

Browse files
Merge origin/master into SDK-337-in-app-close-button-position
Resolved CHANGELOG.md conflict by keeping both the unreleased in-app display mode entries and the 3.6.6 release entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 874e3e1 + 97cedbb commit de2b8af

14 files changed

Lines changed: 1915 additions & 29 deletions

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1111
- `FORCE_RESPECT_BOUNDS` — ensures in-app content never overlaps system bars, keeping UI elements like the close button always accessible.
1212

1313
### Changed
14+
- Replaced the deprecated `AsyncTask`-based push notification handling with `WorkManager` for improved reliability and compatibility with modern Android versions. No action is required.
1415
- In-app messages now match the host app's system bar behavior by default. Previously, fullscreen in-apps would always draw content behind the status bar, which could cause UI elements like the close button to be obscured. The new default (`FOLLOW_APP_LAYOUT`) detects whether your app uses edge-to-edge and matches that configuration.
1516

16-
### Migration guide
17+
#### Migration guide
1718
**No action required for most apps.** The new default `FOLLOW_APP_LAYOUT` automatically adapts to your app's layout.
1819

1920
If you relied on the previous behavior where fullscreen in-apps drew content behind the status bar, you can restore it explicitly:
@@ -34,6 +35,12 @@ IterableConfig config = new IterableConfig.Builder()
3435
.build();
3536
```
3637

38+
## [3.6.6]
39+
### Fixed
40+
- Fixed push notifications killing the existing activity when opened
41+
- Fixed in-app message crash caused by WebView creation issues
42+
- Fixed `BROADCAST_CLOSE_SYSTEM_DIALOGS` permission error on Android 12+ by restricting usage to Android SDK 30 and below
43+
3744
## [3.6.5]
3845
### Fixed
3946
- Fixed IterableEmbeddedView not having an empty constructor and causing crashes

iterableapi-ui/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ dependencies {
6161

6262
ext {
6363
libraryName = 'iterableapi-ui'
64-
libraryVersion = '3.6.5'
64+
libraryVersion = '3.6.6'
6565
}
6666

6767
if (hasProperty("mavenPublishEnabled")) {

iterableapi/build.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ android {
2121
minSdkVersion 21
2222
targetSdkVersion 34
2323

24-
buildConfigField "String", "ITERABLE_SDK_VERSION", "\"3.6.5\""
24+
buildConfigField "String", "ITERABLE_SDK_VERSION", "\"3.6.6\""
2525

2626
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2727
}
@@ -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'
@@ -89,7 +91,7 @@ dependencies {
8991

9092
ext {
9193
libraryName = 'iterableapi'
92-
libraryVersion = '3.6.5'
94+
libraryVersion = '3.6.6'
9395
}
9496

9597
if (hasProperty("mavenPublishEnabled")) {

iterableapi/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
android:name=".IterableTrampolineActivity"
3030
android:exported="false"
3131
android:launchMode="singleTask"
32+
android:taskAffinity=""
3233
android:excludeFromRecents="true"
3334
android:theme="@style/TrampolineActivity.Transparent"/>
3435

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

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.iterable.iterableapi;
22

33
import android.content.Context;
4-
import android.os.AsyncTask;
54
import android.os.Bundle;
5+
66
import androidx.annotation.NonNull;
77

88
import com.google.android.gms.tasks.Tasks;
@@ -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,17 @@ 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+
if (IterableNotificationHelper.hasAttachmentUrl(extras)) {
67+
enqueueNotificationWork(context, extras);
68+
} else {
69+
handleNow(context, extras);
70+
}
6571
} else {
6672
IterableLogger.d(TAG, "Iterable OS notification push received");
6773
}
@@ -105,9 +111,7 @@ public static String getFirebaseToken() {
105111
String registrationToken = null;
106112
try {
107113
registrationToken = Tasks.await(FirebaseMessaging.getInstance().getToken());
108-
} catch (ExecutionException e) {
109-
IterableLogger.e(TAG, e.getLocalizedMessage());
110-
} catch (InterruptedException e) {
114+
} catch (ExecutionException | InterruptedException e) {
111115
IterableLogger.e(TAG, e.getLocalizedMessage());
112116
} catch (Exception e) {
113117
IterableLogger.e(TAG, "Failed to fetch firebase token");
@@ -122,25 +126,60 @@ public static String getFirebaseToken() {
122126
* @return Boolean indicating whether the message is an Iterable ghost push or silent push
123127
*/
124128
public static boolean isGhostPush(RemoteMessage remoteMessage) {
125-
Map<String, String> messageData = remoteMessage.getData();
129+
try {
130+
Map<String, String> messageData = remoteMessage.getData();
131+
132+
if (messageData.isEmpty()) {
133+
return false;
134+
}
126135

127-
if (messageData == null || messageData.isEmpty()) {
136+
Bundle extras = IterableNotificationHelper.mapToBundle(messageData);
137+
return IterableNotificationHelper.isGhostPush(extras);
138+
} catch (Exception e) {
139+
IterableLogger.e(TAG, e.getMessage());
128140
return false;
129141
}
142+
}
130143

131-
Bundle extras = IterableNotificationHelper.mapToBundle(messageData);
132-
return IterableNotificationHelper.isGhostPush(extras);
144+
private static void enqueueNotificationWork(@NonNull final Context context, @NonNull final Bundle extras) {
145+
IterableNotificationWorkScheduler scheduler = new IterableNotificationWorkScheduler(context);
146+
147+
scheduler.scheduleNotificationWork(
148+
extras,
149+
new IterableNotificationWorkScheduler.SchedulerCallback() {
150+
@Override
151+
public void onScheduleSuccess(UUID workId) {
152+
IterableLogger.d(TAG, "Notification work scheduled: " + workId);
153+
}
154+
155+
@Override
156+
public void onScheduleFailure(Exception exception, Bundle notificationData) {
157+
IterableLogger.e(TAG, "Failed to schedule notification work, falling back to immediate posting", exception);
158+
handleNow(context, notificationData);
159+
}
160+
}
161+
);
133162
}
134-
}
135163

136-
class IterableNotificationManager extends AsyncTask<IterableNotificationBuilder, Void, Void> {
164+
private static void handleNow(@NonNull Context context, @NonNull Bundle extras) {
165+
Bundle safeExtras = extras;
137166

138-
@Override
139-
protected Void doInBackground(IterableNotificationBuilder... params) {
140-
if (params != null && params[0] != null) {
141-
IterableNotificationBuilder notificationBuilder = params[0];
142-
IterableNotificationHelper.postNotificationOnDevice(notificationBuilder.context, notificationBuilder);
167+
if (IterableNotificationHelper.hasAttachmentUrl(extras)) {
168+
IterableLogger.w(TAG, "image found when handling on main thread, removing it for safe handling");
169+
safeExtras = IterableNotificationHelper.removePushImageFromBundle(extras);
170+
}
171+
172+
try {
173+
IterableNotificationBuilder notificationBuilder = IterableNotificationHelper
174+
.createNotification(
175+
context.getApplicationContext(),
176+
safeExtras
177+
);
178+
if (notificationBuilder != null) {
179+
IterableNotificationHelper.postNotificationOnDevice(context, notificationBuilder);
180+
}
181+
} catch (Exception e) {
182+
IterableLogger.e(TAG, "Failed to post notification directly", e);
143183
}
144-
return null;
145184
}
146-
}
185+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private PendingIntent getPendingIntent(Context context, IterableNotificationData
135135
if (button.openApp) {
136136
IterableLogger.d(TAG, "Go through TrampolineActivity");
137137
buttonIntent.setClass(context, IterableTrampolineActivity.class);
138-
buttonIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
138+
buttonIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
139139
pendingButtonIntent = PendingIntent.getActivity(context, buttonIntent.hashCode(),
140140
buttonIntent, pendingIntentFlag);
141141
} else {

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

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ static boolean isEmptyBody(Bundle extras) {
8787
return instance.isEmptyBody(extras);
8888
}
8989

90+
/**
91+
* Returns whether the notification payload includes an image attachment URL,
92+
* meaning display requires a network image download (long-running work).
93+
* @param extras what is inside the bundle
94+
* @return if it has an attachment url
95+
*/
96+
static boolean hasAttachmentUrl(Bundle extras) {
97+
return instance.hasAttachmentUrl(extras);
98+
}
99+
100+
static Bundle removePushImageFromBundle(Bundle extras) {
101+
return instance.removePushImageFromBundle(extras);
102+
}
103+
90104
static Bundle mapToBundle(Map<String, String> map) {
91105
Bundle bundle = new Bundle();
92106
for (Map.Entry<String, String> entry : map.entrySet()) {
@@ -98,6 +112,11 @@ static Bundle mapToBundle(Map<String, String> map) {
98112
static class IterableNotificationHelperImpl {
99113

100114
public IterableNotificationBuilder createNotification(Context context, Bundle extras) {
115+
if (extras == null) {
116+
IterableLogger.w(IterableNotificationBuilder.TAG, "Notification extras is null. Skipping.");
117+
return null;
118+
}
119+
101120
String applicationName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
102121
String title = null;
103122
String notificationBody = null;
@@ -194,7 +213,7 @@ public IterableNotificationBuilder createNotification(Context context, Bundle ex
194213
trampolineActivityIntent.setClass(context, IterableTrampolineActivity.class);
195214
trampolineActivityIntent.putExtras(extras);
196215
trampolineActivityIntent.putExtra(IterableConstants.ITERABLE_DATA_ACTION_IDENTIFIER, IterableConstants.ITERABLE_ACTION_DEFAULT);
197-
trampolineActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
216+
trampolineActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
198217

199218
// Action buttons
200219
if (notificationData.getActionButtons() != null) {
@@ -436,7 +455,7 @@ boolean isIterablePush(Bundle extras) {
436455

437456
boolean isGhostPush(Bundle extras) {
438457
boolean isGhostPush = false;
439-
if (extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
458+
if (extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
440459
String iterableData = extras.getString(IterableConstants.ITERABLE_DATA_KEY);
441460
IterableNotificationData data = new IterableNotificationData(iterableData);
442461
isGhostPush = data.getIsGhostPush();
@@ -447,12 +466,50 @@ boolean isGhostPush(Bundle extras) {
447466

448467
boolean isEmptyBody(Bundle extras) {
449468
String notificationBody = "";
450-
if (extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
469+
if (extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
451470
notificationBody = extras.getString(IterableConstants.ITERABLE_DATA_BODY, "");
452471
}
453472

454473
return notificationBody.isEmpty();
455474
}
475+
476+
@Nullable
477+
private JSONObject getIterableJsonFromBundle(Bundle extras) {
478+
if (extras == null || !extras.containsKey(IterableConstants.ITERABLE_DATA_KEY)) {
479+
return null;
480+
}
481+
try {
482+
String iterableData = extras.getString(IterableConstants.ITERABLE_DATA_KEY);
483+
return new JSONObject(iterableData);
484+
} catch (Exception e) {
485+
return null;
486+
}
487+
}
488+
489+
boolean hasAttachmentUrl(Bundle extras) {
490+
JSONObject iterableJson = getIterableJsonFromBundle(extras);
491+
if (iterableJson == null) {
492+
return false;
493+
}
494+
String attachmentUrl = iterableJson.optString(IterableConstants.ITERABLE_DATA_PUSH_IMAGE, "");
495+
return !attachmentUrl.isEmpty();
496+
}
497+
498+
Bundle removePushImageFromBundle(Bundle extras) {
499+
JSONObject iterableJson = getIterableJsonFromBundle(extras);
500+
if (iterableJson == null) {
501+
return extras;
502+
}
503+
try {
504+
Bundle newExtras = new Bundle(extras);
505+
iterableJson.remove(IterableConstants.ITERABLE_DATA_PUSH_IMAGE);
506+
newExtras.putString(IterableConstants.ITERABLE_DATA_KEY, iterableJson.toString());
507+
return newExtras;
508+
} catch (Exception e) {
509+
IterableLogger.e("IterableNotificationHelper", "Failed to remove push image from bundle", e);
510+
return extras;
511+
}
512+
}
456513
}
457514

458515
@Nullable
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.Data;
10+
import androidx.work.OneTimeWorkRequest;
11+
import androidx.work.OutOfQuotaPolicy;
12+
import androidx.work.WorkManager;
13+
14+
import java.util.UUID;
15+
16+
class IterableNotificationWorkScheduler {
17+
18+
private static final String TAG = "IterableNotificationWorkScheduler";
19+
20+
private final Context context;
21+
private final WorkManager workManager;
22+
23+
interface SchedulerCallback {
24+
void onScheduleSuccess(UUID workId);
25+
void onScheduleFailure(Exception exception, Bundle notificationData);
26+
}
27+
28+
IterableNotificationWorkScheduler(@NonNull Context context) {
29+
this(context, WorkManager.getInstance(context));
30+
}
31+
32+
@VisibleForTesting
33+
IterableNotificationWorkScheduler(@NonNull Context context, @NonNull WorkManager workManager) {
34+
this.context = context.getApplicationContext();
35+
this.workManager = workManager;
36+
}
37+
38+
void scheduleNotificationWork(
39+
@NonNull Bundle notificationData,
40+
@Nullable SchedulerCallback callback
41+
) {
42+
43+
try {
44+
Data inputData = IterableNotificationWorker.createInputData(
45+
notificationData
46+
);
47+
48+
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(IterableNotificationWorker.class)
49+
.setInputData(inputData)
50+
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
51+
.build();
52+
53+
workManager.enqueue(workRequest);
54+
55+
UUID workId = workRequest.getId();
56+
IterableLogger.d(TAG, "Notification work scheduled: " + workId);
57+
58+
if (callback != null) {
59+
callback.onScheduleSuccess(workId);
60+
}
61+
62+
} catch (Exception e) {
63+
IterableLogger.e(TAG, "Failed to schedule notification work", e);
64+
65+
if (callback != null) {
66+
callback.onScheduleFailure(e, notificationData);
67+
}
68+
}
69+
}
70+
71+
@VisibleForTesting
72+
Context getContext() {
73+
return context;
74+
}
75+
76+
@VisibleForTesting
77+
WorkManager getWorkManager() {
78+
return workManager;
79+
}
80+
}

0 commit comments

Comments
 (0)