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

## [Unreleased]
### Added
- 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`.

## [3.9.0]
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ public final class IterableConstants {
public static final String INSTANCE_ID_CLASS = "com.google.android.gms.iid.InstanceID";
public static final String ICON_FOLDER_IDENTIFIER = "drawable";
public static final String NOTIFICATION_ICON_NAME = "iterable_notification_icon";
public static final String FIREBASE_NOTIFICATION_ICON_KEY = "com.google.firebase.messaging.default_notification_icon";
public static final String NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON = "notification_icon";
public static final String NOTIFICATION_ICON_DRAWABLE_IC_NOTIFICATION = "ic_notification";
public static final String NOTIFICAION_BADGING = "iterable_notification_badging";
public static final String NOTIFICATION_COLOR = "iterable_notification_color";
public static final String NOTIFICATION_CHANNEL_NAME = "iterable_notification_channel_name";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ static Bundle mapToBundle(Map<String, String> map) {
return bundle;
}

static int getIconId(Context context) {
return instance.getIconId(context);
}

static class IterableNotificationHelperImpl {

public IterableNotificationBuilder createNotification(Context context, Bundle extras) {
Expand Down Expand Up @@ -407,48 +411,67 @@ private String getChannelName(Context context) {
}

/**
* Returns the iconId from potential resource locations
*
* @param context
* @return
* Resolves the notification small-icon by checking each supported source in priority order,
* falling back to the app launcher icon. Returns 0 if no icon is available.
*/
private int getIconId(Context context) {
int iconId = 0;
Bundle metaData = getApplicationMetaData(context);

// iterable_notification_icon meta-data in AndroidManifest.xml
int iconId = metaData.getInt(IterableConstants.NOTIFICATION_ICON_NAME, 0);

//Get the iconId set in the AndroidManifest.xml
// Icon name set in code via IterableApi.setNotificationIcon()
if (iconId == 0) {
try {
ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
if (info.metaData != null) {
iconId = info.metaData.getInt(IterableConstants.NOTIFICATION_ICON_NAME, 0);
IterableLogger.d(IterableNotificationBuilder.TAG, "iconID: " + info.metaData.get(IterableConstants.NOTIFICATION_ICON_NAME));
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
iconId = resolveDrawable(context, IterableApi.getNotificationIcon(context));
}

// Firebase default_notification_icon meta-data in AndroidManifest.xml
if (iconId == 0) {
iconId = metaData.getInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, 0);
}

//Get the iconId set in code
// @drawable/notification_icon (Expo / React Native convention)
if (iconId == 0) {
iconId = context.getResources().getIdentifier(
IterableApi.getNotificationIcon(context),
IterableConstants.ICON_FOLDER_IDENTIFIER,
context.getPackageName());
iconId = resolveDrawable(context, IterableConstants.NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON);
}

//Get id from the default app settings
// @drawable/ic_notification (common Android convention)
if (iconId == 0) {
if (context.getApplicationInfo().icon != 0) {
IterableLogger.d(IterableNotificationBuilder.TAG, "No Notification Icon defined - defaulting to app icon");
iconId = context.getApplicationInfo().icon;
} else {
IterableLogger.w(IterableNotificationBuilder.TAG, "No Notification Icon defined - push notifications will not be displayed");
iconId = resolveDrawable(context, IterableConstants.NOTIFICATION_ICON_DRAWABLE_IC_NOTIFICATION);
}

// App launcher icon
if (iconId == 0) {
iconId = context.getApplicationInfo().icon;
if (iconId == 0) {
IterableLogger.w(IterableNotificationBuilder.TAG, "No notification icon defined - push notifications will not be displayed");
}
}

return iconId;
}

private Bundle getApplicationMetaData(Context context) {
try {
ApplicationInfo info = context.getPackageManager()
.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
if (info.metaData != null) {
return info.metaData;
}
} catch (PackageManager.NameNotFoundException e) {
IterableLogger.w(IterableNotificationBuilder.TAG, "Could not read application metadata for notification icon");
}
return new Bundle();
}

private int resolveDrawable(Context context, String name) {
if (name == null || name.isEmpty()) {
return 0;
}
return context.getResources().getIdentifier(
name, IterableConstants.ICON_FOLDER_IDENTIFIER, context.getPackageName());
}

boolean isIterablePush(Bundle extras) {
return extras != null && extras.containsKey(IterableConstants.ITERABLE_DATA_KEY);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,41 +58,14 @@ internal class IterableNotificationWorker(
}

val notification = NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(getSmallIconId())
.setSmallIcon(IterableNotificationHelper.getIconId(applicationContext))
.setContentTitle(getAppName())
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()

return ForegroundInfo(FOREGROUND_NOTIFICATION_ID, notification)
}

private fun getSmallIconId(): Int {
var iconId = 0

try {
val info = applicationContext.packageManager.getApplicationInfo(
applicationContext.packageName, PackageManager.GET_META_DATA
)
iconId = info.metaData?.getInt(IterableConstants.NOTIFICATION_ICON_NAME, 0) ?: 0
} catch (e: PackageManager.NameNotFoundException) {
IterableLogger.w(TAG, "Could not read application metadata for icon")
}

if (iconId == 0) {
iconId = applicationContext.resources.getIdentifier(
IterableApi.getNotificationIcon(applicationContext),
IterableConstants.ICON_FOLDER_IDENTIFIER,
applicationContext.packageName
)
}

if (iconId == 0) {
iconId = applicationContext.applicationInfo.icon
}

return iconId
}

private fun getAppName(): String {
return applicationContext.applicationInfo
.loadLabel(applicationContext.packageManager).toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
Expand All @@ -29,6 +32,9 @@

import static org.robolectric.Shadows.shadowOf;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import org.json.JSONArray;
import org.json.JSONObject;

Expand Down Expand Up @@ -241,4 +247,98 @@ public void testPendingIntentFlags() throws Exception {
assertTrue((textInputFlags & PendingIntent.FLAG_UPDATE_CURRENT) != 0);
assertTrue((textInputFlags & PendingIntent.FLAG_MUTABLE) != 0); // Should be mutable for text input
}

@Test
public void testIconFallbackToAppIcon() throws Exception {
Context context = iconTestContext(new Bundle(), null, 0, android.R.drawable.sym_def_app_icon);

assertEquals(android.R.drawable.sym_def_app_icon, IterableNotificationHelper.getIconId(context));
}

@Test
public void testIconUsesIterableMetadata() throws Exception {
Bundle metaData = new Bundle();
metaData.putInt(IterableConstants.NOTIFICATION_ICON_NAME, android.R.drawable.ic_dialog_email);
Context context = iconTestContext(metaData, null, 0, android.R.drawable.sym_def_app_icon);

assertEquals(android.R.drawable.ic_dialog_email, IterableNotificationHelper.getIconId(context));
}

@Test
public void testIconFallbackToFirebaseMetadata() throws Exception {
Bundle metaData = new Bundle();
metaData.putInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, android.R.drawable.ic_dialog_info);
Context context = iconTestContext(metaData, null, 0, android.R.drawable.sym_def_app_icon);

assertEquals(android.R.drawable.ic_dialog_info, IterableNotificationHelper.getIconId(context));
}

@Test
public void testIconIterableMetadataTakesPriorityOverFirebase() throws Exception {
Bundle metaData = new Bundle();
metaData.putInt(IterableConstants.NOTIFICATION_ICON_NAME, android.R.drawable.ic_dialog_alert);
metaData.putInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, android.R.drawable.ic_dialog_info);
Context context = iconTestContext(metaData, null, 0, android.R.drawable.sym_def_app_icon);

assertEquals(android.R.drawable.ic_dialog_alert, IterableNotificationHelper.getIconId(context));
}

@Test
public void testIconFallbackToNotificationIconDrawable() throws Exception {
Context context = iconTestContext(new Bundle(),
IterableConstants.NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON, android.R.drawable.ic_menu_info_details,
android.R.drawable.sym_def_app_icon);

assertEquals(android.R.drawable.ic_menu_info_details, IterableNotificationHelper.getIconId(context));
}

@Test
public void testIconFallbackToIcNotificationDrawable() throws Exception {
Context context = iconTestContext(new Bundle(),
IterableConstants.NOTIFICATION_ICON_DRAWABLE_IC_NOTIFICATION, android.R.drawable.ic_menu_help,
android.R.drawable.sym_def_app_icon);

assertEquals(android.R.drawable.ic_menu_help, IterableNotificationHelper.getIconId(context));
}

@Test
public void testIconFirebaseMetadataTakesPriorityOverDrawable() throws Exception {
Bundle metaData = new Bundle();
metaData.putInt(IterableConstants.FIREBASE_NOTIFICATION_ICON_KEY, android.R.drawable.ic_dialog_info);
Context context = iconTestContext(metaData,
IterableConstants.NOTIFICATION_ICON_DRAWABLE_NOTIFICATION_ICON, android.R.drawable.ic_menu_info_details,
android.R.drawable.sym_def_app_icon);

assertEquals(android.R.drawable.ic_dialog_info, IterableNotificationHelper.getIconId(context));
}

/**
* Builds a context spy with fully controlled icon inputs: the application meta-data bundle, an
* optional drawable name resolvable via {@link Resources#getIdentifier}, and the launcher icon.
* Lets each fallback tier of {@link IterableNotificationHelper#getIconId} be exercised in
* isolation without mutating shared Robolectric state.
*/
private Context iconTestContext(Bundle metaData, String drawableName, int drawableResId, int appIcon) throws Exception {
Context context = spy(getContext());

ApplicationInfo appInfo = new ApplicationInfo();
appInfo.packageName = getContext().getPackageName();
appInfo.metaData = metaData;
appInfo.icon = appIcon;

PackageManager packageManager = spy(getContext().getPackageManager());
when(packageManager.getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA))
.thenReturn(appInfo);
when(context.getPackageManager()).thenReturn(packageManager);
when(context.getApplicationInfo()).thenReturn(appInfo);

if (drawableName != null) {
Resources resources = spy(getContext().getResources());
when(resources.getIdentifier(drawableName, IterableConstants.ICON_FOLDER_IDENTIFIER, getContext().getPackageName()))
.thenReturn(drawableResId);
when(context.getResources()).thenReturn(resources);
}

return context;
}
}
Loading