Skip to content

Commit ac0917c

Browse files
[SDK-415] Fix for notification icon (#1068)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1cb555b commit ac0917c

5 files changed

Lines changed: 155 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased]
6+
### Added
7+
- Notification small-icon resolution now falls back through standard conventions — the Firebase `com.google.firebase.messaging.default_notification_icon` meta-data, `@drawable/notification_icon` (Expo / React Native), and `@drawable/ic_notification` — before defaulting to the app launcher icon. This fixes white-square notification icons on Android 5.0+ for apps that configure their icon through these conventions but don't set `iterable_notification_icon`.
68

79
## [3.9.0]
810
### Added

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ public final class IterableConstants {
178178
public static final String INSTANCE_ID_CLASS = "com.google.android.gms.iid.InstanceID";
179179
public static final String ICON_FOLDER_IDENTIFIER = "drawable";
180180
public static final String NOTIFICATION_ICON_NAME = "iterable_notification_icon";
181+
public static final String FIREBASE_NOTIFICATION_ICON_KEY = "com.google.firebase.messaging.default_notification_icon";
182+
public static final String NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON = "notification_icon";
183+
public static final String NOTIFICATION_ICON_DRAWABLE_IC_NOTIFICATION = "ic_notification";
181184
public static final String NOTIFICAION_BADGING = "iterable_notification_badging";
182185
public static final String NOTIFICATION_COLOR = "iterable_notification_color";
183186
public static final String NOTIFICATION_CHANNEL_NAME = "iterable_notification_channel_name";

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

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ static Bundle mapToBundle(Map<String, String> map) {
109109
return bundle;
110110
}
111111

112+
static int getIconId(Context context) {
113+
return instance.getIconId(context);
114+
}
115+
112116
static class IterableNotificationHelperImpl {
113117

114118
public IterableNotificationBuilder createNotification(Context context, Bundle extras) {
@@ -407,48 +411,67 @@ private String getChannelName(Context context) {
407411
}
408412

409413
/**
410-
* Returns the iconId from potential resource locations
411-
*
412-
* @param context
413-
* @return
414+
* Resolves the notification small-icon by checking each supported source in priority order,
415+
* falling back to the app launcher icon. Returns 0 if no icon is available.
414416
*/
415417
private int getIconId(Context context) {
416-
int iconId = 0;
418+
Bundle metaData = getApplicationMetaData(context);
419+
420+
// iterable_notification_icon meta-data in AndroidManifest.xml
421+
int iconId = metaData.getInt(IterableConstants.NOTIFICATION_ICON_NAME, 0);
417422

418-
//Get the iconId set in the AndroidManifest.xml
423+
// Icon name set in code via IterableApi.setNotificationIcon()
419424
if (iconId == 0) {
420-
try {
421-
ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
422-
if (info.metaData != null) {
423-
iconId = info.metaData.getInt(IterableConstants.NOTIFICATION_ICON_NAME, 0);
424-
IterableLogger.d(IterableNotificationBuilder.TAG, "iconID: " + info.metaData.get(IterableConstants.NOTIFICATION_ICON_NAME));
425-
}
426-
} catch (PackageManager.NameNotFoundException e) {
427-
e.printStackTrace();
428-
}
425+
iconId = resolveDrawable(context, IterableApi.getNotificationIcon(context));
426+
}
427+
428+
// Firebase default_notification_icon meta-data in AndroidManifest.xml
429+
if (iconId == 0) {
430+
iconId = metaData.getInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, 0);
429431
}
430432

431-
//Get the iconId set in code
433+
// @drawable/notification_icon (Expo / React Native convention)
432434
if (iconId == 0) {
433-
iconId = context.getResources().getIdentifier(
434-
IterableApi.getNotificationIcon(context),
435-
IterableConstants.ICON_FOLDER_IDENTIFIER,
436-
context.getPackageName());
435+
iconId = resolveDrawable(context, IterableConstants.NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON);
437436
}
438437

439-
//Get id from the default app settings
438+
// @drawable/ic_notification (common Android convention)
440439
if (iconId == 0) {
441-
if (context.getApplicationInfo().icon != 0) {
442-
IterableLogger.d(IterableNotificationBuilder.TAG, "No Notification Icon defined - defaulting to app icon");
443-
iconId = context.getApplicationInfo().icon;
444-
} else {
445-
IterableLogger.w(IterableNotificationBuilder.TAG, "No Notification Icon defined - push notifications will not be displayed");
440+
iconId = resolveDrawable(context, IterableConstants.NOTIFICATION_ICON_DRAWABLE_IC_NOTIFICATION);
441+
}
442+
443+
// App launcher icon
444+
if (iconId == 0) {
445+
iconId = context.getApplicationInfo().icon;
446+
if (iconId == 0) {
447+
IterableLogger.w(IterableNotificationBuilder.TAG, "No notification icon defined - push notifications will not be displayed");
446448
}
447449
}
448450

449451
return iconId;
450452
}
451453

454+
private Bundle getApplicationMetaData(Context context) {
455+
try {
456+
ApplicationInfo info = context.getPackageManager()
457+
.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
458+
if (info.metaData != null) {
459+
return info.metaData;
460+
}
461+
} catch (PackageManager.NameNotFoundException e) {
462+
IterableLogger.w(IterableNotificationBuilder.TAG, "Could not read application metadata for notification icon");
463+
}
464+
return new Bundle();
465+
}
466+
467+
private int resolveDrawable(Context context, String name) {
468+
if (name == null || name.isEmpty()) {
469+
return 0;
470+
}
471+
return context.getResources().getIdentifier(
472+
name, IterableConstants.ICON_FOLDER_IDENTIFIER, context.getPackageName());
473+
}
474+
452475
boolean isIterablePush(Bundle extras) {
453476
return extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY);
454477
}

iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationWorker.kt

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,41 +58,14 @@ internal class IterableNotificationWorker(
5858
}
5959

6060
val notification = NotificationCompat.Builder(applicationContext, channelId)
61-
.setSmallIcon(getSmallIconId())
61+
.setSmallIcon(IterableNotificationHelper.getIconId(applicationContext))
6262
.setContentTitle(getAppName())
6363
.setPriority(NotificationCompat.PRIORITY_LOW)
6464
.build()
6565

6666
return ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification)
6767
}
6868

69-
private fun getSmallIconId(): Int {
70-
var iconId = 0
71-
72-
try {
73-
val info = applicationContext.packageManager.getApplicationInfo(
74-
applicationContext.packageName, PackageManager.GET_META_DATA
75-
)
76-
iconId = info.metaData?.getInt(IterableConstants.NOTIFICATION_ICON_NAME, 0) ?: 0
77-
} catch (e: PackageManager.NameNotFoundException) {
78-
IterableLogger.w(TAG, "Could not read application metadata for icon")
79-
}
80-
81-
if (iconId == 0) {
82-
iconId = applicationContext.resources.getIdentifier(
83-
IterableApi.getNotificationIcon(applicationContext),
84-
IterableConstants.ICON_FOLDER_IDENTIFIER,
85-
applicationContext.packageName
86-
)
87-
}
88-
89-
if (iconId == 0) {
90-
iconId = applicationContext.applicationInfo.icon
91-
}
92-
93-
return iconId
94-
}
95-
9669
private fun getAppName(): String {
9770
return applicationContext.applicationInfo
9871
.loadLabel(applicationContext.packageManager).toString()

iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import android.app.NotificationManager;
55
import android.app.PendingIntent;
66
import android.content.Context;
7+
import android.content.pm.ApplicationInfo;
8+
import android.content.pm.PackageManager;
9+
import android.content.res.Resources;
710
import android.os.Build;
811
import android.os.Bundle;
912
import android.service.notification.StatusBarNotification;
@@ -29,6 +32,9 @@
2932

3033
import static org.robolectric.Shadows.shadowOf;
3134

35+
import static org.mockito.Mockito.spy;
36+
import static org.mockito.Mockito.when;
37+
3238
import org.json.JSONArray;
3339
import org.json.JSONObject;
3440

@@ -241,4 +247,98 @@ public void testPendingIntentFlags() throws Exception {
241247
assertTrue((textInputFlags & PendingIntent.FLAG_UPDATE_CURRENT) != 0);
242248
assertTrue((textInputFlags & PendingIntent.FLAG_MUTABLE) != 0); // Should be mutable for text input
243249
}
250+
251+
@Test
252+
public void testIconFallbackToAppIcon() throws Exception {
253+
Context context = iconTestContext(new Bundle(), null, 0, android.R.drawable.sym_def_app_icon);
254+
255+
assertEquals(android.R.drawable.sym_def_app_icon, IterableNotificationHelper.getIconId(context));
256+
}
257+
258+
@Test
259+
public void testIconUsesIterableMetadata() throws Exception {
260+
Bundle metaData = new Bundle();
261+
metaData.putInt(IterableConstants.NOTIFICATION_ICON_NAME, android.R.drawable.ic_dialog_email);
262+
Context context = iconTestContext(metaData, null, 0, android.R.drawable.sym_def_app_icon);
263+
264+
assertEquals(android.R.drawable.ic_dialog_email, IterableNotificationHelper.getIconId(context));
265+
}
266+
267+
@Test
268+
public void testIconFallbackToFirebaseMetadata() throws Exception {
269+
Bundle metaData = new Bundle();
270+
metaData.putInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, android.R.drawable.ic_dialog_info);
271+
Context context = iconTestContext(metaData, null, 0, android.R.drawable.sym_def_app_icon);
272+
273+
assertEquals(android.R.drawable.ic_dialog_info, IterableNotificationHelper.getIconId(context));
274+
}
275+
276+
@Test
277+
public void testIconIterableMetadataTakesPriorityOverFirebase() throws Exception {
278+
Bundle metaData = new Bundle();
279+
metaData.putInt(IterableConstants.NOTIFICATION_ICON_NAME, android.R.drawable.ic_dialog_alert);
280+
metaData.putInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, android.R.drawable.ic_dialog_info);
281+
Context context = iconTestContext(metaData, null, 0, android.R.drawable.sym_def_app_icon);
282+
283+
assertEquals(android.R.drawable.ic_dialog_alert, IterableNotificationHelper.getIconId(context));
284+
}
285+
286+
@Test
287+
public void testIconFallbackToNotificationIconDrawable() throws Exception {
288+
Context context = iconTestContext(new Bundle(),
289+
IterableConstants.NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON, android.R.drawable.ic_menu_info_details,
290+
android.R.drawable.sym_def_app_icon);
291+
292+
assertEquals(android.R.drawable.ic_menu_info_details, IterableNotificationHelper.getIconId(context));
293+
}
294+
295+
@Test
296+
public void testIconFallbackToIcNotificationDrawable() throws Exception {
297+
Context context = iconTestContext(new Bundle(),
298+
IterableConstants.NOTIFICATION_ICON_DRAWABLE_IC_NOTIFICATION, android.R.drawable.ic_menu_help,
299+
android.R.drawable.sym_def_app_icon);
300+
301+
assertEquals(android.R.drawable.ic_menu_help, IterableNotificationHelper.getIconId(context));
302+
}
303+
304+
@Test
305+
public void testIconFirebaseMetadataTakesPriorityOverDrawable() throws Exception {
306+
Bundle metaData = new Bundle();
307+
metaData.putInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, android.R.drawable.ic_dialog_info);
308+
Context context = iconTestContext(metaData,
309+
IterableConstants.NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON, android.R.drawable.ic_menu_info_details,
310+
android.R.drawable.sym_def_app_icon);
311+
312+
assertEquals(android.R.drawable.ic_dialog_info, IterableNotificationHelper.getIconId(context));
313+
}
314+
315+
/**
316+
* Builds a context spy with fully controlled icon inputs: the application meta-data bundle, an
317+
* optional drawable name resolvable via {@link Resources#getIdentifier}, and the launcher icon.
318+
* Lets each fallback tier of {@link IterableNotificationHelper#getIconId} be exercised in
319+
* isolation without mutating shared Robolectric state.
320+
*/
321+
private Context iconTestContext(Bundle metaData, String drawableName, int drawableResId, int appIcon) throws Exception {
322+
Context context = spy(getContext());
323+
324+
ApplicationInfo appInfo = new ApplicationInfo();
325+
appInfo.packageName = getContext().getPackageName();
326+
appInfo.metaData = metaData;
327+
appInfo.icon = appIcon;
328+
329+
PackageManager packageManager = spy(getContext().getPackageManager());
330+
when(packageManager.getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA))
331+
.thenReturn(appInfo);
332+
when(context.getPackageManager()).thenReturn(packageManager);
333+
when(context.getApplicationInfo()).thenReturn(appInfo);
334+
335+
if (drawableName != null) {
336+
Resources resources = spy(getContext().getResources());
337+
when(resources.getIdentifier(drawableName, IterableConstants.ICON_FOLDER_IDENTIFIER, getContext().getPackageName()))
338+
.thenReturn(drawableResId);
339+
when(context.getResources()).thenReturn(resources);
340+
}
341+
342+
return context;
343+
}
244344
}

0 commit comments

Comments
 (0)