From d370a70515f2a604dd12bb95449fbe0037459d0b Mon Sep 17 00:00:00 2001 From: Ryan Morales Date: Thu, 1 Aug 2024 16:14:46 -0700 Subject: [PATCH 1/3] fixes for MOB-21261, MOB-21262, and MOB-21447 MOB-21447: replaces the heads up notification (HUD) on API 21 with the expanded push template as the heads up notification is not expandable. additionally, the heads up notification is replaced by the collapsed layout on API22 and 23 as the HUD notification is expandable but does not show any title or body text. MOB-21261: use the `setOnlyAlertOnce` on the NotificationCompat.Builder to prevent an interaction intent from creating a new notification each time a push template interaction occurs. MOB-21262: use a expanded layout with a smaller image area to allow the notification action buttons to be displayed on the bottom of each notification. --- .../builders/AEPPushNotificationBuilder.kt | 13 ++++++ .../builders/BasicNotificationBuilder.kt | 10 ++++- .../builders/InputBoxNotificationBuilder.kt | 10 ++++- .../layout/push_template_expanded_api23.xml | 43 +++++++++++++++++++ .../src/main/res/values/dimens.xml | 1 + code/testapp/src/main/assets/basic/basic.json | 2 +- .../src/main/assets/basic/basic_colors.json | 2 +- 7 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 code/notificationbuilder/src/main/res/layout/push_template_expanded_api23.xml diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt index 31b078a9..13f2979c 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt @@ -106,6 +106,19 @@ internal object AEPPushNotificationBuilder { ) .setNotificationDeleteAction(context, trackerActivityClass) + // API21 specific fixes + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) { + // intent handling fix, see MOB-21261 for more info + builder.setOnlyAlertOnce(true) + // heads up display fix, see MOB-21447 for more info + builder.setCustomHeadsUpContentView(expandedLayout) + } + + // API23 and 22 heads up display fix, see MOB-21447 for more info + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { + builder.setCustomHeadsUpContentView(smallLayout) + } + // if not from intent, set custom sound, note this applies to API 25 and lower only as // API 26 and up set the sound on the notification channel if (!pushTemplate.isFromIntent) { diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilder.kt index a9b00bae..da11b702 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilder.kt @@ -16,6 +16,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context +import android.os.Build import android.widget.RemoteViews import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationCompat @@ -47,7 +48,14 @@ internal object BasicNotificationBuilder { Log.trace(LOG_TAG, SELF_TAG, "Building a basic template push notification.") val packageName = context.packageName val smallLayout = RemoteViews(packageName, R.layout.push_template_collapsed) - val expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded) + var expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded) + + // API23 and below have a limited notification display area. the notification elements + // must use a smaller area to fix buttons not showing on expanded notification. + // see MOB-21262 for more info + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded_api23) + } val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt index 97a4d595..dfae3c76 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt @@ -17,6 +17,7 @@ import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.os.Build import android.widget.RemoteViews import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput @@ -47,7 +48,14 @@ internal object InputBoxNotificationBuilder { Log.trace(LOG_TAG, SELF_TAG, "Building an input box template push notification.") val packageName = context.packageName val smallLayout = RemoteViews(packageName, R.layout.push_template_collapsed) - val expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded) + var expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded) + + // API23 and below have a limited notification display area. the notification elements + // must use a smaller area to fix buttons not showing on expanded notification. + // see MOB-21262 for more info + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded_api23) + } val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/code/notificationbuilder/src/main/res/layout/push_template_expanded_api23.xml b/code/notificationbuilder/src/main/res/layout/push_template_expanded_api23.xml new file mode 100644 index 00000000..51d8f311 --- /dev/null +++ b/code/notificationbuilder/src/main/res/layout/push_template_expanded_api23.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/code/notificationbuilder/src/main/res/values/dimens.xml b/code/notificationbuilder/src/main/res/values/dimens.xml index 4d2c85cf..dbfc23a2 100644 --- a/code/notificationbuilder/src/main/res/values/dimens.xml +++ b/code/notificationbuilder/src/main/res/values/dimens.xml @@ -15,6 +15,7 @@ 35dp 35dp + 160dp 200dp 200dp 250dp diff --git a/code/testapp/src/main/assets/basic/basic.json b/code/testapp/src/main/assets/basic/basic.json index 73ca86c8..b4a9e6e1 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://i.ibb.co/QN078XB/Resize-image-project.jpg", + "adb_image": "https://images.pexels.com/photos/260024/pexels-photo-260024.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", "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", diff --git a/code/testapp/src/main/assets/basic/basic_colors.json b/code/testapp/src/main/assets/basic/basic_colors.json index d8102ebf..752b4810 100644 --- a/code/testapp/src/main/assets/basic/basic_colors.json +++ b/code/testapp/src/main/assets/basic/basic_colors.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://i.ibb.co/QN078XB/Resize-image-project.jpg", + "adb_image": "https://images.pexels.com/photos/260024/pexels-photo-260024.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", "adb_act": "[{\"label\":\"Go to chess.com\",\"uri\":\"https:\/\/chess.com\/games\/552\",\"type\":\"DEEPLINK\"},{\"label\":\"Open the app\",\"uri\":\"\",\"type\":\"OPENAPP\"}]", "adb_small_icon": "chat_bubble", "adb_large_icon": "https://cdn-icons-png.flaticon.com/128/864/864639.png", From 8937b2295d2fe6692331581f43e5ff44d8f5239f Mon Sep 17 00:00:00 2001 From: Ryan Morales Date: Fri, 2 Aug 2024 15:21:21 -0700 Subject: [PATCH 2/3] add input box input retrieval to test app --- .../notificationBuilder/NotificationTrackerActivity.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/NotificationTrackerActivity.kt b/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/NotificationTrackerActivity.kt index 9538521d..b8244d0b 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/NotificationTrackerActivity.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/mobile/notificationbuilder/testapp/notificationBuilder/NotificationTrackerActivity.kt @@ -17,13 +17,21 @@ import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput import com.adobe.marketing.mobile.notificationbuilder.PushTemplateConstants +import com.adobe.marketing.mobile.services.Log import com.adobe.marketing.mobile.services.ServiceProvider class NotificationTrackerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - when (intent?.action) { + val intent = intent + when (intent.action) { + "Input Received" -> { + val results = RemoteInput.getResultsFromIntent(intent) + val quickReplyResult = results?.getCharSequence("developer intent action name") + Log.debug("MyApp", "NotificationTrackerActivity", "input box quick reply result: $quickReplyResult") + } PushTemplateConstants.NotificationAction.CLICKED -> executePushAction(intent) else -> {} } From de8a0f77b76c23965d2b71be6a9786c27c302b35 Mon Sep 17 00:00:00 2001 From: Ryan Morales Date: Mon, 5 Aug 2024 10:26:14 -0700 Subject: [PATCH 3/3] fix input box notification api gating, add unit tests for API21-23 fixes --- .../builders/AEPPushNotificationBuilder.kt | 4 +- .../builders/InputBoxNotificationBuilder.kt | 14 ++- .../AEPPushNotificationBuilderTest.kt | 94 +++++++++++++++++++ .../builders/BasicNotificationBuilderTest.kt | 14 ++- .../InputBoxNotificationBuilderTest.kt | 17 +++- 5 files changed, 131 insertions(+), 12 deletions(-) diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt index 13f2979c..174229b7 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilder.kt @@ -114,8 +114,8 @@ internal object AEPPushNotificationBuilder { builder.setCustomHeadsUpContentView(expandedLayout) } - // API23 and 22 heads up display fix, see MOB-21447 for more info - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { + // API22 and 23 heads up display fix, see MOB-21447 for more info + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M || Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) { builder.setCustomHeadsUpContentView(smallLayout) } diff --git a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt index dfae3c76..02f2fa96 100644 --- a/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt +++ b/code/notificationbuilder/src/main/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilder.kt @@ -45,17 +45,15 @@ internal object InputBoxNotificationBuilder { trackerActivityClass: Class?, broadcastReceiverClass: Class? ): NotificationCompat.Builder { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + throw NotificationConstructionFailedException("Input box push notification on devices below Android N is not supported.") + } + Log.trace(LOG_TAG, SELF_TAG, "Building an input box template push notification.") val packageName = context.packageName val smallLayout = RemoteViews(packageName, R.layout.push_template_collapsed) - var expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded) - - // API23 and below have a limited notification display area. the notification elements - // must use a smaller area to fix buttons not showing on expanded notification. - // see MOB-21262 for more info - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { - expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded_api23) - } + val expandedLayout = RemoteViews(packageName, R.layout.push_template_expanded) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilderTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilderTest.kt index 4680d60b..e38fd5f2 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilderTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/AEPPushNotificationBuilderTest.kt @@ -33,10 +33,13 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.util.IntentData import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData import io.mockk.unmockkAll import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue import org.junit.After import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertNotEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -126,6 +129,97 @@ class AEPPushNotificationBuilderTest { verifyNotificationViewDataAndColors(pushTemplate) } + @Config(sdk = [21]) + @Test + fun `construct should set alert only once for API level below 22`() { + val pushTemplate = BasicPushTemplate(MapData(dataMap)) + val notification = + AEPPushNotificationBuilder.construct( + context, + pushTemplate, + CHANNEL_ID_TO_USE, + trackerActivityClass, + smallLayout, + expandedLayout, + CONTAINER_LAYOUT_VIEW_ID + ).build() + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { + assertTrue(NotificationCompat.getOnlyAlertOnce(notification)) + } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { + assertFalse(NotificationCompat.getOnlyAlertOnce(notification)) + } + } + + @Config(sdk = [21]) + @Test + fun `construct should set the expanded layout as the custom heads up content view for API level below 22`() { + val pushTemplate = BasicPushTemplate(MapData(dataMap)) + val notificationBuilder = + AEPPushNotificationBuilder.construct( + context, + pushTemplate, + CHANNEL_ID_TO_USE, + trackerActivityClass, + smallLayout, + expandedLayout, + CONTAINER_LAYOUT_VIEW_ID + ) + assertEquals(expandedLayout, notificationBuilder.headsUpContentView) + } + + @Config(sdk = [22]) + @Test + fun `construct should set the small layout as the custom heads up content view for API 22`() { + val pushTemplate = BasicPushTemplate(MapData(dataMap)) + val notificationBuilder = + AEPPushNotificationBuilder.construct( + context, + pushTemplate, + CHANNEL_ID_TO_USE, + trackerActivityClass, + smallLayout, + expandedLayout, + CONTAINER_LAYOUT_VIEW_ID + ) + assertEquals(smallLayout, notificationBuilder.headsUpContentView) + } + + @Config(sdk = [23]) + @Test + fun `construct should set the small layout as the custom heads up content view for API 23`() { + val pushTemplate = BasicPushTemplate(MapData(dataMap)) + val notificationBuilder = + AEPPushNotificationBuilder.construct( + context, + pushTemplate, + CHANNEL_ID_TO_USE, + trackerActivityClass, + smallLayout, + expandedLayout, + CONTAINER_LAYOUT_VIEW_ID + ) + assertEquals(smallLayout, notificationBuilder.headsUpContentView) + } + + @Config(sdk = [24]) + @Test + fun `construct should not set a custom heads up content view for API 24 or higher`() { + val pushTemplate = BasicPushTemplate(MapData(dataMap)) + val notificationBuilder = + AEPPushNotificationBuilder.construct( + context, + pushTemplate, + CHANNEL_ID_TO_USE, + trackerActivityClass, + smallLayout, + expandedLayout, + CONTAINER_LAYOUT_VIEW_ID + ) + assertNotEquals(smallLayout, notificationBuilder.headsUpContentView) + assertNotEquals(expandedLayout, notificationBuilder.headsUpContentView) + } + @Test fun `construct should not set notification sound if pushTemplate sound is invalid`() { dataMap.replaceValueInMap( diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilderTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilderTest.kt index 8d005678..e2a3414a 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilderTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/BasicNotificationBuilderTest.kt @@ -33,7 +33,6 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.templates.MockAEP import com.adobe.marketing.mobile.notificationbuilder.internal.templates.provideMockedBasicPushTemplateWithAllKeys import com.adobe.marketing.mobile.notificationbuilder.internal.templates.provideMockedBasicPushTemplateWithRequiredData import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData -import com.google.common.base.Verify.verify import io.mockk.every import io.mockk.mockkConstructor import io.mockk.mockkObject @@ -102,6 +101,19 @@ class BasicNotificationBuilderTest { verify(exactly = 1) { any().setRemoteViewImage(MOCKED_IMAGE_URI, R.id.expanded_template_image) } } + @Config(sdk = [23]) + @Test + fun `construct should use the api23 expanded layout for API level below 24`() { + val pushTemplate = provideMockedBasicPushTemplateWithAllKeys() + val notificationBuilder = BasicNotificationBuilder.construct( + context, + pushTemplate, + trackerActivityClass, + broadcastReceiverClass + ) + assertEquals(R.layout.push_template_expanded_api23, notificationBuilder.bigContentView.layoutId) + } + @Test fun `construct should set parameters for notification builder properly`() { val pushTemplate = provideMockedBasicPushTemplateWithAllKeys() diff --git a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilderTest.kt b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilderTest.kt index d362bc8b..5839a983 100644 --- a/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilderTest.kt +++ b/code/notificationbuilder/src/test/java/com/adobe/marketing/mobile/notificationbuilder/internal/builders/InputBoxNotificationBuilderTest.kt @@ -14,8 +14,10 @@ package com.adobe.marketing.mobile.notificationbuilder.internal.builders import android.app.Activity import android.content.BroadcastReceiver import android.content.Context +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.R import com.adobe.marketing.mobile.notificationbuilder.internal.extensions.setRemoteImage @@ -35,7 +37,6 @@ import com.adobe.marketing.mobile.notificationbuilder.internal.templates.provide import com.adobe.marketing.mobile.notificationbuilder.internal.templates.removeKeysFromMap import com.adobe.marketing.mobile.notificationbuilder.internal.templates.replaceValueInMap import com.adobe.marketing.mobile.notificationbuilder.internal.util.MapData -import com.google.common.base.Verify.verify import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -52,6 +53,7 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.Shadows import org.robolectric.annotation.Config +import org.robolectric.util.ReflectionHelpers import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -93,6 +95,19 @@ class InputBoxNotificationBuilderTest { assertEquals(NotificationCompat.Builder::class.java, notificationBuilder.javaClass) } + @Test(expected = NotificationConstructionFailedException::class) + fun `construct throws exception when API is less than 24`() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 23) + val pushTemplate = provideMockedInputBoxPushTemplateWithRequiredData(true) + val result = InputBoxNotificationBuilder.construct( + context, + pushTemplate, + trackerActivityClass, + broadcastReceiverClass + ) + assertNull(result) + } + @Test fun `construct should not have any inputText action if the template is created from intent`() { val pushTemplate = provideMockedInputBoxPushTemplateWithRequiredData(true)