From 150282709087862729b4c0bacc0b2e036fb418e8 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Thu, 4 Jun 2026 23:52:02 +0530 Subject: [PATCH 01/12] Add AJO Basic push notification template support with scale type --- .../NotificationBuilder.kt | 11 + .../PushTemplateConstants.kt | 23 ++ .../internal/PushTemplateType.kt | 2 + .../builders/AJOBasicNotificationBuilder.kt | 196 ++++++++++++++++++ .../ajo/templates/AJOBasicPushTemplate.kt | 104 ++++++++++ .../internal/ajo/templates/AJOPushTemplate.kt | 172 +++++++++++++++ .../internal/templates/AEPPushTemplate.kt | 2 +- .../layout/ajo_push_template_collapsed.xml | 36 ++++ ...ajo_push_template_expanded_center_crop.xml | 43 ++++ .../ajo_push_template_expanded_fit_center.xml | 48 +++++ .../ajo_basic/ajo_basic_center_crop.json | 16 ++ .../ajo_basic/ajo_basic_fit_center.json | 16 ++ code/testapp/src/main/assets/basic/basic.json | 5 +- .../testapp/notificationBuilder/Template.kt | 3 +- .../UINotificationBuilderActivity.kt | 9 +- 15 files changed, 679 insertions(+), 7 deletions(-) create mode 100644 code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt create mode 100644 code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplate.kt create mode 100644 code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt create mode 100644 code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml create mode 100644 code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml create mode 100644 code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_fit_center.xml create mode 100644 code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json create mode 100644 code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt index dd574ccd..0312c0d0 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt @@ -31,6 +31,8 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductC import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductRatingNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AEPPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AutoCarouselPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.BasicPushTemplate @@ -222,6 +224,15 @@ object NotificationBuilder { ) } + PushTemplateType.AJO_BASIC -> { + return AJOBasicNotificationBuilder.construct( + context, + AJOBasicPushTemplate(notificationData), + trackerActivityClass, + broadcastReceiverClass + ) + } + PushTemplateType.UNKNOWN -> { return LegacyNotificationBuilder.construct( context, diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt index b08f24d5..a86b6262 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt @@ -92,6 +92,7 @@ object PushTemplateConstants { object PushPayloadKeys { const val TEMPLATE_TYPE = "adb_template_type" + const val AJO_TEMPLATE_PROPERTIES = "adb_template_properties" const val TITLE = "adb_title" const val BODY = "adb_body" const val SOUND = "adb_sound" @@ -197,4 +198,26 @@ object PushTemplateConstants { const val URI = "uri" const val TYPE = "type" } + + // Keys for parsing the adb_template_properties JSON blob for AJO templates. + // These are NOT top-level FCM keys — they are keys inside the parsed JSON object. + internal object AJOTemplatePropertyKeys { + internal const val VERSION = "adb_version" + internal const val TITLE = "adb_title" + internal const val BODY = "adb_body" + internal const val IMAGE = "adb_image" + internal const val LARGE_ICON = "adb_large_icon" + + // Sub-field names shared across nested objects (e.g. adb_title.text, adb_image.url) + internal object SubKeys { + internal const val TEXT = "text" + internal const val URL = "url" + internal const val SCALE_TYPE = "scale_type" + } + + internal object ScaleType { + internal const val CENTER_CROP = "center_crop" + internal const val FIT_CENTER = "fit_center" + } + } } diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt index 659dca1c..745eb63d 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt @@ -23,6 +23,7 @@ internal enum class PushTemplateType(val value: String) { PRODUCT_CATALOG("cat"), MULTI_ICON("icon"), TIMER("timer"), + AJO_BASIC("ajo_basic"), UNKNOWN("unknown"); companion object { @@ -42,6 +43,7 @@ internal enum class PushTemplateType(val value: String) { "rate" -> PRODUCT_RATING "icon" -> MULTI_ICON "timer" -> TIMER + "ajo_basic" -> AJO_BASIC else -> UNKNOWN } } diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt new file mode 100644 index 00000000..a0553987 --- /dev/null +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt @@ -0,0 +1,196 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders + +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.media.RingtoneManager +import android.os.Build +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import com.adobe.marketing.mobile.notificationbuilder.NotificationConstructionFailedException +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG +import com.adobe.marketing.mobile.notificationbuilder.R +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.addActionButtons +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.getSoundUriForResourceName +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setNotificationClickAction +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setNotificationDeleteAction +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setRemoteViewImage +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setSmallIcon +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setSound +import com.adobe.marketing.mobile.services.Log + +/** + * Object responsible for constructing a [NotificationCompat.Builder] object containing an + * AJO basic ("ajo_basic") push template notification. + * + * This builder is segregated from the out-of-the-box (ACC) builders. It reuses the shared + * primitive RemoteViews / NotificationCompat.Builder extension functions for rendering, but + * keeps its own orchestration and channel creation so AJO concerns evolve independently and no + * existing code is modified. + */ +internal object AJOBasicNotificationBuilder { + private const val SELF_TAG = "AJOBasicNotificationBuilder" + + @Throws(NotificationConstructionFailedException::class) + fun construct( + context: Context, + pushTemplate: AJOBasicPushTemplate, + trackerActivityClass: Class?, + broadcastReceiverClass: Class? + ): NotificationCompat.Builder { + Log.trace(LOG_TAG, SELF_TAG, "Building an AJO basic template push notification.") + val packageName = context.packageName + val smallLayout = RemoteViews(packageName, R.layout.ajo_push_template_collapsed) + + // pick the expanded layout based on the requested image scale type + val expandedLayoutId = + if (pushTemplate.imgScaleType == AJOTemplatePropertyKeys.ScaleType.FIT_CENTER) { + R.layout.ajo_push_template_expanded_fit_center + } else { + R.layout.ajo_push_template_expanded_center_crop + } + val expandedLayout = RemoteViews(packageName, expandedLayoutId) + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channelIdToUse = createChannelIfRequired(context, notificationManager, pushTemplate) + + // set the title and body text + smallLayout.setTextViewText(R.id.notification_title, pushTemplate.title) + smallLayout.setTextViewText(R.id.notification_body, pushTemplate.body) + expandedLayout.setTextViewText(R.id.notification_title, pushTemplate.title) + expandedLayout.setTextViewText(R.id.notification_body_expanded, pushTemplate.body) + + // set a large icon if one is present + smallLayout.setRemoteViewImage(pushTemplate.largeIcon, R.id.large_icon) + expandedLayout.setRemoteViewImage(pushTemplate.largeIcon, R.id.large_icon) + + // set the expanded image (scaled per the layout chosen above) + expandedLayout.setRemoteViewImage(pushTemplate.imageUrl, R.id.expanded_template_image) + + val builder = NotificationCompat.Builder(context, channelIdToUse) + .setTicker(pushTemplate.ticker) + .setNumber(pushTemplate.badgeCount) + .setAutoCancel(!pushTemplate.isNotificationSticky) + .setOngoing(pushTemplate.isNotificationSticky) + .setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setCustomContentView(smallLayout) + .setCustomBigContentView(expandedLayout) + // small icon must be present, otherwise the notification will not be displayed. + .setSmallIcon(context, pushTemplate.smallIcon, null) + .setVisibility(pushTemplate.visibility.value) + .setNotificationClickAction( + context, + trackerActivityClass, + pushTemplate.actionUri, + pushTemplate.actionType, + pushTemplate.data.getBundle() + ) + .setNotificationDeleteAction(context, trackerActivityClass) + + // if not from intent, set custom sound. applies to API 25 and lower only as + // API 26 and up set the sound on the notification channel. + if (!pushTemplate.isFromIntent) { + builder.setSound(context, pushTemplate.sound) + } + + // below API 26 (no notification channels) priority is set on the builder + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + builder.setPriority(NotificationCompat.PRIORITY_HIGH) + .setVibrate(LongArray(0)) + } + + // add any action buttons defined for the notification + builder.addActionButtons( + context, + trackerActivityClass, + pushTemplate.actionButtonsList, + pushTemplate.data.getBundle() + ) + + return builder + } + + /** + * Creates a notification channel if required. Logic mirrors the shared + * `NotificationManager.createNotificationChannelIfRequired` extension but is kept local so + * the AJO builder does not depend on or modify the AEPPushTemplate-typed shared extension. + * + * @param context the application [Context] + * @param notificationManager the [NotificationManager] used to create / look up channels + * @param pushTemplate the [AJOBasicPushTemplate] providing channel id, sound and importance + * @return the channel ID to use for the notification + */ + private fun createChannelIfRequired( + context: Context, + notificationManager: NotificationManager, + pushTemplate: AJOBasicPushTemplate + ): String { + val channelIdToUse = + if (pushTemplate.isFromIntent) { + PushTemplateConstants.DefaultValues.SILENT_NOTIFICATION_CHANNEL_ID + } else { + pushTemplate.channelId ?: PushTemplateConstants.DefaultValues.DEFAULT_CHANNEL_ID + } + + // no channel creation required below API 26 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return channelIdToUse + } + + // don't create a channel if it already exists + if (notificationManager.getNotificationChannel(channelIdToUse) != null) { + Log.trace( + LOG_TAG, + SELF_TAG, + "Using previously created notification channel: $channelIdToUse." + ) + return channelIdToUse + } + + val channel = NotificationChannel( + channelIdToUse, + if (pushTemplate.isFromIntent) { + PushTemplateConstants.DefaultValues.SILENT_CHANNEL_NAME + } else { + PushTemplateConstants.DefaultValues.DEFAULT_CHANNEL_NAME + }, + pushTemplate.getNotificationImportance() + ) + + if (pushTemplate.isFromIntent) { + channel.setSound(null, null) + } else { + val soundUri = if (pushTemplate.sound.isNullOrEmpty()) { + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } else { + context.getSoundUriForResourceName(pushTemplate.sound) + } + channel.setSound(soundUri, null) + } + + Log.trace( + LOG_TAG, + SELF_TAG, + "Creating a new notification channel with ID: $channelIdToUse." + ) + notificationManager.createNotificationChannel(channel) + return channelIdToUse + } +} diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplate.kt new file mode 100644 index 00000000..77fb8938 --- /dev/null +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplate.kt @@ -0,0 +1,104 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates + +import androidx.annotation.VisibleForTesting +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.BasicPushTemplate +import com.adobe.marketing.mobile.notificationbuilder.internal.util.NotificationData +import com.adobe.marketing.mobile.services.Log +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +/** + * Represents the AJO "ajo_basic" push template. + * + * Template-specific fields (imgScaleType, largeIconScaleType) are read from the + * [adb_template_properties] JSON blob parsed by [AJOPushTemplate]. + * Action buttons are read from the flat [adb_act] key, consistent with ACC/AJO handling. + */ +internal class AJOBasicPushTemplate(data: NotificationData) : AJOPushTemplate(data) { + + private val SELF_TAG = "AJOBasicPushTemplate" + + // Scale type for the main expanded image. Defaults to CENTER_CROP. + internal val imgScaleType: String + + // Scale type for the large icon. Defaults to CENTER_CROP. + internal val largeIconScaleType: String + + // Optional, action buttons for the notification as a raw string + internal val actionButtonsString: String? + + // Optional, parsed list of action buttons + internal val actionButtonsList: List? + + init { + val props: JSONObject? = parsePropertiesBlob( + data.getString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES) + ) + + imgScaleType = props?.optJSONObject(AJOTemplatePropertyKeys.IMAGE) + ?.optString(AJOTemplatePropertyKeys.SubKeys.SCALE_TYPE) + ?.takeIf { it.isNotEmpty() } + ?: AJOTemplatePropertyKeys.ScaleType.CENTER_CROP + + largeIconScaleType = props?.optJSONObject(AJOTemplatePropertyKeys.LARGE_ICON) + ?.optString(AJOTemplatePropertyKeys.SubKeys.SCALE_TYPE) + ?.takeIf { it.isNotEmpty() } + ?: AJOTemplatePropertyKeys.ScaleType.CENTER_CROP + + // Action buttons come from the flat adb_act key, same as ACC/AJO today + actionButtonsString = data.getString(PushPayloadKeys.ACTION_BUTTONS) + actionButtonsList = getActionButtonsFromString(actionButtonsString) + } + + @VisibleForTesting + internal fun getActionButtonsFromString(actionButtons: String?): List? { + if (actionButtons == null) { + Log.debug( + LOG_TAG, SELF_TAG, + "Exception in converting actionButtons json string to json object, Error : actionButtons is null" + ) + return null + } + val actionButtonList = mutableListOf() + try { + val jsonArray = JSONArray(actionButtons) + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.getJSONObject(i) + val button = BasicPushTemplate.ActionButton.getActionButtonFromJSONObject(jsonObject) + ?: continue + actionButtonList.add(button) + } + } catch (e: JSONException) { + Log.warning( + LOG_TAG, SELF_TAG, + "Exception in converting actionButtons json string to json object, Error : ${e.localizedMessage}" + ) + return null + } + return actionButtonList + } + + private fun parsePropertiesBlob(raw: String?): JSONObject? { + if (raw.isNullOrEmpty()) return null + return try { + JSONObject(raw) + } catch (e: JSONException) { + null + } + } +} diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt new file mode 100644 index 00000000..8320f7fc --- /dev/null +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt @@ -0,0 +1,172 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates + +import android.app.NotificationManager +import android.os.Build +import androidx.annotation.RequiresApi +import com.adobe.marketing.mobile.notificationbuilder.NotificationPriority +import com.adobe.marketing.mobile.notificationbuilder.NotificationVisibility +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.ActionType +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys +import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType +import com.adobe.marketing.mobile.notificationbuilder.internal.util.IntentData +import com.adobe.marketing.mobile.notificationbuilder.internal.util.NotificationData +import com.adobe.marketing.mobile.services.Log +import org.json.JSONException +import org.json.JSONObject + +/** + * Base class for all Adobe Journey Optimizer (AJO) push templates. + * + * Parsing strategy (two zones): + * - [adb_template_properties] blob → title, body, imageUrl, largeIcon (template-specific fields) + * - Flat FCM data keys → sound, channelId, priority, visibility, actionType, actionUri, + * badgeCount, sticky (general notification config — backward-compat with old campaigns) + */ +internal sealed class AJOPushTemplate(val data: NotificationData) { + + private val SELF_TAG = "AJOPushTemplate" + + // Required — sourced from adb_template_properties blob + internal val title: String + + // Required — sourced from adb_template_properties blob + internal val body: String + + // Optional — sourced from adb_template_properties blob + internal val imageUrl: String? + + // Optional — sourced from adb_template_properties blob + internal val largeIcon: String? + + // Optional — version of the template schema, sourced from blob. Defaults to "1". + internal val payloadVersion: String + + // --- Fields below are sourced from flat FCM data keys (backward-compat zone) --- + + // Optional, small icon resource name. Reads adb_small_icon then falls back to adb_icon. + internal val smallIcon: String? + + // Optional, sound to play when the notification is shown + internal val sound: String? + + // Optional, number to show on the badge of the app + internal val badgeCount: Int + + // Optional, priority of the notification + internal val priority: NotificationPriority + + // Optional, visibility of the notification + internal val visibility: NotificationVisibility + + // Optional, notification channel (Android O+) + internal val channelId: String? + + // Optional, action type for the notification tap + internal val actionType: ActionType? + + // Optional, action uri for the notification tap + internal val actionUri: String? + + // Optional, ticker text for accessibility services + internal val ticker: String? + + // Optional, the type of push template this payload contains + internal val templateType: PushTemplateType? + + // Optional, when true the notification persists after the user taps it + internal val isNotificationSticky: Boolean + + // Flag to denote if the PushTemplate was built from an intent + internal val isFromIntent: Boolean + + init { + templateType = PushTemplateType.fromString(data.getString(PushPayloadKeys.TEMPLATE_TYPE)) + isFromIntent = data is IntentData + + // Parse the adb_template_properties JSON blob + val props: JSONObject? = parseTemplateProperties(data.getString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES)) + + // Required fields from blob + title = props?.optJSONObject(AJOTemplatePropertyKeys.TITLE) + ?.optString(AJOTemplatePropertyKeys.SubKeys.TEXT) + ?.takeIf { it.isNotEmpty() } + ?: data.getRequiredString(PushPayloadKeys.TITLE) + + body = props?.optJSONObject(AJOTemplatePropertyKeys.BODY) + ?.optString(AJOTemplatePropertyKeys.SubKeys.TEXT) + ?.takeIf { it.isNotEmpty() } + ?: data.getRequiredString(PushPayloadKeys.BODY) + + // Optional fields from blob + payloadVersion = props?.optString(AJOTemplatePropertyKeys.VERSION) + ?.takeIf { it.isNotEmpty() } + ?: DEFAULT_PAYLOAD_VERSION + + imageUrl = props?.optJSONObject(AJOTemplatePropertyKeys.IMAGE) + ?.optString(AJOTemplatePropertyKeys.SubKeys.URL) + ?.takeIf { it.isNotEmpty() } + ?: data.getString(PushPayloadKeys.IMAGE_URL) + + largeIcon = props?.optJSONObject(AJOTemplatePropertyKeys.LARGE_ICON) + ?.optString(AJOTemplatePropertyKeys.SubKeys.URL) + ?.takeIf { it.isNotEmpty() } + + // Flat FCM keys — general notification config (backward-compat zone) + smallIcon = data.getString(PushPayloadKeys.SMALL_ICON) + ?: data.getString(PushPayloadKeys.LEGACY_SMALL_ICON) + sound = data.getString(PushPayloadKeys.SOUND) + channelId = data.getString(PushPayloadKeys.CHANNEL_ID) + badgeCount = data.getInteger(PushPayloadKeys.BADGE_COUNT) ?: 0 + isNotificationSticky = data.getBoolean(PushPayloadKeys.STICKY) ?: false + ticker = data.getString(PushPayloadKeys.TICKER) + priority = NotificationPriority.fromString(data.getString(PushPayloadKeys.PRIORITY)) + visibility = NotificationVisibility.fromString(data.getString(PushPayloadKeys.VISIBILITY)) + actionUri = data.getString(PushPayloadKeys.ACTION_URI) + actionType = ActionType.valueOf( + data.getString(PushPayloadKeys.ACTION_TYPE) ?: ActionType.NONE.name + ) + } + + @RequiresApi(api = Build.VERSION_CODES.N) + fun getNotificationImportance(): Int = + notificationImportanceMap[priority.stringValue] ?: NotificationManager.IMPORTANCE_DEFAULT + + private fun parseTemplateProperties(raw: String?): JSONObject? { + if (raw.isNullOrEmpty()) return null + return try { + JSONObject(raw) + } catch (e: JSONException) { + Log.warning( + "AJOPushTemplate", + SELF_TAG, + "Failed to parse adb_template_properties: ${e.localizedMessage}" + ) + null + } + } + + companion object { + private const val DEFAULT_PAYLOAD_VERSION = "1" + + @RequiresApi(api = Build.VERSION_CODES.N) + internal val notificationImportanceMap: Map = mapOf( + NotificationPriority.PRIORITY_MIN.toString() to NotificationManager.IMPORTANCE_MIN, + NotificationPriority.PRIORITY_LOW.toString() to NotificationManager.IMPORTANCE_LOW, + NotificationPriority.PRIORITY_DEFAULT.toString() to NotificationManager.IMPORTANCE_DEFAULT, + NotificationPriority.PRIORITY_HIGH.toString() to NotificationManager.IMPORTANCE_HIGH, + NotificationPriority.PRIORITY_MAX.toString() to NotificationManager.IMPORTANCE_MAX + ) + } +} diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt index aad48296..197da283 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt @@ -1,4 +1,4 @@ -/* + /* Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy diff --git a/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml b/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml new file mode 100644 index 00000000..97197db3 --- /dev/null +++ b/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml b/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml new file mode 100644 index 00000000..4798ebf3 --- /dev/null +++ b/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_fit_center.xml b/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_fit_center.xml new file mode 100644 index 00000000..43533d55 --- /dev/null +++ b/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_fit_center.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json b/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json new file mode 100644 index 00000000..0801e959 --- /dev/null +++ b/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json @@ -0,0 +1,16 @@ +{ + "adb_template_type": "ajo_basic", + "adb_title": "AJO Basic - Center Crop", + "adb_body": "This image is scaled using CENTER_CROP.", + "adb_image": "https://slimages.macysassets.com/is/image/MCY/products/9/optimized/27433493_fpx.tif?wid=1200&fmt=jpeg&qlt=100", + "adb_icon": "ic_launcher_background", + "adb_sound": "bells", + "adb_channel_id": "ajo_basic_channel", + "adb_n_count": "1", + "adb_n_priority": "PRIORITY_HIGH", + "adb_n_visibility": "PUBLIC", + "adb_a_type": "WEBURL", + "adb_uri": "https://www.adobe.com", + "adb_act": "[{\"label\":\"Learn More\",\"uri\":\"https://www.adobe.com\",\"type\":\"WEBURL\"},{\"label\":\"Open App\",\"uri\":\"\",\"type\":\"OPENAPP\"}]", + "adb_template_properties": "{\"adb_version\":\"1\",\"adb_title\":{\"text\":\"AJO Basic - Center Crop\"},\"adb_body\":{\"text\":\"CENTER_CROP fills the image completely — it may be cropped on the edges.\"},\"adb_image\":{\"url\":\"https://slimages.macysassets.com/is/image/MCY/products/9/optimized/27433493_fpx.tif?wid=1200&fmt=jpeg&qlt=100\",\"scale_type\":\"center_crop\"},\"adb_large_icon\":{\"url\":\"https://cdn-icons-png.flaticon.com/128/864/864639.png\",\"scale_type\":\"center_crop\"}}" +} diff --git a/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json b/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json new file mode 100644 index 00000000..2c17d73f --- /dev/null +++ b/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json @@ -0,0 +1,16 @@ +{ + "adb_template_type": "ajo_basic", + "adb_title": "AJO Basic - Fit Center", + "adb_body": "This image is scaled using FIT_CENTER.", + "adb_image": "https://slimages.macysassets.com/is/image/MCY/products/9/optimized/27433493_fpx.tif?wid=1200&fmt=jpeg&qlt=100", + "adb_icon": "ic_launcher_background", + "adb_sound": "bells", + "adb_channel_id": "ajo_basic_channel", + "adb_n_count": "1", + "adb_n_priority": "PRIORITY_HIGH", + "adb_n_visibility": "PUBLIC", + "adb_a_type": "WEBURL", + "adb_uri": "https://www.adobe.com", + "adb_act": "[{\"label\":\"Learn More\",\"uri\":\"https://www.adobe.com\",\"type\":\"WEBURL\"},{\"label\":\"Open App\",\"uri\":\"\",\"type\":\"OPENAPP\"}]", + "adb_template_properties": "{\"adb_version\":\"1\",\"adb_title\":{\"text\":\"AJO Basic - Fit Center\"},\"adb_body\":{\"text\":\"FIT_CENTER shows the full image — aspect ratio is preserved with possible empty space.\"},\"adb_image\":{\"url\":\"https://plus.unsplash.com/premium_photo-1690576837258-8750545fb430?q=80&w=3390&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"scale_type\":\"fit_center\"},\"adb_large_icon\":{\"url\":\"https://cdn-icons-png.flaticon.com/128/864/864639.png\",\"scale_type\":\"fit_center\"}}" +} diff --git a/code/testapp/src/main/assets/basic/basic.json b/code/testapp/src/main/assets/basic/basic.json index b4a9e6e1..c314a1b9 100644 --- a/code/testapp/src/main/assets/basic/basic.json +++ b/code/testapp/src/main/assets/basic/basic.json @@ -6,7 +6,7 @@ "adb_body_ex": "Basic push template with action buttons.", "adb_a_type": "WEBURL", "adb_uri": "https://chess.com/games", - "adb_image": "https://images.pexels.com/photos/260024/pexels-photo-260024.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", + "adb_image": "https://slimages.macysassets.com/is/image/MCY/products/9/optimized/27433493_fpx.tif?wid=1200&fmt=jpeg&qlt=100", "adb_act": "[{\"label\":\"Go to chess.com\",\"uri\":\"https:\/\/chess.com\/games\/552\",\"type\":\"DEEPLINK\"},{\"label\":\"Open the app\",\"uri\":\"\",\"type\":\"OPENAPP\"}]", "adb_sound": "bells", "adb_channel_id": "2024", @@ -14,4 +14,5 @@ "adb_n_priority": "PRIORITY_HIGH", "adb_n_visibility": "PRIVATE", "adb_sticky": "true" -} \ No newline at end of file +} + diff --git a/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/Template.kt b/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/Template.kt index 282bd095..05e8e812 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/Template.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/Template.kt @@ -21,5 +21,6 @@ enum class Template(val displayName: String, val directoryName: String) { ZeroBezel("Zero Bezel", "zerobezel"), InputBox("InputBox", "inputbox"), FiveIcon("Five Icon", "fiveicon"), - Rating("Rating", "rating") + Rating("Rating", "rating"), + AJOBasic("AJO Basic", "ajo_basic") } diff --git a/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/UINotificationBuilderActivity.kt b/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/UINotificationBuilderActivity.kt index 716a6b5e..52b94d5f 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/UINotificationBuilderActivity.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/UINotificationBuilderActivity.kt @@ -101,9 +101,12 @@ fun NotificationJSONSelector(activity: Activity) { val PLACEHOLDER_SELECT_FILE = "Select JSON ▼" val sharedPreferences = activity.getSharedPreferences(SharedPreferenceKeys.NAME, Context.MODE_PRIVATE) val selectedTemplate = remember { mutableStateOf( - Template.valueOf( - sharedPreferences.getString(SharedPreferenceKeys.SELECTED_TEMPLATE, Template.Timer.displayName) ?: Template.Timer.displayName - )) } + runCatching { + Template.valueOf( + sharedPreferences.getString(SharedPreferenceKeys.SELECTED_TEMPLATE, Template.Timer.name) ?: Template.Timer.name + ) + }.getOrDefault(Template.Timer) + ) } val expanded = remember { mutableStateOf(false) } val selectedFile = remember { mutableStateOf(sharedPreferences.getString(SharedPreferenceKeys.SELECTED_FILE, PLACEHOLDER_SELECT_FILE) ?: PLACEHOLDER_SELECT_FILE) } var files = FileUtil.getFilesInPath(activity, selectedTemplate.value.directoryName) From 358c4ab589a4aa6bfc6bce32cc6cb714dc6aef94 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Fri, 5 Jun 2026 18:17:29 +0530 Subject: [PATCH 02/12] Apply Spotless formatting --- .../notificationbuilder/NotificationBuilder.kt | 4 ++-- .../notificationbuilder/PushTemplateConstants.kt | 14 +++++++------- .../internal/ajo/templates/AJOPushTemplate.kt | 2 +- .../internal/templates/AEPPushTemplate.kt | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt index 0312c0d0..96c184bf 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt @@ -21,6 +21,8 @@ import com.adobe.marketing.mobile.notificationbuilder.NotificationBuilderConstan import com.adobe.marketing.mobile.notificationbuilder.NotificationBuilderConstants.VERSION import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -31,8 +33,6 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductC import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductRatingNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder -import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder -import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AEPPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AutoCarouselPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.BasicPushTemplate diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt index a86b6262..f8e53e94 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt @@ -202,22 +202,22 @@ object PushTemplateConstants { // Keys for parsing the adb_template_properties JSON blob for AJO templates. // These are NOT top-level FCM keys — they are keys inside the parsed JSON object. internal object AJOTemplatePropertyKeys { - internal const val VERSION = "adb_version" - internal const val TITLE = "adb_title" - internal const val BODY = "adb_body" - internal const val IMAGE = "adb_image" + internal const val VERSION = "adb_version" + internal const val TITLE = "adb_title" + internal const val BODY = "adb_body" + internal const val IMAGE = "adb_image" internal const val LARGE_ICON = "adb_large_icon" // Sub-field names shared across nested objects (e.g. adb_title.text, adb_image.url) internal object SubKeys { - internal const val TEXT = "text" - internal const val URL = "url" + internal const val TEXT = "text" + internal const val URL = "url" internal const val SCALE_TYPE = "scale_type" } internal object ScaleType { internal const val CENTER_CROP = "center_crop" - internal const val FIT_CENTER = "fit_center" + internal const val FIT_CENTER = "fit_center" } } } diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt index 8320f7fc..7cfa1a8b 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt @@ -16,8 +16,8 @@ import android.os.Build import androidx.annotation.RequiresApi import com.adobe.marketing.mobile.notificationbuilder.NotificationPriority import com.adobe.marketing.mobile.notificationbuilder.NotificationVisibility -import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.ActionType import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.ActionType import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType import com.adobe.marketing.mobile.notificationbuilder.internal.util.IntentData diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt index 197da283..aad48296 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AEPPushTemplate.kt @@ -1,4 +1,4 @@ - /* +/* Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy From fd8b43ef8882c4d6f13d6f2ea38ae9a2872d0200 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Sun, 7 Jun 2026 17:07:39 +0530 Subject: [PATCH 03/12] Add unit tests for AJO basic push template and notification builder --- .../NotificationBuilderTests.kt | 17 ++ .../AJOBasicNotificationBuilderTest.kt | 132 ++++++++++ .../ajo/templates/AJOBasicPushTemplateTest.kt | 239 ++++++++++++++++++ .../internal/templates/MockDataUtils.kt | 29 +++ 4 files changed, 417 insertions(+) create mode 100644 code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt create mode 100644 code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index 99b180dd..0cf8978d 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -22,6 +22,7 @@ import androidx.core.app.NotificationManagerCompat import com.adobe.marketing.mobile.notificationbuilder.internal.PendingIntentUtils import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateImageUtils import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -32,6 +33,9 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductC import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductRatingNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockAEPPushTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockCarousalTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockProductCatalogTemplateDataProvider @@ -95,6 +99,7 @@ class NotificationBuilderTests { mockkObject(MultiIconNotificationBuilder) mockkObject(TimerNotificationBuilder) mockkObject(LegacyNotificationBuilder) + mockkObject(AJOBasicNotificationBuilder) } private fun setupApplicationMocks() { @@ -289,6 +294,18 @@ class NotificationBuilderTests { verify(exactly = 1) { TimerNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } } + @Test + fun `verify private createNotificationBuilder calls AJOBasicNotificationBuilder construct`() { + val mapData = mutableMapOf( + PushTemplateConstants.PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushTemplateConstants.PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushTemplateConstants.PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushTemplateConstants.PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + NotificationBuilder.constructNotificationBuilder(mapData, trackerActivityClass, broadcastReceiverClass) + verify(exactly = 1) { AJOBasicNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } + } + private fun setNullContext() { val mockAppContextService = mockk() val mockServiceProvider = mockkClass(ServiceProvider::class) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt new file mode 100644 index 00000000..5ba478a0 --- /dev/null +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt @@ -0,0 +1,132 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys +import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateImageUtils +import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate +import com.adobe.marketing.mobile.notificationbuilder.internal.builders.DummyActivity +import com.adobe.marketing.mobile.notificationbuilder.internal.builders.DummyBroadcastReceiver +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER +import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockkConstructor +import io.mockk.mockkObject +import io.mockk.unmockkAll +import junit.framework.TestCase.assertNotNull +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [31]) +class AJOBasicNotificationBuilderTest { + + private lateinit var context: Context + private lateinit var trackerActivityClass: Class + private lateinit var broadcastReceiverClass: Class + + @Before + fun setUp() { + context = RuntimeEnvironment.getApplication() + trackerActivityClass = DummyActivity::class.java + broadcastReceiverClass = DummyBroadcastReceiver::class.java + mockkObject(PushTemplateImageUtils) + mockkConstructor(RemoteViews::class) + every { anyConstructed().setTextViewText(any(), any()) } just Runs + every { anyConstructed().setImageViewBitmap(any(), any()) } just Runs + every { anyConstructed().setViewVisibility(any(), any()) } just Runs + every { anyConstructed().setOnClickPendingIntent(any(), any()) } just Runs + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `construct returns NotificationCompat Builder for center_crop template`() { + val pushTemplate = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP + ) + ) + ) + + val result = AJOBasicNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct returns NotificationCompat Builder for fit_center template`() { + val pushTemplate = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + + val result = AJOBasicNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct returns NotificationCompat Builder when no blob is present`() { + val pushTemplate = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + + val result = AJOBasicNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } +} diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt new file mode 100644 index 00000000..d70fbefd --- /dev/null +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt @@ -0,0 +1,239 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates + +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys +import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_BODY +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_IMAGE_URL +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_LARGE_ICON_URL +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TITLE +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MOCKED_ACTION_BUTTON_DATA +import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class AJOBasicPushTemplateTest { + + // ── AJOPushTemplate (base class) parsing ────────────────────────────────── + + @Test + fun `AJOPushTemplate parses blob title and body over flat keys`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertEquals(AJO_MOCKED_TITLE, template.title) + assertEquals(AJO_MOCKED_BODY, template.body) + } + + @Test + fun `AJOPushTemplate falls back to flat title and body when blob is absent`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_TITLE, template.title) + assertEquals(AJO_MOCKED_FLAT_BODY, template.body) + } + + @Test + fun `AJOPushTemplate falls back to flat keys when blob is malformed JSON`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to "not valid json {{{" + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_TITLE, template.title) + assertEquals(AJO_MOCKED_FLAT_BODY, template.body) + } + + @Test + fun `AJOPushTemplate parses imageUrl and largeIcon from blob`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertEquals(AJO_MOCKED_IMAGE_URL, template.imageUrl) + assertEquals(AJO_MOCKED_LARGE_ICON_URL, template.largeIcon) + } + + @Test + fun `AJOPushTemplate falls back to flat imageUrl when blob has no image`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.IMAGE_URL to "https://example.com/flat.jpg" + ) + ) + ) + assertEquals("https://example.com/flat.jpg", template.imageUrl) + } + + @Test + fun `AJOPushTemplate reads flat notification properties`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.SOUND to "bells", + PushPayloadKeys.CHANNEL_ID to "ajo_channel", + PushPayloadKeys.PRIORITY to "PRIORITY_HIGH", + PushPayloadKeys.VISIBILITY to "PUBLIC", + PushPayloadKeys.BADGE_COUNT to "3", + PushPayloadKeys.STICKY to "true", + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertEquals("bells", template.sound) + assertEquals("ajo_channel", template.channelId) + assertEquals(3, template.badgeCount) + assertEquals(true, template.isNotificationSticky) + } + + // ── AJOBasicPushTemplate specific ───────────────────────────────────────── + + @Test + fun `AJOBasicPushTemplate sets imgScaleType to fit_center from blob`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.FIT_CENTER, template.imgScaleType) + assertEquals(AJOTemplatePropertyKeys.ScaleType.FIT_CENTER, template.largeIconScaleType) + } + + @Test + fun `AJOBasicPushTemplate sets imgScaleType to center_crop from blob`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.largeIconScaleType) + } + + @Test + fun `AJOBasicPushTemplate defaults imgScaleType to center_crop when scale_type key is absent`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.largeIconScaleType) + } + + @Test + fun `AJOBasicPushTemplate parses action buttons from flat adb_act key`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to MOCKED_ACTION_BUTTON_DATA, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertEquals(2, template.actionButtonsList?.size) + assertEquals("Go to chess.com", template.actionButtonsList?.get(0)?.label) + assertEquals("Open the app", template.actionButtonsList?.get(1)?.label) + } + + @Test + fun `AJOBasicPushTemplate returns null action buttons when adb_act is absent`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertNull(template.actionButtonsList) + } + + @Test + fun `AJOBasicPushTemplate returns null action buttons when adb_act is invalid JSON`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to "not json", + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertNull(template.actionButtonsList) + } +} diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt index b44ebeb6..42a8b454 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt @@ -55,6 +55,35 @@ const val MOCKED_MALFORMED_JSON_ACTION_BUTTON = "[" + "{\"label\":\"Open the app\",\"uri\":\"\",\"type\":\"GO_TO_WEB_PAGE\"}," + "{\"label\":\"Go to chess.com\",\"uri\":\"https://chess.com/games/552\",\"type\":\"DEEPLINK\"}]" const val MOCKED_CHANNEL_ID = "AEPSDKPushChannel1" + +// AJO template test data +const val AJO_MOCKED_TITLE = "AJO Blob Title" +const val AJO_MOCKED_BODY = "AJO Blob Body" +const val AJO_MOCKED_IMAGE_URL = "https://example.com/ajo_img.jpg" +const val AJO_MOCKED_LARGE_ICON_URL = "https://example.com/ajo_icon.png" +const val AJO_MOCKED_FLAT_TITLE = "AJO Flat Title" +const val AJO_MOCKED_FLAT_BODY = "AJO Flat Body" + +const val AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER = + "{\"adb_version\":\"1\"," + + "\"adb_title\":{\"text\":\"AJO Blob Title\"}," + + "\"adb_body\":{\"text\":\"AJO Blob Body\"}," + + "\"adb_image\":{\"url\":\"https://example.com/ajo_img.jpg\",\"scale_type\":\"fit_center\"}," + + "\"adb_large_icon\":{\"url\":\"https://example.com/ajo_icon.png\",\"scale_type\":\"fit_center\"}}" + +const val AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP = + "{\"adb_version\":\"1\"," + + "\"adb_title\":{\"text\":\"AJO Blob Title\"}," + + "\"adb_body\":{\"text\":\"AJO Blob Body\"}," + + "\"adb_image\":{\"url\":\"https://example.com/ajo_img.jpg\",\"scale_type\":\"center_crop\"}," + + "\"adb_large_icon\":{\"url\":\"https://example.com/ajo_icon.png\",\"scale_type\":\"center_crop\"}}" + +const val AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE = + "{\"adb_version\":\"1\"," + + "\"adb_title\":{\"text\":\"AJO Blob Title\"}," + + "\"adb_body\":{\"text\":\"AJO Blob Body\"}," + + "\"adb_image\":{\"url\":\"https://example.com/ajo_img.jpg\"}," + + "\"adb_large_icon\":{\"url\":\"https://example.com/ajo_icon.png\"}}" const val MOCKED_RECEIVER_NAME = "receiverName" const val MOCKED_HINT = "hint" const val MOCKED_FEEDBACK_TEXT = "feedbackText" From 48e16d6270a04fd3c0b636b2c4a5c3aee306248e Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Mon, 8 Jun 2026 19:47:31 +0530 Subject: [PATCH 04/12] refactor(ajo_basic): merge expanded layouts and honor large icon scale type - Collapse two scale-type expanded XMLs into ajo_basic_push_template_expanded.xml - Use visibility toggling for centerCrop/fitCenter on both expanded image and large icon - Update collapsed layout to use large_icon_container FrameLayout pattern - Delete ajo_push_template_expanded_center_crop.xml and ajo_push_template_expanded_fit_center.xml --- .../builders/AJOBasicNotificationBuilder.kt | 42 +++++++++++------- ...l => ajo_basic_push_template_expanded.xml} | 43 +++++++++++++++---- .../layout/ajo_push_template_collapsed.xml | 28 +++++++++--- ...ajo_push_template_expanded_center_crop.xml | 43 ------------------- 4 files changed, 84 insertions(+), 72 deletions(-) rename code/notificationbuilder/src/main/res/layout/{ajo_push_template_expanded_fit_center.xml => ajo_basic_push_template_expanded.xml} (54%) delete mode 100644 code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt index a0553987..124a2f3f 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt @@ -18,6 +18,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.media.RingtoneManager import android.os.Build +import android.view.View import android.widget.RemoteViews import androidx.core.app.NotificationCompat import com.adobe.marketing.mobile.notificationbuilder.NotificationConstructionFailedException @@ -57,15 +58,7 @@ internal object AJOBasicNotificationBuilder { Log.trace(LOG_TAG, SELF_TAG, "Building an AJO basic template push notification.") val packageName = context.packageName val smallLayout = RemoteViews(packageName, R.layout.ajo_push_template_collapsed) - - // pick the expanded layout based on the requested image scale type - val expandedLayoutId = - if (pushTemplate.imgScaleType == AJOTemplatePropertyKeys.ScaleType.FIT_CENTER) { - R.layout.ajo_push_template_expanded_fit_center - } else { - R.layout.ajo_push_template_expanded_center_crop - } - val expandedLayout = RemoteViews(packageName, expandedLayoutId) + val expandedLayout = RemoteViews(packageName, R.layout.ajo_basic_push_template_expanded) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -77,12 +70,31 @@ internal object AJOBasicNotificationBuilder { expandedLayout.setTextViewText(R.id.notification_title, pushTemplate.title) expandedLayout.setTextViewText(R.id.notification_body_expanded, pushTemplate.body) - // set a large icon if one is present - smallLayout.setRemoteViewImage(pushTemplate.largeIcon, R.id.large_icon) - expandedLayout.setRemoteViewImage(pushTemplate.largeIcon, R.id.large_icon) - - // set the expanded image (scaled per the layout chosen above) - expandedLayout.setRemoteViewImage(pushTemplate.imageUrl, R.id.expanded_template_image) + // set large icon with the correct scale type view, hide the other + val largeIconIsFitCenter = + pushTemplate.largeIconScaleType == AJOTemplatePropertyKeys.ScaleType.FIT_CENTER + val (largeIconVisibleId, largeIconGoneId) = if (largeIconIsFitCenter) { + R.id.large_icon_fit_center to R.id.large_icon_center_crop + } else { + R.id.large_icon_center_crop to R.id.large_icon_fit_center + } + smallLayout.setViewVisibility(largeIconGoneId, View.GONE) + smallLayout.setViewVisibility(largeIconVisibleId, View.VISIBLE) + smallLayout.setRemoteViewImage(pushTemplate.largeIcon, largeIconVisibleId) + expandedLayout.setViewVisibility(largeIconGoneId, View.GONE) + expandedLayout.setViewVisibility(largeIconVisibleId, View.VISIBLE) + expandedLayout.setRemoteViewImage(pushTemplate.largeIcon, largeIconVisibleId) + + // set the expanded image with the correct scale type view, hide the other + val (expandedImageVisibleId, expandedImageGoneId) = + if (pushTemplate.imgScaleType == AJOTemplatePropertyKeys.ScaleType.FIT_CENTER) { + R.id.expanded_image_fit_center to R.id.expanded_image_center_crop + } else { + R.id.expanded_image_center_crop to R.id.expanded_image_fit_center + } + expandedLayout.setViewVisibility(expandedImageGoneId, View.GONE) + expandedLayout.setViewVisibility(expandedImageVisibleId, View.VISIBLE) + expandedLayout.setRemoteViewImage(pushTemplate.imageUrl, expandedImageVisibleId) val builder = NotificationCompat.Builder(context, channelIdToUse) .setTicker(pushTemplate.ticker) diff --git a/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_fit_center.xml b/code/notificationbuilder/src/main/res/layout/ajo_basic_push_template_expanded.xml similarity index 54% rename from code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_fit_center.xml rename to code/notificationbuilder/src/main/res/layout/ajo_basic_push_template_expanded.xml index 43533d55..de333102 100644 --- a/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_fit_center.xml +++ b/code/notificationbuilder/src/main/res/layout/ajo_basic_push_template_expanded.xml @@ -8,11 +8,27 @@ android:orientation="vertical" android:theme="@style/DayNightTheme"> - + android:layout_height="@dimen/large_icon_height" + android:layout_alignParentRight="true"> + + + + + + + android:layout_toLeftOf="@+id/large_icon_container"/> + android:layout_toLeftOf="@+id/large_icon_container"/> + + + android:layout_below="@id/notification_body_expanded" + android:visibility="gone" /> + diff --git a/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml b/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml index 97197db3..258908c8 100644 --- a/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml +++ b/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml @@ -8,12 +8,28 @@ android:orientation="vertical" android:theme="@style/DayNightTheme"> - + android:layout_centerVertical="true"> + + + + + + + android:layout_toLeftOf="@+id/large_icon_container"/> + android:layout_toLeftOf="@+id/large_icon_container"/> diff --git a/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml b/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml deleted file mode 100644 index 4798ebf3..00000000 --- a/code/notificationbuilder/src/main/res/layout/ajo_push_template_expanded_center_crop.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - From edc764b579fd65e146b4e3f3fe5b039c3169bcf6 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Mon, 8 Jun 2026 20:12:15 +0530 Subject: [PATCH 05/12] add missing tests --- .../AJOBasicNotificationBuilderTest.kt | 64 +++++++++++ .../ajo/templates/AJOBasicPushTemplateTest.kt | 106 ++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt index 5ba478a0..713ceddb 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt @@ -14,6 +14,7 @@ package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders import android.app.Activity import android.content.BroadcastReceiver import android.content.Context +import android.os.Bundle import android.widget.RemoteViews import androidx.core.app.NotificationCompat import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys @@ -26,6 +27,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOC import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER +import com.adobe.marketing.mobile.notificationbuilder.internal.util.IntentData import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData import io.mockk.Runs import io.mockk.every @@ -129,4 +131,66 @@ class AJOBasicNotificationBuilderTest { assertNotNull(result) assert(result is NotificationCompat.Builder) } + + @Test + fun `construct uses silent channel when template is from intent`() { + val bundle = Bundle().apply { + putString(PushPayloadKeys.TEMPLATE_TYPE, PushTemplateType.AJO_BASIC.value) + putString(PushPayloadKeys.TITLE, AJO_MOCKED_FLAT_TITLE) + putString(PushPayloadKeys.BODY, AJO_MOCKED_FLAT_BODY) + putString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES, AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP) + } + val pushTemplate = AJOBasicPushTemplate(IntentData(bundle, null)) + + val result = AJOBasicNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + @Config(sdk = [21]) + fun `construct returns builder on pre-Oreo device`() { + val pushTemplate = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP + ) + ) + ) + + val result = AJOBasicNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct succeeds with custom sound`() { + val pushTemplate = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.SOUND to "bells", + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP + ) + ) + ) + + val result = AJOBasicNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } } diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt index d70fbefd..d3d945e2 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt @@ -11,6 +11,7 @@ package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates +import com.adobe.marketing.mobile.notificationbuilder.NotificationPriority import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType @@ -26,6 +27,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOC import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MOCKED_ACTION_BUTTON_DATA import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith @@ -236,4 +238,108 @@ class AJOBasicPushTemplateTest { ) assertNull(template.actionButtonsList) } + + // ── AJOPushTemplate uncovered fields ────────────────────────────────────── + + @Test + fun `AJOPushTemplate parses payloadVersion from blob`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertEquals("1", template.payloadVersion) + } + + @Test + fun `AJOPushTemplate defaults payloadVersion to 1 when blob is absent`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + assertEquals("1", template.payloadVersion) + } + + @Test + fun `AJOPushTemplate parses priority from flat key`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.PRIORITY to "PRIORITY_HIGH" + ) + ) + ) + assertEquals(NotificationPriority.PRIORITY_HIGH, template.priority) + } + + @Test + fun `AJOPushTemplate defaults priority to PRIORITY_DEFAULT when absent`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + assertEquals(NotificationPriority.PRIORITY_DEFAULT, template.priority) + } + + @Test + fun `AJOPushTemplate parses templateType as AJO_BASIC`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + assertEquals(PushTemplateType.AJO_BASIC, template.templateType) + } + + @Test + fun `AJOBasicPushTemplate stores raw actionButtonsString`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to MOCKED_ACTION_BUTTON_DATA + ) + ) + ) + assertEquals(MOCKED_ACTION_BUTTON_DATA, template.actionButtonsString) + } + + @Test + fun `AJOBasicPushTemplate skips null action buttons and returns only valid ones`() { + val template = AJOBasicPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to MOCKED_MALFORMED_JSON_ACTION_BUTTON + ) + ) + ) + assertNotNull(template.actionButtonsList) + } } From b45858b09c112f2a214f88af1b0f58755f779846 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Mon, 8 Jun 2026 23:50:18 +0530 Subject: [PATCH 06/12] fix(tests): add missing import and AJO_BIG_TEXT dispatch test for CI coverage --- .../NotificationBuilderTests.kt | 15 +++++++++++++++ .../ajo/templates/AJOBasicPushTemplateTest.kt | 1 + 2 files changed, 16 insertions(+) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index 0cf8978d..f5c4f944 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -23,6 +23,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.PendingIntentUtil import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateImageUtils import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBigTextNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -33,6 +34,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductC import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductRatingNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_BIGTEXT_PROPS_FULL import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER @@ -100,6 +102,7 @@ class NotificationBuilderTests { mockkObject(TimerNotificationBuilder) mockkObject(LegacyNotificationBuilder) mockkObject(AJOBasicNotificationBuilder) + mockkObject(AJOBigTextNotificationBuilder) } private fun setupApplicationMocks() { @@ -306,6 +309,18 @@ class NotificationBuilderTests { verify(exactly = 1) { AJOBasicNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } } + @Test + fun `verify private createNotificationBuilder calls AJOBigTextNotificationBuilder construct`() { + val mapData = mutableMapOf( + PushTemplateConstants.PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushTemplateConstants.PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushTemplateConstants.PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushTemplateConstants.PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + NotificationBuilder.constructNotificationBuilder(mapData, trackerActivityClass, broadcastReceiverClass) + verify(exactly = 1) { AJOBigTextNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } + } + private fun setNullContext() { val mockAppContextService = mockk() val mockServiceProvider = mockkClass(ServiceProvider::class) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt index d3d945e2..5391655c 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt @@ -25,6 +25,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOC import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MOCKED_ACTION_BUTTON_DATA +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MOCKED_MALFORMED_JSON_ACTION_BUTTON import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull From 1ec0ba61e2b2228fb1bd1578308d684b069d92a4 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Tue, 9 Jun 2026 00:20:52 +0530 Subject: [PATCH 07/12] fix(tests): remove bigtext refs and fix missing import in AJOBasicPushTemplateTest --- .../NotificationBuilderTests.kt | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index f5c4f944..0cf8978d 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -23,7 +23,6 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.PendingIntentUtil import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateImageUtils import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder -import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBigTextNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -34,7 +33,6 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductC import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductRatingNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_BIGTEXT_PROPS_FULL import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER @@ -102,7 +100,6 @@ class NotificationBuilderTests { mockkObject(TimerNotificationBuilder) mockkObject(LegacyNotificationBuilder) mockkObject(AJOBasicNotificationBuilder) - mockkObject(AJOBigTextNotificationBuilder) } private fun setupApplicationMocks() { @@ -309,18 +306,6 @@ class NotificationBuilderTests { verify(exactly = 1) { AJOBasicNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } } - @Test - fun `verify private createNotificationBuilder calls AJOBigTextNotificationBuilder construct`() { - val mapData = mutableMapOf( - PushTemplateConstants.PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, - PushTemplateConstants.PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, - PushTemplateConstants.PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushTemplateConstants.PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL - ) - NotificationBuilder.constructNotificationBuilder(mapData, trackerActivityClass, broadcastReceiverClass) - verify(exactly = 1) { AJOBigTextNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } - } - private fun setNullContext() { val mockAppContextService = mockk() val mockServiceProvider = mockkClass(ServiceProvider::class) From b167a9f6f7f874cc11cdfda05f9fedbab47fb833 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Tue, 9 Jun 2026 13:02:49 +0530 Subject: [PATCH 08/12] test: cover unknown carousel type else branch in NotificationBuilder --- .../notificationbuilder/NotificationBuilderTests.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index 0cf8978d..cd1e281e 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -37,6 +37,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOC import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockAEPPushTemplateDataProvider +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.CarouselPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockCarousalTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockProductCatalogTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockTimerTemplateDataProvider @@ -236,6 +237,17 @@ class NotificationBuilderTests { verify(exactly = 1) { AutoCarouselNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } } + @Test + fun `verify private createNotificationBuilder falls back to LegacyNotificationBuilder for unknown carousel type`() { + mockkObject(CarouselPushTemplate.Companion) + val unknownCarousel = mockk(relaxed = true) + every { CarouselPushTemplate(any()) } returns unknownCarousel + + val mapData = MockCarousalTemplateDataProvider.getMockedMapWithAutoCarouselData() + NotificationBuilder.constructNotificationBuilder(mapData, trackerActivityClass, broadcastReceiverClass) + verify(exactly = 1) { LegacyNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass) } + } + @Test fun `verify private createNotificationBuilder calls InputBoxNotificationBuilder construct`() { val mapData = MockAEPPushTemplateDataProvider.getMockedAEPDataMapWithAllKeys() From 780a6267c6385c814919f6e650dbfb953ba64a02 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Tue, 9 Jun 2026 13:13:25 +0530 Subject: [PATCH 09/12] fix: sort CarouselPushTemplate import to satisfy Spotless --- .../mobile/notificationbuilder/NotificationBuilderTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index cd1e281e..63f0687e 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -36,8 +36,8 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBeze import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockAEPPushTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.CarouselPushTemplate +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockAEPPushTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockCarousalTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockProductCatalogTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockTimerTemplateDataProvider From 9ec1c8e92e202cad35d3b529c7e58215a8f51388 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Tue, 9 Jun 2026 17:02:22 +0530 Subject: [PATCH 10/12] fix: remove untestable carousel else branch test --- code/notificationbuilder/build.gradle.kts | 2 +- .../notificationbuilder/NotificationBuilderTests.kt | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/code/notificationbuilder/build.gradle.kts b/code/notificationbuilder/build.gradle.kts index 60943c91..81ec93b8 100644 --- a/code/notificationbuilder/build.gradle.kts +++ b/code/notificationbuilder/build.gradle.kts @@ -38,6 +38,6 @@ aepLibrary { dependencies { implementation("com.adobe.marketing.mobile:core:$mavenCoreVersion") - testImplementation("org.robolectric:robolectric:4.7") + testImplementation("org.robolectric:robolectric:4.11.1") testImplementation("io.mockk:mockk:1.13.11") } diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index 63f0687e..0cf8978d 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -36,7 +36,6 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBeze import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.CarouselPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockAEPPushTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockCarousalTemplateDataProvider import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockProductCatalogTemplateDataProvider @@ -237,17 +236,6 @@ class NotificationBuilderTests { verify(exactly = 1) { AutoCarouselNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } } - @Test - fun `verify private createNotificationBuilder falls back to LegacyNotificationBuilder for unknown carousel type`() { - mockkObject(CarouselPushTemplate.Companion) - val unknownCarousel = mockk(relaxed = true) - every { CarouselPushTemplate(any()) } returns unknownCarousel - - val mapData = MockCarousalTemplateDataProvider.getMockedMapWithAutoCarouselData() - NotificationBuilder.constructNotificationBuilder(mapData, trackerActivityClass, broadcastReceiverClass) - verify(exactly = 1) { LegacyNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass) } - } - @Test fun `verify private createNotificationBuilder calls InputBoxNotificationBuilder construct`() { val mapData = MockAEPPushTemplateDataProvider.getMockedAEPDataMapWithAllKeys() From cf70e9c4083f00eed5f0b1be8e12d0cae522fd9f Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Tue, 23 Jun 2026 01:42:51 +0530 Subject: [PATCH 11/12] refactor: flatten ajo_basic schema and extend AEPPushTemplate --- .../NotificationBuilder.kt | 2 +- .../PushTemplateConstants.kt | 14 +- .../builders/AJOBasicNotificationBuilder.kt | 19 +- .../internal/ajo/templates/AJOPushTemplate.kt | 172 ------------------ .../templates/AJOBasicPushTemplate.kt | 51 +++--- .../NotificationBuilderTests.kt | 1 + .../AJOBasicNotificationBuilderTest.kt | 8 +- .../templates/AJOBasicPushTemplateTest.kt | 161 ++++++++-------- .../internal/templates/MockDataUtils.kt | 26 +-- .../ajo_basic/ajo_basic_center_crop.json | 5 +- .../ajo_basic/ajo_basic_fit_center.json | 5 +- 11 files changed, 125 insertions(+), 339 deletions(-) delete mode 100644 code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt rename code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/{ajo => }/templates/AJOBasicPushTemplate.kt (72%) rename code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/{ajo => }/templates/AJOBasicPushTemplateTest.kt (71%) diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt index 96c184bf..cb13987e 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt @@ -22,7 +22,6 @@ import com.adobe.marketing.mobile.notificationbuilder.NotificationBuilderConstan import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder -import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -34,6 +33,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductR import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AEPPushTemplate +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AutoCarouselPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.BasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.CarouselPushTemplate diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt index f8e53e94..ab470546 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt @@ -202,18 +202,8 @@ object PushTemplateConstants { // Keys for parsing the adb_template_properties JSON blob for AJO templates. // These are NOT top-level FCM keys — they are keys inside the parsed JSON object. internal object AJOTemplatePropertyKeys { - internal const val VERSION = "adb_version" - internal const val TITLE = "adb_title" - internal const val BODY = "adb_body" - internal const val IMAGE = "adb_image" - internal const val LARGE_ICON = "adb_large_icon" - - // Sub-field names shared across nested objects (e.g. adb_title.text, adb_image.url) - internal object SubKeys { - internal const val TEXT = "text" - internal const val URL = "url" - internal const val SCALE_TYPE = "scale_type" - } + // Basic template: scale type applied to the expanded hero image. + internal const val IMAGE_SCALE_TYPE = "adb_image_scale_type" internal object ScaleType { internal const val CENTER_CROP = "center_crop" diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt index 124a2f3f..79d4424d 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt @@ -26,7 +26,6 @@ import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG import com.adobe.marketing.mobile.notificationbuilder.R -import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.addActionButtons import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.getSoundUriForResourceName import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setNotificationClickAction @@ -34,6 +33,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setNot import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setRemoteViewImage import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setSmallIcon import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setSound +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.services.Log /** @@ -70,20 +70,9 @@ internal object AJOBasicNotificationBuilder { expandedLayout.setTextViewText(R.id.notification_title, pushTemplate.title) expandedLayout.setTextViewText(R.id.notification_body_expanded, pushTemplate.body) - // set large icon with the correct scale type view, hide the other - val largeIconIsFitCenter = - pushTemplate.largeIconScaleType == AJOTemplatePropertyKeys.ScaleType.FIT_CENTER - val (largeIconVisibleId, largeIconGoneId) = if (largeIconIsFitCenter) { - R.id.large_icon_fit_center to R.id.large_icon_center_crop - } else { - R.id.large_icon_center_crop to R.id.large_icon_fit_center - } - smallLayout.setViewVisibility(largeIconGoneId, View.GONE) - smallLayout.setViewVisibility(largeIconVisibleId, View.VISIBLE) - smallLayout.setRemoteViewImage(pushTemplate.largeIcon, largeIconVisibleId) - expandedLayout.setViewVisibility(largeIconGoneId, View.GONE) - expandedLayout.setViewVisibility(largeIconVisibleId, View.VISIBLE) - expandedLayout.setRemoteViewImage(pushTemplate.largeIcon, largeIconVisibleId) + // the basic template has no large side icon — hide the container in both layouts + smallLayout.setViewVisibility(R.id.large_icon_container, View.GONE) + expandedLayout.setViewVisibility(R.id.large_icon_container, View.GONE) // set the expanded image with the correct scale type view, hide the other val (expandedImageVisibleId, expandedImageGoneId) = diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt deleted file mode 100644 index 7cfa1a8b..00000000 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOPushTemplate.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - Copyright 2024 Adobe. All rights reserved. - This file is licensed to you under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. You may obtain a copy - of the License at http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under - the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - OF ANY KIND, either express or implied. See the License for the specific language - governing permissions and limitations under the License. -*/ - -package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates - -import android.app.NotificationManager -import android.os.Build -import androidx.annotation.RequiresApi -import com.adobe.marketing.mobile.notificationbuilder.NotificationPriority -import com.adobe.marketing.mobile.notificationbuilder.NotificationVisibility -import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys -import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.ActionType -import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys -import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType -import com.adobe.marketing.mobile.notificationbuilder.internal.util.IntentData -import com.adobe.marketing.mobile.notificationbuilder.internal.util.NotificationData -import com.adobe.marketing.mobile.services.Log -import org.json.JSONException -import org.json.JSONObject - -/** - * Base class for all Adobe Journey Optimizer (AJO) push templates. - * - * Parsing strategy (two zones): - * - [adb_template_properties] blob → title, body, imageUrl, largeIcon (template-specific fields) - * - Flat FCM data keys → sound, channelId, priority, visibility, actionType, actionUri, - * badgeCount, sticky (general notification config — backward-compat with old campaigns) - */ -internal sealed class AJOPushTemplate(val data: NotificationData) { - - private val SELF_TAG = "AJOPushTemplate" - - // Required — sourced from adb_template_properties blob - internal val title: String - - // Required — sourced from adb_template_properties blob - internal val body: String - - // Optional — sourced from adb_template_properties blob - internal val imageUrl: String? - - // Optional — sourced from adb_template_properties blob - internal val largeIcon: String? - - // Optional — version of the template schema, sourced from blob. Defaults to "1". - internal val payloadVersion: String - - // --- Fields below are sourced from flat FCM data keys (backward-compat zone) --- - - // Optional, small icon resource name. Reads adb_small_icon then falls back to adb_icon. - internal val smallIcon: String? - - // Optional, sound to play when the notification is shown - internal val sound: String? - - // Optional, number to show on the badge of the app - internal val badgeCount: Int - - // Optional, priority of the notification - internal val priority: NotificationPriority - - // Optional, visibility of the notification - internal val visibility: NotificationVisibility - - // Optional, notification channel (Android O+) - internal val channelId: String? - - // Optional, action type for the notification tap - internal val actionType: ActionType? - - // Optional, action uri for the notification tap - internal val actionUri: String? - - // Optional, ticker text for accessibility services - internal val ticker: String? - - // Optional, the type of push template this payload contains - internal val templateType: PushTemplateType? - - // Optional, when true the notification persists after the user taps it - internal val isNotificationSticky: Boolean - - // Flag to denote if the PushTemplate was built from an intent - internal val isFromIntent: Boolean - - init { - templateType = PushTemplateType.fromString(data.getString(PushPayloadKeys.TEMPLATE_TYPE)) - isFromIntent = data is IntentData - - // Parse the adb_template_properties JSON blob - val props: JSONObject? = parseTemplateProperties(data.getString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES)) - - // Required fields from blob - title = props?.optJSONObject(AJOTemplatePropertyKeys.TITLE) - ?.optString(AJOTemplatePropertyKeys.SubKeys.TEXT) - ?.takeIf { it.isNotEmpty() } - ?: data.getRequiredString(PushPayloadKeys.TITLE) - - body = props?.optJSONObject(AJOTemplatePropertyKeys.BODY) - ?.optString(AJOTemplatePropertyKeys.SubKeys.TEXT) - ?.takeIf { it.isNotEmpty() } - ?: data.getRequiredString(PushPayloadKeys.BODY) - - // Optional fields from blob - payloadVersion = props?.optString(AJOTemplatePropertyKeys.VERSION) - ?.takeIf { it.isNotEmpty() } - ?: DEFAULT_PAYLOAD_VERSION - - imageUrl = props?.optJSONObject(AJOTemplatePropertyKeys.IMAGE) - ?.optString(AJOTemplatePropertyKeys.SubKeys.URL) - ?.takeIf { it.isNotEmpty() } - ?: data.getString(PushPayloadKeys.IMAGE_URL) - - largeIcon = props?.optJSONObject(AJOTemplatePropertyKeys.LARGE_ICON) - ?.optString(AJOTemplatePropertyKeys.SubKeys.URL) - ?.takeIf { it.isNotEmpty() } - - // Flat FCM keys — general notification config (backward-compat zone) - smallIcon = data.getString(PushPayloadKeys.SMALL_ICON) - ?: data.getString(PushPayloadKeys.LEGACY_SMALL_ICON) - sound = data.getString(PushPayloadKeys.SOUND) - channelId = data.getString(PushPayloadKeys.CHANNEL_ID) - badgeCount = data.getInteger(PushPayloadKeys.BADGE_COUNT) ?: 0 - isNotificationSticky = data.getBoolean(PushPayloadKeys.STICKY) ?: false - ticker = data.getString(PushPayloadKeys.TICKER) - priority = NotificationPriority.fromString(data.getString(PushPayloadKeys.PRIORITY)) - visibility = NotificationVisibility.fromString(data.getString(PushPayloadKeys.VISIBILITY)) - actionUri = data.getString(PushPayloadKeys.ACTION_URI) - actionType = ActionType.valueOf( - data.getString(PushPayloadKeys.ACTION_TYPE) ?: ActionType.NONE.name - ) - } - - @RequiresApi(api = Build.VERSION_CODES.N) - fun getNotificationImportance(): Int = - notificationImportanceMap[priority.stringValue] ?: NotificationManager.IMPORTANCE_DEFAULT - - private fun parseTemplateProperties(raw: String?): JSONObject? { - if (raw.isNullOrEmpty()) return null - return try { - JSONObject(raw) - } catch (e: JSONException) { - Log.warning( - "AJOPushTemplate", - SELF_TAG, - "Failed to parse adb_template_properties: ${e.localizedMessage}" - ) - null - } - } - - companion object { - private const val DEFAULT_PAYLOAD_VERSION = "1" - - @RequiresApi(api = Build.VERSION_CODES.N) - internal val notificationImportanceMap: Map = mapOf( - NotificationPriority.PRIORITY_MIN.toString() to NotificationManager.IMPORTANCE_MIN, - NotificationPriority.PRIORITY_LOW.toString() to NotificationManager.IMPORTANCE_LOW, - NotificationPriority.PRIORITY_DEFAULT.toString() to NotificationManager.IMPORTANCE_DEFAULT, - NotificationPriority.PRIORITY_HIGH.toString() to NotificationManager.IMPORTANCE_HIGH, - NotificationPriority.PRIORITY_MAX.toString() to NotificationManager.IMPORTANCE_MAX - ) - } -} diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplate.kt similarity index 72% rename from code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplate.kt rename to code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplate.kt index 77fb8938..ef6e7f6a 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplate.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplate.kt @@ -9,36 +9,50 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates +package com.adobe.marketing.mobile.notificationbuilder.internal.templates import androidx.annotation.VisibleForTesting import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.BasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.util.NotificationData import com.adobe.marketing.mobile.services.Log import org.json.JSONArray import org.json.JSONException import org.json.JSONObject +/** + * Parses the raw [adb_template_properties] blob into a [JSONObject], returning null when the blob + * is missing or malformed. Shared by the AJO template subclasses. + */ +internal fun parseAjoTemplateProperties(raw: String?): JSONObject? { + if (raw.isNullOrEmpty()) return null + return try { + JSONObject(raw) + } catch (e: JSONException) { + Log.warning( + LOG_TAG, "AJOTemplateProperties", + "Failed to parse adb_template_properties: ${e.localizedMessage}" + ) + null + } +} + /** * Represents the AJO "ajo_basic" push template. * - * Template-specific fields (imgScaleType, largeIconScaleType) are read from the - * [adb_template_properties] JSON blob parsed by [AJOPushTemplate]. - * Action buttons are read from the flat [adb_act] key, consistent with ACC/AJO handling. + * All general fields (title, body, version, image url, icons, sound, action, etc.) are parsed by + * [AEPPushTemplate] from the flat top-level FCM keys. The only template-specific field lives in the + * [adb_template_properties] blob as the flat [adb_image_scale_type] key. This template has no large + * side icon. Action buttons are read from the flat [adb_act] key. */ -internal class AJOBasicPushTemplate(data: NotificationData) : AJOPushTemplate(data) { +internal class AJOBasicPushTemplate(data: NotificationData) : AEPPushTemplate(data) { private val SELF_TAG = "AJOBasicPushTemplate" // Scale type for the main expanded image. Defaults to CENTER_CROP. internal val imgScaleType: String - // Scale type for the large icon. Defaults to CENTER_CROP. - internal val largeIconScaleType: String - // Optional, action buttons for the notification as a raw string internal val actionButtonsString: String? @@ -46,17 +60,11 @@ internal class AJOBasicPushTemplate(data: NotificationData) : AJOPushTemplate(da internal val actionButtonsList: List? init { - val props: JSONObject? = parsePropertiesBlob( + val props: JSONObject? = parseAjoTemplateProperties( data.getString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES) ) - imgScaleType = props?.optJSONObject(AJOTemplatePropertyKeys.IMAGE) - ?.optString(AJOTemplatePropertyKeys.SubKeys.SCALE_TYPE) - ?.takeIf { it.isNotEmpty() } - ?: AJOTemplatePropertyKeys.ScaleType.CENTER_CROP - - largeIconScaleType = props?.optJSONObject(AJOTemplatePropertyKeys.LARGE_ICON) - ?.optString(AJOTemplatePropertyKeys.SubKeys.SCALE_TYPE) + imgScaleType = props?.optString(AJOTemplatePropertyKeys.IMAGE_SCALE_TYPE) ?.takeIf { it.isNotEmpty() } ?: AJOTemplatePropertyKeys.ScaleType.CENTER_CROP @@ -92,13 +100,4 @@ internal class AJOBasicPushTemplate(data: NotificationData) : AJOPushTemplate(da } return actionButtonList } - - private fun parsePropertiesBlob(raw: String?): JSONObject? { - if (raw.isNullOrEmpty()) return null - return try { - JSONObject(raw) - } catch (e: JSONException) { - null - } - } } diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index 0cf8978d..498bf4fe 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -298,6 +298,7 @@ class NotificationBuilderTests { fun `verify private createNotificationBuilder calls AJOBasicNotificationBuilder construct`() { val mapData = mutableMapOf( PushTemplateConstants.PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushTemplateConstants.PushPayloadKeys.VERSION to "1", PushTemplateConstants.PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushTemplateConstants.PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushTemplateConstants.PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt index 713ceddb..893ae285 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt @@ -20,9 +20,9 @@ import androidx.core.app.NotificationCompat import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateImageUtils import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType -import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.builders.DummyActivity import com.adobe.marketing.mobile.notificationbuilder.internal.builders.DummyBroadcastReceiver +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJOBasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP @@ -76,6 +76,7 @@ class AJOBasicNotificationBuilderTest { MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP @@ -97,6 +98,7 @@ class AJOBasicNotificationBuilderTest { MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER @@ -118,6 +120,7 @@ class AJOBasicNotificationBuilderTest { MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY ) @@ -136,6 +139,7 @@ class AJOBasicNotificationBuilderTest { fun `construct uses silent channel when template is from intent`() { val bundle = Bundle().apply { putString(PushPayloadKeys.TEMPLATE_TYPE, PushTemplateType.AJO_BASIC.value) + putString(PushPayloadKeys.VERSION, "1") putString(PushPayloadKeys.TITLE, AJO_MOCKED_FLAT_TITLE) putString(PushPayloadKeys.BODY, AJO_MOCKED_FLAT_BODY) putString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES, AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP) @@ -157,6 +161,7 @@ class AJOBasicNotificationBuilderTest { MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP @@ -178,6 +183,7 @@ class AJOBasicNotificationBuilderTest { MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.SOUND to "bells", diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplateTest.kt similarity index 71% rename from code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt rename to code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplateTest.kt index 5391655c..e609cc22 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/templates/AJOBasicPushTemplateTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplateTest.kt @@ -9,23 +9,13 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.templates +package com.adobe.marketing.mobile.notificationbuilder.internal.templates import com.adobe.marketing.mobile.notificationbuilder.NotificationPriority import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.ActionType import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_BODY -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_IMAGE_URL -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_LARGE_ICON_URL -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TITLE -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MOCKED_ACTION_BUTTON_DATA -import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MOCKED_MALFORMED_JSON_ACTION_BUTTON import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -37,92 +27,79 @@ import org.mockito.junit.MockitoJUnitRunner @RunWith(MockitoJUnitRunner::class) class AJOBasicPushTemplateTest { - // ── AJOPushTemplate (base class) parsing ────────────────────────────────── + // ── Inherited AEPPushTemplate parsing ───────────────────────────────────── @Test - fun `AJOPushTemplate parses blob title and body over flat keys`() { + fun `parses title body and version from flat keys`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER ) ) ) - assertEquals(AJO_MOCKED_TITLE, template.title) - assertEquals(AJO_MOCKED_BODY, template.body) - } - - @Test - fun `AJOPushTemplate falls back to flat title and body when blob is absent`() { - val template = AJOBasicPushTemplate( - MapData( - mutableMapOf( - PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, - PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, - PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY - ) - ) - ) assertEquals(AJO_MOCKED_FLAT_TITLE, template.title) assertEquals(AJO_MOCKED_FLAT_BODY, template.body) + assertEquals("1", template.payloadVersion) } - @Test - fun `AJOPushTemplate falls back to flat keys when blob is malformed JSON`() { - val template = AJOBasicPushTemplate( + @Test(expected = IllegalArgumentException::class) + fun `throws when required adb_version is absent`() { + AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, - PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to "not valid json {{{" + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY ) ) ) - assertEquals(AJO_MOCKED_FLAT_TITLE, template.title) - assertEquals(AJO_MOCKED_FLAT_BODY, template.body) } @Test - fun `AJOPushTemplate parses imageUrl and largeIcon from blob`() { + fun `parses imageUrl from flat adb_image key`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.IMAGE_URL to AJO_MOCKED_IMAGE_URL, PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER ) ) ) assertEquals(AJO_MOCKED_IMAGE_URL, template.imageUrl) - assertEquals(AJO_MOCKED_LARGE_ICON_URL, template.largeIcon) } @Test - fun `AJOPushTemplate falls back to flat imageUrl when blob has no image`() { + fun `imageUrl is null when flat adb_image is absent`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.IMAGE_URL to "https://example.com/flat.jpg" + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER ) ) ) - assertEquals("https://example.com/flat.jpg", template.imageUrl) + assertNull(template.imageUrl) } @Test - fun `AJOPushTemplate reads flat notification properties`() { + fun `reads flat notification properties`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.SOUND to "bells", @@ -139,187 +116,194 @@ class AJOBasicPushTemplateTest { assertEquals("ajo_channel", template.channelId) assertEquals(3, template.badgeCount) assertEquals(true, template.isNotificationSticky) + assertEquals(NotificationPriority.PRIORITY_HIGH, template.priority) } - // ── AJOBasicPushTemplate specific ───────────────────────────────────────── - @Test - fun `AJOBasicPushTemplate sets imgScaleType to fit_center from blob`() { + fun `parses templateType as AJO_BASIC`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, - PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY ) ) ) - assertEquals(AJOTemplatePropertyKeys.ScaleType.FIT_CENTER, template.imgScaleType) - assertEquals(AJOTemplatePropertyKeys.ScaleType.FIT_CENTER, template.largeIconScaleType) + assertEquals(PushTemplateType.AJO_BASIC, template.templateType) } @Test - fun `AJOBasicPushTemplate sets imgScaleType to center_crop from blob`() { + fun `uses legacy small icon when adb_small_icon is absent`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP + PushPayloadKeys.LEGACY_SMALL_ICON to "legacy_icon" ) ) ) - assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) - assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.largeIconScaleType) + assertEquals("legacy_icon", template.smallIcon) } @Test - fun `AJOBasicPushTemplate defaults imgScaleType to center_crop when scale_type key is absent`() { + fun `parses actionType from flat adb_a_type key`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE + PushPayloadKeys.ACTION_TYPE to ActionType.DEEPLINK.name ) ) ) - assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) - assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.largeIconScaleType) + assertEquals(ActionType.DEEPLINK, template.actionType) } + // ── AJOBasicPushTemplate specific (blob) ────────────────────────────────── + @Test - fun `AJOBasicPushTemplate parses action buttons from flat adb_act key`() { + fun `sets imgScaleType to fit_center from blob`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.ACTION_BUTTONS to MOCKED_ACTION_BUTTON_DATA, PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER ) ) ) - assertEquals(2, template.actionButtonsList?.size) - assertEquals("Go to chess.com", template.actionButtonsList?.get(0)?.label) - assertEquals("Open the app", template.actionButtonsList?.get(1)?.label) + assertEquals(AJOTemplatePropertyKeys.ScaleType.FIT_CENTER, template.imgScaleType) } @Test - fun `AJOBasicPushTemplate returns null action buttons when adb_act is absent`() { + fun `sets imgScaleType to center_crop from blob`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP ) ) ) - assertNull(template.actionButtonsList) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) } @Test - fun `AJOBasicPushTemplate returns null action buttons when adb_act is invalid JSON`() { + fun `defaults imgScaleType to center_crop when scale_type key is absent`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.ACTION_BUTTONS to "not json", - PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE ) ) ) - assertNull(template.actionButtonsList) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) } - // ── AJOPushTemplate uncovered fields ────────────────────────────────────── - @Test - fun `AJOPushTemplate parses payloadVersion from blob`() { + fun `defaults imgScaleType to center_crop when blob is absent`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, - PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY ) ) ) - assertEquals("1", template.payloadVersion) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) } @Test - fun `AJOPushTemplate defaults payloadVersion to 1 when blob is absent`() { + fun `defaults imgScaleType to center_crop when blob is malformed`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, - PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to "not valid json {{{" ) ) ) - assertEquals("1", template.payloadVersion) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) } @Test - fun `AJOPushTemplate parses priority from flat key`() { + fun `parses action buttons from flat adb_act key`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, - PushPayloadKeys.PRIORITY to "PRIORITY_HIGH" + PushPayloadKeys.ACTION_BUTTONS to MOCKED_ACTION_BUTTON_DATA, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER ) ) ) - assertEquals(NotificationPriority.PRIORITY_HIGH, template.priority) + assertEquals(2, template.actionButtonsList?.size) + assertEquals("Go to chess.com", template.actionButtonsList?.get(0)?.label) + assertEquals("Open the app", template.actionButtonsList?.get(1)?.label) } @Test - fun `AJOPushTemplate defaults priority to PRIORITY_DEFAULT when absent`() { + fun `returns null action buttons when adb_act is absent`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY ) ) ) - assertEquals(NotificationPriority.PRIORITY_DEFAULT, template.priority) + assertNull(template.actionButtonsList) } @Test - fun `AJOPushTemplate parses templateType as AJO_BASIC`() { + fun `returns null action buttons when adb_act is invalid JSON`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, - PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to "not json" ) ) ) - assertEquals(PushTemplateType.AJO_BASIC, template.templateType) + assertNull(template.actionButtonsList) } @Test - fun `AJOBasicPushTemplate stores raw actionButtonsString`() { + fun `stores raw actionButtonsString`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.ACTION_BUTTONS to MOCKED_ACTION_BUTTON_DATA @@ -330,11 +314,12 @@ class AJOBasicPushTemplateTest { } @Test - fun `AJOBasicPushTemplate skips null action buttons and returns only valid ones`() { + fun `skips null action buttons and returns only valid ones`() { val template = AJOBasicPushTemplate( MapData( mutableMapOf( PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BASIC.value, + PushPayloadKeys.VERSION to "1", PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, PushPayloadKeys.ACTION_BUTTONS to MOCKED_MALFORMED_JSON_ACTION_BUTTON diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt index 42a8b454..227e8974 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt @@ -56,34 +56,20 @@ const val MOCKED_MALFORMED_JSON_ACTION_BUTTON = "[" + "{\"label\":\"Go to chess.com\",\"uri\":\"https://chess.com/games/552\",\"type\":\"DEEPLINK\"}]" const val MOCKED_CHANNEL_ID = "AEPSDKPushChannel1" -// AJO template test data -const val AJO_MOCKED_TITLE = "AJO Blob Title" -const val AJO_MOCKED_BODY = "AJO Blob Body" +// AJO template test data. Title, body, image url and version come from flat top-level FCM keys; +// only template-specific flat keys live inside the adb_template_properties blob. const val AJO_MOCKED_IMAGE_URL = "https://example.com/ajo_img.jpg" -const val AJO_MOCKED_LARGE_ICON_URL = "https://example.com/ajo_icon.png" const val AJO_MOCKED_FLAT_TITLE = "AJO Flat Title" const val AJO_MOCKED_FLAT_BODY = "AJO Flat Body" +// Basic: the blob only carries the flat adb_image_scale_type key const val AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER = - "{\"adb_version\":\"1\"," + - "\"adb_title\":{\"text\":\"AJO Blob Title\"}," + - "\"adb_body\":{\"text\":\"AJO Blob Body\"}," + - "\"adb_image\":{\"url\":\"https://example.com/ajo_img.jpg\",\"scale_type\":\"fit_center\"}," + - "\"adb_large_icon\":{\"url\":\"https://example.com/ajo_icon.png\",\"scale_type\":\"fit_center\"}}" + "{\"adb_image_scale_type\":\"fit_center\"}" const val AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP = - "{\"adb_version\":\"1\"," + - "\"adb_title\":{\"text\":\"AJO Blob Title\"}," + - "\"adb_body\":{\"text\":\"AJO Blob Body\"}," + - "\"adb_image\":{\"url\":\"https://example.com/ajo_img.jpg\",\"scale_type\":\"center_crop\"}," + - "\"adb_large_icon\":{\"url\":\"https://example.com/ajo_icon.png\",\"scale_type\":\"center_crop\"}}" + "{\"adb_image_scale_type\":\"center_crop\"}" -const val AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE = - "{\"adb_version\":\"1\"," + - "\"adb_title\":{\"text\":\"AJO Blob Title\"}," + - "\"adb_body\":{\"text\":\"AJO Blob Body\"}," + - "\"adb_image\":{\"url\":\"https://example.com/ajo_img.jpg\"}," + - "\"adb_large_icon\":{\"url\":\"https://example.com/ajo_icon.png\"}}" +const val AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE = "{}" const val MOCKED_RECEIVER_NAME = "receiverName" const val MOCKED_HINT = "hint" const val MOCKED_FEEDBACK_TEXT = "feedbackText" diff --git a/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json b/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json index 0801e959..36617b77 100644 --- a/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json +++ b/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json @@ -1,7 +1,8 @@ { "adb_template_type": "ajo_basic", + "adb_version": "1", "adb_title": "AJO Basic - Center Crop", - "adb_body": "This image is scaled using CENTER_CROP.", + "adb_body": "CENTER_CROP fills the image completely — it may be cropped on the edges.", "adb_image": "https://slimages.macysassets.com/is/image/MCY/products/9/optimized/27433493_fpx.tif?wid=1200&fmt=jpeg&qlt=100", "adb_icon": "ic_launcher_background", "adb_sound": "bells", @@ -12,5 +13,5 @@ "adb_a_type": "WEBURL", "adb_uri": "https://www.adobe.com", "adb_act": "[{\"label\":\"Learn More\",\"uri\":\"https://www.adobe.com\",\"type\":\"WEBURL\"},{\"label\":\"Open App\",\"uri\":\"\",\"type\":\"OPENAPP\"}]", - "adb_template_properties": "{\"adb_version\":\"1\",\"adb_title\":{\"text\":\"AJO Basic - Center Crop\"},\"adb_body\":{\"text\":\"CENTER_CROP fills the image completely — it may be cropped on the edges.\"},\"adb_image\":{\"url\":\"https://slimages.macysassets.com/is/image/MCY/products/9/optimized/27433493_fpx.tif?wid=1200&fmt=jpeg&qlt=100\",\"scale_type\":\"center_crop\"},\"adb_large_icon\":{\"url\":\"https://cdn-icons-png.flaticon.com/128/864/864639.png\",\"scale_type\":\"center_crop\"}}" + "adb_template_properties": "{\"adb_image_scale_type\":\"center_crop\"}" } diff --git a/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json b/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json index 2c17d73f..ce3baaab 100644 --- a/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json +++ b/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json @@ -1,7 +1,8 @@ { "adb_template_type": "ajo_basic", + "adb_version": "1", "adb_title": "AJO Basic - Fit Center", - "adb_body": "This image is scaled using FIT_CENTER.", + "adb_body": "FIT_CENTER shows the full image — aspect ratio is preserved with possible empty space.", "adb_image": "https://slimages.macysassets.com/is/image/MCY/products/9/optimized/27433493_fpx.tif?wid=1200&fmt=jpeg&qlt=100", "adb_icon": "ic_launcher_background", "adb_sound": "bells", @@ -12,5 +13,5 @@ "adb_a_type": "WEBURL", "adb_uri": "https://www.adobe.com", "adb_act": "[{\"label\":\"Learn More\",\"uri\":\"https://www.adobe.com\",\"type\":\"WEBURL\"},{\"label\":\"Open App\",\"uri\":\"\",\"type\":\"OPENAPP\"}]", - "adb_template_properties": "{\"adb_version\":\"1\",\"adb_title\":{\"text\":\"AJO Basic - Fit Center\"},\"adb_body\":{\"text\":\"FIT_CENTER shows the full image — aspect ratio is preserved with possible empty space.\"},\"adb_image\":{\"url\":\"https://plus.unsplash.com/premium_photo-1690576837258-8750545fb430?q=80&w=3390&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\"scale_type\":\"fit_center\"},\"adb_large_icon\":{\"url\":\"https://cdn-icons-png.flaticon.com/128/864/864639.png\",\"scale_type\":\"fit_center\"}}" + "adb_template_properties": "{\"adb_image_scale_type\":\"fit_center\"}" } From 51b75daab39b2dba87e1074bafee801a43c39e63 Mon Sep 17 00:00:00 2001 From: Ritu Singh Date: Tue, 23 Jun 2026 01:56:13 +0530 Subject: [PATCH 12/12] feat: add ajo_bigtext template extending AEPPushTemplate --- .../NotificationBuilder.kt | 11 + .../PushTemplateConstants.kt | 10 +- .../internal/PushTemplateType.kt | 2 + .../builders/AJOBigTextNotificationBuilder.kt | 191 +++++++++++ .../templates/AJOBigTextPushTemplate.kt | 98 ++++++ .../ajo_bigtext_push_template_expanded.xml | 54 +++ .../NotificationBuilderTests.kt | 16 + .../AJOBigTextNotificationBuilderTest.kt | 258 +++++++++++++++ .../templates/AJOBigTextPushTemplateTest.kt | 313 ++++++++++++++++++ .../internal/templates/MockDataUtils.kt | 12 + 10 files changed, 964 insertions(+), 1 deletion(-) create mode 100644 code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilder.kt create mode 100644 code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplate.kt create mode 100644 code/notificationbuilder/src/main/res/layout/ajo_bigtext_push_template_expanded.xml create mode 100644 code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilderTest.kt create mode 100644 code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplateTest.kt diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt index cb13987e..e798058a 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt @@ -22,6 +22,7 @@ import com.adobe.marketing.mobile.notificationbuilder.NotificationBuilderConstan import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBigTextNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -34,6 +35,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNot import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AEPPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJOBasicPushTemplate +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJOBigTextPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AutoCarouselPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.BasicPushTemplate import com.adobe.marketing.mobile.notificationbuilder.internal.templates.CarouselPushTemplate @@ -233,6 +235,15 @@ object NotificationBuilder { ) } + PushTemplateType.AJO_BIG_TEXT -> { + return AJOBigTextNotificationBuilder.construct( + context, + AJOBigTextPushTemplate(notificationData), + trackerActivityClass, + broadcastReceiverClass + ) + } + PushTemplateType.UNKNOWN -> { return LegacyNotificationBuilder.construct( context, diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt index ab470546..f1a4cc50 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/PushTemplateConstants.kt @@ -200,11 +200,19 @@ object PushTemplateConstants { } // Keys for parsing the adb_template_properties JSON blob for AJO templates. - // These are NOT top-level FCM keys — they are keys inside the parsed JSON object. + // These are NOT top-level FCM keys — they are flat keys inside the parsed JSON object. + // Only template-specific fields live here; general fields (title, body, image, version) + // are read from the top-level flat FCM keys in [PushPayloadKeys]. internal object AJOTemplatePropertyKeys { // Basic template: scale type applied to the expanded hero image. internal const val IMAGE_SCALE_TYPE = "adb_image_scale_type" + // BigText template: short text shown in the collapsed state. + internal const val COLLAPSED_TEXT = "adb_collapsed_text" + + // BigText template: large side icon url. + internal const val LARGE_ICON = "adb_large_icon" + internal object ScaleType { internal const val CENTER_CROP = "center_crop" internal const val FIT_CENTER = "fit_center" diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt index 745eb63d..5806810f 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/PushTemplateType.kt @@ -24,6 +24,7 @@ internal enum class PushTemplateType(val value: String) { MULTI_ICON("icon"), TIMER("timer"), AJO_BASIC("ajo_basic"), + AJO_BIG_TEXT("ajo_bigtext"), UNKNOWN("unknown"); companion object { @@ -44,6 +45,7 @@ internal enum class PushTemplateType(val value: String) { "icon" -> MULTI_ICON "timer" -> TIMER "ajo_basic" -> AJO_BASIC + "ajo_bigtext" -> AJO_BIG_TEXT else -> UNKNOWN } } diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilder.kt new file mode 100644 index 00000000..38d4b2b2 --- /dev/null +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilder.kt @@ -0,0 +1,191 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders + +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.media.RingtoneManager +import android.os.Build +import android.view.View +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import com.adobe.marketing.mobile.notificationbuilder.NotificationConstructionFailedException +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG +import com.adobe.marketing.mobile.notificationbuilder.R +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.addActionButtons +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.getSoundUriForResourceName +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setNotificationClickAction +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setNotificationDeleteAction +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setRemoteViewImage +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setSmallIcon +import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setSound +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJOBigTextPushTemplate +import com.adobe.marketing.mobile.services.Log + +/** + * Object responsible for constructing a [NotificationCompat.Builder] object containing an + * AJO big text ("ajo_bigtext") push template notification. + * + * Renders a BigText-style layout: collapsed shows title + short body + large icon; expanded shows + * title + full long body ([AJOBigTextPushTemplate.expandedBodyText]) + large icon. There is no + * hero image. The large icon scale type is applied by choosing between two pre-defined expanded + * layouts, mirroring the pattern used by [AJOBasicNotificationBuilder] for the hero image. + */ +internal object AJOBigTextNotificationBuilder { + private const val SELF_TAG = "AJOBigTextNotificationBuilder" + + @Throws(NotificationConstructionFailedException::class) + fun construct( + context: Context, + pushTemplate: AJOBigTextPushTemplate, + trackerActivityClass: Class?, + broadcastReceiverClass: Class? + ): NotificationCompat.Builder { + Log.trace(LOG_TAG, SELF_TAG, "Building an AJO big text template push notification.") + val packageName = context.packageName + val smallLayout = RemoteViews(packageName, R.layout.ajo_push_template_collapsed) + val expandedLayout = RemoteViews(packageName, R.layout.ajo_bigtext_push_template_expanded) + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channelIdToUse = createChannelIfRequired(context, notificationManager, pushTemplate) + + // set the title and body text. The collapsed state shows the short collapsed text while + // the expanded state shows the full long body text (the flat adb_body key). + smallLayout.setTextViewText(R.id.notification_title, pushTemplate.title) + smallLayout.setTextViewText(R.id.notification_body, pushTemplate.collapsedText) + expandedLayout.setTextViewText(R.id.notification_title, pushTemplate.title) + expandedLayout.setTextViewText(R.id.notification_body_expanded, pushTemplate.body) + + // the large side icon has no scale type option — always render with center crop and + // hide the fit center view + smallLayout.setViewVisibility(R.id.large_icon_fit_center, View.GONE) + smallLayout.setViewVisibility(R.id.large_icon_center_crop, View.VISIBLE) + smallLayout.setRemoteViewImage(pushTemplate.largeIconUrl, R.id.large_icon_center_crop) + expandedLayout.setViewVisibility(R.id.large_icon_fit_center, View.GONE) + expandedLayout.setViewVisibility(R.id.large_icon_center_crop, View.VISIBLE) + expandedLayout.setRemoteViewImage(pushTemplate.largeIconUrl, R.id.large_icon_center_crop) + + val builder = NotificationCompat.Builder(context, channelIdToUse) + .setTicker(pushTemplate.ticker) + .setNumber(pushTemplate.badgeCount) + .setAutoCancel(!pushTemplate.isNotificationSticky) + .setOngoing(pushTemplate.isNotificationSticky) + .setStyle(NotificationCompat.DecoratedCustomViewStyle()) + .setCustomContentView(smallLayout) + .setCustomBigContentView(expandedLayout) + // small icon must be present, otherwise the notification will not be displayed. + .setSmallIcon(context, pushTemplate.smallIcon, null) + .setVisibility(pushTemplate.visibility.value) + .setNotificationClickAction( + context, + trackerActivityClass, + pushTemplate.actionUri, + pushTemplate.actionType, + pushTemplate.data.getBundle() + ) + .setNotificationDeleteAction(context, trackerActivityClass) + + // if not from intent, set custom sound. applies to API 25 and lower only as + // API 26 and up set the sound on the notification channel. + if (!pushTemplate.isFromIntent) { + builder.setSound(context, pushTemplate.sound) + } + + // below API 26 (no notification channels) priority is set on the builder + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + builder.setPriority(NotificationCompat.PRIORITY_HIGH) + .setVibrate(LongArray(0)) + } + + // add any action buttons defined for the notification + builder.addActionButtons( + context, + trackerActivityClass, + pushTemplate.actionButtonsList, + pushTemplate.data.getBundle() + ) + + return builder + } + + /** + * Creates a notification channel if required. Logic mirrors the shared + * `NotificationManager.createNotificationChannelIfRequired` extension but is kept local so + * the AJO builder does not depend on or modify the AEPPushTemplate-typed shared extension. + * + * @param context the application [Context] + * @param notificationManager the [NotificationManager] used to create / look up channels + * @param pushTemplate the [AJOBigTextPushTemplate] providing channel id, sound and importance + * @return the channel ID to use for the notification + */ + private fun createChannelIfRequired( + context: Context, + notificationManager: NotificationManager, + pushTemplate: AJOBigTextPushTemplate + ): String { + val channelIdToUse = + if (pushTemplate.isFromIntent) { + PushTemplateConstants.DefaultValues.SILENT_NOTIFICATION_CHANNEL_ID + } else { + pushTemplate.channelId ?: PushTemplateConstants.DefaultValues.DEFAULT_CHANNEL_ID + } + + // no channel creation required below API 26 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return channelIdToUse + } + + // don't create a channel if it already exists + if (notificationManager.getNotificationChannel(channelIdToUse) != null) { + Log.trace( + LOG_TAG, + SELF_TAG, + "Using previously created notification channel: $channelIdToUse." + ) + return channelIdToUse + } + + val channel = NotificationChannel( + channelIdToUse, + if (pushTemplate.isFromIntent) { + PushTemplateConstants.DefaultValues.SILENT_CHANNEL_NAME + } else { + PushTemplateConstants.DefaultValues.DEFAULT_CHANNEL_NAME + }, + pushTemplate.getNotificationImportance() + ) + + if (pushTemplate.isFromIntent) { + channel.setSound(null, null) + } else { + val soundUri = if (pushTemplate.sound.isNullOrEmpty()) { + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } else { + context.getSoundUriForResourceName(pushTemplate.sound) + } + channel.setSound(soundUri, null) + } + + Log.trace( + LOG_TAG, + SELF_TAG, + "Creating a new notification channel with ID: $channelIdToUse." + ) + notificationManager.createNotificationChannel(channel) + return channelIdToUse + } +} diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplate.kt new file mode 100644 index 00000000..7f040419 --- /dev/null +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplate.kt @@ -0,0 +1,98 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.templates + +import androidx.annotation.VisibleForTesting +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.AJOTemplatePropertyKeys +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.LOG_TAG +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys +import com.adobe.marketing.mobile.notificationbuilder.internal.util.NotificationData +import com.adobe.marketing.mobile.services.Log +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +/** + * Represents the AJO "ajo_bigtext" push template. + * + * Renders a BigText-style notification with optional large-icon support in both collapsed and + * expanded states. There is no hero image. General fields are parsed by [AEPPushTemplate]. + * + * Body semantics: + * - The flat [adb_body] key holds the full long text shown in the expanded state — exposed as the + * inherited [body]. + * - The blob [adb_collapsed_text] key holds the short text shown in the collapsed state + * ([collapsedText]); it falls back to [body] when absent. + * + * The large side icon url comes from the blob [adb_large_icon] key ([largeIconUrl], no scale type). + * Note: the inherited [largeIcon] reads the flat key and is unused by this template. + */ +internal class AJOBigTextPushTemplate(data: NotificationData) : AEPPushTemplate(data) { + + private val SELF_TAG = "AJOBigTextPushTemplate" + + // Short text shown in the collapsed state. Falls back to `body` when absent. + internal val collapsedText: String + + // Optional, large side icon url sourced from the blob adb_large_icon key. + internal val largeIconUrl: String? + + // Optional, action buttons for the notification as a raw string + internal val actionButtonsString: String? + + // Optional, parsed list of action buttons + internal val actionButtonsList: List? + + init { + val props: JSONObject? = parseAjoTemplateProperties( + data.getString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES) + ) + + collapsedText = props?.optString(AJOTemplatePropertyKeys.COLLAPSED_TEXT) + ?.takeIf { it.isNotEmpty() } + ?: body + + largeIconUrl = props?.optString(AJOTemplatePropertyKeys.LARGE_ICON) + ?.takeIf { it.isNotEmpty() } + + actionButtonsString = data.getString(PushPayloadKeys.ACTION_BUTTONS) + actionButtonsList = getActionButtonsFromString(actionButtonsString) + } + + @VisibleForTesting + internal fun getActionButtonsFromString(actionButtons: String?): List? { + if (actionButtons == null) { + Log.debug( + LOG_TAG, SELF_TAG, + "Exception in converting actionButtons json string to json object, Error : actionButtons is null" + ) + return null + } + val actionButtonList = mutableListOf() + try { + val jsonArray = JSONArray(actionButtons) + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.getJSONObject(i) + val button = BasicPushTemplate.ActionButton.getActionButtonFromJSONObject(jsonObject) + ?: continue + actionButtonList.add(button) + } + } catch (e: JSONException) { + Log.warning( + LOG_TAG, SELF_TAG, + "Exception in converting actionButtons json string to json object, Error : ${e.localizedMessage}" + ) + return null + } + return actionButtonList + } +} diff --git a/code/notificationbuilder/src/main/res/layout/ajo_bigtext_push_template_expanded.xml b/code/notificationbuilder/src/main/res/layout/ajo_bigtext_push_template_expanded.xml new file mode 100644 index 00000000..16a9196d --- /dev/null +++ b/code/notificationbuilder/src/main/res/layout/ajo_bigtext_push_template_expanded.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt index 498bf4fe..6acb6ac2 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilderTests.kt @@ -23,6 +23,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.PendingIntentUtil import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateImageUtils import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBasicNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders.AJOBigTextNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -33,6 +34,7 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductC import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ProductRatingNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.TimerNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.ZeroBezelNotificationBuilder +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_BIGTEXT_PROPS_FULL import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER @@ -100,6 +102,7 @@ class NotificationBuilderTests { mockkObject(TimerNotificationBuilder) mockkObject(LegacyNotificationBuilder) mockkObject(AJOBasicNotificationBuilder) + mockkObject(AJOBigTextNotificationBuilder) } private fun setupApplicationMocks() { @@ -307,6 +310,19 @@ class NotificationBuilderTests { verify(exactly = 1) { AJOBasicNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } } + @Test + fun `verify private createNotificationBuilder calls AJOBigTextNotificationBuilder construct`() { + val mapData = mutableMapOf( + PushTemplateConstants.PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushTemplateConstants.PushPayloadKeys.VERSION to "1", + PushTemplateConstants.PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushTemplateConstants.PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushTemplateConstants.PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + NotificationBuilder.constructNotificationBuilder(mapData, trackerActivityClass, broadcastReceiverClass) + verify(exactly = 1) { AJOBigTextNotificationBuilder.construct(any(Context::class), any(), trackerActivityClass, broadcastReceiverClass) } + } + private fun setNullContext() { val mockAppContextService = mockk() val mockServiceProvider = mockkClass(ServiceProvider::class) diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilderTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilderTest.kt new file mode 100644 index 00000000..a48d941c --- /dev/null +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBigTextNotificationBuilderTest.kt @@ -0,0 +1,258 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.ajo.builders + +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.os.Bundle +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys +import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateImageUtils +import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType +import com.adobe.marketing.mobile.notificationbuilder.internal.builders.DummyActivity +import com.adobe.marketing.mobile.notificationbuilder.internal.builders.DummyBroadcastReceiver +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJOBigTextPushTemplate +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_BIGTEXT_PROPS_FULL +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_BIGTEXT_PROPS_NO_COLLAPSED +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_BODY +import com.adobe.marketing.mobile.notificationbuilder.internal.templates.AJO_MOCKED_FLAT_TITLE +import com.adobe.marketing.mobile.notificationbuilder.internal.util.IntentData +import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockkConstructor +import io.mockk.mockkObject +import io.mockk.unmockkAll +import junit.framework.TestCase.assertNotNull +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [31]) +class AJOBigTextNotificationBuilderTest { + + private lateinit var context: Context + private lateinit var trackerActivityClass: Class + private lateinit var broadcastReceiverClass: Class + + @Before + fun setUp() { + context = RuntimeEnvironment.getApplication() + trackerActivityClass = DummyActivity::class.java + broadcastReceiverClass = DummyBroadcastReceiver::class.java + mockkObject(PushTemplateImageUtils) + mockkConstructor(RemoteViews::class) + every { anyConstructed().setTextViewText(any(), any()) } just Runs + every { anyConstructed().setImageViewBitmap(any(), any()) } just Runs + every { anyConstructed().setViewVisibility(any(), any()) } just Runs + every { anyConstructed().setOnClickPendingIntent(any(), any()) } just Runs + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `construct returns NotificationCompat Builder for full bigtext payload with fit_center icon`() { + val pushTemplate = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct returns NotificationCompat Builder with large icon and no collapsed text`() { + val pushTemplate = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_NO_COLLAPSED + ) + ) + ) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct returns NotificationCompat Builder when collapsed text is absent and collapses with body text`() { + val pushTemplate = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_NO_COLLAPSED + ) + ) + ) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct returns NotificationCompat Builder when no blob is present`() { + val pushTemplate = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct uses silent channel when template is from intent`() { + val bundle = Bundle().apply { + putString(PushPayloadKeys.TEMPLATE_TYPE, PushTemplateType.AJO_BIG_TEXT.value) + putString(PushPayloadKeys.VERSION, "1") + putString(PushPayloadKeys.TITLE, AJO_MOCKED_FLAT_TITLE) + putString(PushPayloadKeys.BODY, AJO_MOCKED_FLAT_BODY) + putString(PushPayloadKeys.AJO_TEMPLATE_PROPERTIES, AJO_MOCKED_BIGTEXT_PROPS_FULL) + } + val pushTemplate = AJOBigTextPushTemplate(IntentData(bundle, null)) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + @Config(sdk = [21]) + fun `construct returns builder on pre-Oreo device`() { + val pushTemplate = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct succeeds with custom sound`() { + val pushTemplate = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.SOUND to "bells", + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } + + @Test + fun `construct reuses existing notification channel`() { + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel( + NotificationChannel( + "AEPSDKPushChannel", + "Push Notifications", + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + + val pushTemplate = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + + val result = AJOBigTextNotificationBuilder.construct( + context, pushTemplate, trackerActivityClass, broadcastReceiverClass + ) + + assertNotNull(result) + assert(result is NotificationCompat.Builder) + } +} diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplateTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplateTest.kt new file mode 100644 index 00000000..c194fa46 --- /dev/null +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBigTextPushTemplateTest.kt @@ -0,0 +1,313 @@ +/* + Copyright 2024 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. +*/ + +package com.adobe.marketing.mobile.notificationbuilder.internal.templates + +import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants.PushPayloadKeys +import com.adobe.marketing.mobile.notificationbuilder.internal.PushTemplateType +import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class AJOBigTextPushTemplateTest { + + // ── Inherited AEPPushTemplate parsing ───────────────────────────────────── + + @Test + fun `parses title body and version from flat keys`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_TITLE, template.title) + assertEquals(AJO_MOCKED_FLAT_BODY, template.body) + assertEquals("1", template.payloadVersion) + } + + @Test(expected = IllegalArgumentException::class) + fun `throws when required adb_version is absent`() { + AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + } + + @Test + fun `reads flat notification properties`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.SOUND to "bells", + PushPayloadKeys.CHANNEL_ID to "ajo_bigtext_channel", + PushPayloadKeys.PRIORITY to "PRIORITY_HIGH", + PushPayloadKeys.VISIBILITY to "PUBLIC", + PushPayloadKeys.BADGE_COUNT to "3", + PushPayloadKeys.STICKY to "true", + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals("bells", template.sound) + assertEquals("ajo_bigtext_channel", template.channelId) + assertEquals(3, template.badgeCount) + assertEquals(true, template.isNotificationSticky) + } + + // ── AJOBigTextPushTemplate specific (blob) ──────────────────────────────── + + @Test + fun `expanded text is the flat adb_body`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_BODY, template.body) + } + + @Test + fun `parses largeIconUrl from blob`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals(AJO_MOCKED_LARGE_ICON_URL, template.largeIconUrl) + } + + @Test + fun `largeIconUrl is null when blob has no adb_large_icon key`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to "{\"adb_collapsed_text\":\"AJO Collapsed Body\"}" + ) + ) + ) + assertNull(template.largeIconUrl) + } + + @Test + fun `largeIconUrl is null when blob is absent`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + assertNull(template.largeIconUrl) + } + + @Test + fun `collapsedText parsed from adb_collapsed_text in blob`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals(AJO_MOCKED_COLLAPSED_TEXT, template.collapsedText) + } + + @Test + fun `collapsedText falls back to flat adb_body when adb_collapsed_text is absent`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_NO_COLLAPSED + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_BODY, template.collapsedText) + } + + @Test + fun `collapsedText falls back to flat adb_body when blob is absent`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_BODY, template.collapsedText) + } + + @Test + fun `collapsedText falls back to flat adb_body when blob is malformed`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to "not valid json {{{" + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_BODY, template.collapsedText) + } + + @Test + fun `collapsedText falls back to flat adb_body when adb_collapsed_text is empty string`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to "{\"adb_collapsed_text\":\"\"}" + ) + ) + ) + assertEquals(AJO_MOCKED_FLAT_BODY, template.collapsedText) + } + + @Test + fun `parses action buttons from flat adb_act key`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to MOCKED_ACTION_BUTTON_DATA, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals(2, template.actionButtonsList?.size) + assertEquals("Go to chess.com", template.actionButtonsList?.get(0)?.label) + assertEquals("Open the app", template.actionButtonsList?.get(1)?.label) + } + + @Test + fun `returns null action buttons when adb_act is absent`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertNull(template.actionButtonsList) + } + + @Test + fun `returns null action buttons when adb_act is invalid JSON`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to "not json", + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertNull(template.actionButtonsList) + } + + @Test + fun `stores raw actionButtonsString`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to MOCKED_ACTION_BUTTON_DATA, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals(MOCKED_ACTION_BUTTON_DATA, template.actionButtonsString) + } + + @Test + fun `action buttons list skips entries where getActionButtonFromJSONObject returns null`() { + val template = AJOBigTextPushTemplate( + MapData( + mutableMapOf( + PushPayloadKeys.TEMPLATE_TYPE to PushTemplateType.AJO_BIG_TEXT.value, + PushPayloadKeys.VERSION to "1", + PushPayloadKeys.TITLE to AJO_MOCKED_FLAT_TITLE, + PushPayloadKeys.BODY to AJO_MOCKED_FLAT_BODY, + PushPayloadKeys.ACTION_BUTTONS to MOCKED_MALFORMED_JSON_ACTION_BUTTON, + PushPayloadKeys.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_BIGTEXT_PROPS_FULL + ) + ) + ) + assertEquals(2, template.actionButtonsList?.size) + } +} diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt index 227e8974..886f1bbf 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/MockDataUtils.kt @@ -59,6 +59,7 @@ const val MOCKED_CHANNEL_ID = "AEPSDKPushChannel1" // AJO template test data. Title, body, image url and version come from flat top-level FCM keys; // only template-specific flat keys live inside the adb_template_properties blob. const val AJO_MOCKED_IMAGE_URL = "https://example.com/ajo_img.jpg" +const val AJO_MOCKED_LARGE_ICON_URL = "https://example.com/ajo_icon.png" const val AJO_MOCKED_FLAT_TITLE = "AJO Flat Title" const val AJO_MOCKED_FLAT_BODY = "AJO Flat Body" @@ -70,6 +71,17 @@ const val AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP = "{\"adb_image_scale_type\":\"center_crop\"}" const val AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE = "{}" + +// AJO bigtext template test data. The flat adb_body holds the expanded text; the blob carries the +// collapsed text and the large icon url. +const val AJO_MOCKED_COLLAPSED_TEXT = "AJO Collapsed Body" + +const val AJO_MOCKED_BIGTEXT_PROPS_FULL = + "{\"adb_collapsed_text\":\"AJO Collapsed Body\"," + + "\"adb_large_icon\":\"https://example.com/ajo_icon.png\"}" + +const val AJO_MOCKED_BIGTEXT_PROPS_NO_COLLAPSED = + "{\"adb_large_icon\":\"https://example.com/ajo_icon.png\"}" const val MOCKED_RECEIVER_NAME = "receiverName" const val MOCKED_HINT = "hint" const val MOCKED_FEEDBACK_TEXT = "feedbackText"