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/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/NotificationBuilder.kt index dd574ccd..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 @@ -21,6 +21,7 @@ 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.builders.AutoCarouselNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.BasicNotificationBuilder import com.adobe.marketing.mobile.notificationbuilder.internal.builders.InputBoxNotificationBuilder @@ -32,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 @@ -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..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 @@ -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,16 @@ 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 { + // 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" + 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..79d4424d --- /dev/null +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilder.kt @@ -0,0 +1,197 @@ +/* + 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.AJOTemplatePropertyKeys +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.AJOBasicPushTemplate +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) + val expandedLayout = RemoteViews(packageName, R.layout.ajo_basic_push_template_expanded) + + 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) + + // 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) = + 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) + .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/templates/AJOBasicPushTemplate.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplate.kt new file mode 100644 index 00000000..ef6e7f6a --- /dev/null +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplate.kt @@ -0,0 +1,103 @@ +/* + 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 + +/** + * 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. + * + * 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) : AEPPushTemplate(data) { + + private val SELF_TAG = "AJOBasicPushTemplate" + + // Scale type for the main expanded image. Defaults to CENTER_CROP. + internal val imgScaleType: 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) + ) + + imgScaleType = props?.optString(AJOTemplatePropertyKeys.IMAGE_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 + } +} diff --git a/code/notificationbuilder/src/main/res/layout/ajo_basic_push_template_expanded.xml b/code/notificationbuilder/src/main/res/layout/ajo_basic_push_template_expanded.xml new file mode 100644 index 00000000..de333102 --- /dev/null +++ b/code/notificationbuilder/src/main/res/layout/ajo_basic_push_template_expanded.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + 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..258908c8 --- /dev/null +++ b/code/notificationbuilder/src/main/res/layout/ajo_push_template_collapsed.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + 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..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 @@ -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,19 @@ 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.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 + ) + 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..893ae285 --- /dev/null +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/ajo/builders/AJOBasicNotificationBuilderTest.kt @@ -0,0 +1,202 @@ +/* + 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.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.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 +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 +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.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 + ) + ) + ) + + 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.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 + ) + ) + ) + + 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.VERSION to "1", + 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) + } + + @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.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) + } + 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.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 + ) + ) + ) + + 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.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_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/templates/AJOBasicPushTemplateTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplateTest.kt new file mode 100644 index 00000000..e609cc22 --- /dev/null +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/templates/AJOBasicPushTemplateTest.kt @@ -0,0 +1,331 @@ +/* + 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.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.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 +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class AJOBasicPushTemplateTest { + + // ── Inherited AEPPushTemplate parsing ───────────────────────────────────── + + @Test + 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_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`() { + 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 + ) + ) + ) + } + + @Test + 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) + } + + @Test + 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.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertNull(template.imageUrl) + } + + @Test + 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", + 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) + assertEquals(NotificationPriority.PRIORITY_HIGH, template.priority) + } + + @Test + 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 + ) + ) + ) + assertEquals(PushTemplateType.AJO_BASIC, template.templateType) + } + + @Test + 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.LEGACY_SMALL_ICON to "legacy_icon" + ) + ) + ) + assertEquals("legacy_icon", template.smallIcon) + } + + @Test + 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.ACTION_TYPE to ActionType.DEEPLINK.name + ) + ) + ) + assertEquals(ActionType.DEEPLINK, template.actionType) + } + + // ── AJOBasicPushTemplate specific (blob) ────────────────────────────────── + + @Test + 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.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_FIT_CENTER + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.FIT_CENTER, template.imgScaleType) + } + + @Test + 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_CENTER_CROP + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) + } + + @Test + 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.AJO_TEMPLATE_PROPERTIES to AJO_MOCKED_TEMPLATE_PROPS_NO_SCALE + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) + } + + @Test + 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 + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) + } + + @Test + 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.AJO_TEMPLATE_PROPERTIES to "not valid json {{{" + ) + ) + ) + assertEquals(AJOTemplatePropertyKeys.ScaleType.CENTER_CROP, template.imgScaleType) + } + + @Test + 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.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 `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 + ) + ) + ) + assertNull(template.actionButtonsList) + } + + @Test + 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.ACTION_BUTTONS to "not json" + ) + ) + ) + assertNull(template.actionButtonsList) + } + + @Test + 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 + ) + ) + ) + assertEquals(MOCKED_ACTION_BUTTON_DATA, template.actionButtonsString) + } + + @Test + 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 + ) + ) + ) + assertNotNull(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..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 @@ -55,6 +55,21 @@ 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. 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_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_image_scale_type\":\"fit_center\"}" + +const val AJO_MOCKED_TEMPLATE_PROPS_CENTER_CROP = + "{\"adb_image_scale_type\":\"center_crop\"}" + +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 new file mode 100644 index 00000000..36617b77 --- /dev/null +++ b/code/testapp/src/main/assets/ajo_basic/ajo_basic_center_crop.json @@ -0,0 +1,17 @@ +{ + "adb_template_type": "ajo_basic", + "adb_version": "1", + "adb_title": "AJO Basic - 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", + "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_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 new file mode 100644 index 00000000..ce3baaab --- /dev/null +++ b/code/testapp/src/main/assets/ajo_basic/ajo_basic_fit_center.json @@ -0,0 +1,17 @@ +{ + "adb_template_type": "ajo_basic", + "adb_version": "1", + "adb_title": "AJO Basic - 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", + "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_image_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)