diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f52b5b5..06c62a943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index c3ac914a4..6c896f6d7 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -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"; diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationHelper.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationHelper.java index fdcae7024..865125eb3 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationHelper.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationHelper.java @@ -109,6 +109,10 @@ static Bundle mapToBundle(Map map) { return bundle; } + static int getIconId(Context context) { + return instance.getIconId(context); + } + static class IterableNotificationHelperImpl { public IterableNotificationBuilder createNotification(Context context, Bundle extras) { @@ -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); } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationWorker.kt b/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationWorker.kt index 66c8771e2..b80dcd4ce 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationWorker.kt +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableNotificationWorker.kt @@ -58,7 +58,7 @@ internal class IterableNotificationWorker( } val notification = NotificationCompat.Builder(applicationContext, channelId) - .setSmallIcon(getSmallIconId()) + .setSmallIcon(IterableNotificationHelper.getIconId(applicationContext)) .setContentTitle(getAppName()) .setPriority(NotificationCompat.PRIORITY_LOW) .build() @@ -66,33 +66,6 @@ internal class IterableNotificationWorker( 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() diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java index 817547693..a8a65c9f5 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableNotificationTest.java @@ -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; @@ -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; @@ -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; + } } \ No newline at end of file