From 511ba84de185c331d9e4168abc2a3decba7df572 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Wed, 3 Apr 2024 12:10:18 -0700 Subject: [PATCH 01/30] Fix IAM scrolling --- .../ui/message/views/MessageContent.kt | 35 +-------------- .../services/ui/message/views/MessageFrame.kt | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt index cbefffd22..5682f9a41 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt @@ -13,14 +13,10 @@ package com.adobe.marketing.mobile.services.ui.message.views import android.view.ViewGroup import android.webkit.WebView -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -55,11 +51,6 @@ internal fun MessageContent( val widthDp: Dp = remember { ((currentConfiguration.screenWidthDp * inAppMessageSettings.width) / 100).dp } - // Swipe/Drag variables - val offsetX = remember { mutableStateOf(0f) } - val offsetY = remember { mutableStateOf(0f) } - val dragVelocity = remember { mutableStateOf(0f) } - AndroidView( factory = { WebView(it).apply { @@ -93,30 +84,6 @@ internal fun MessageContent( .height(heightDp) .width(widthDp) .clip(RoundedCornerShape(inAppMessageSettings.cornerRadius.dp)) - .draggable( - state = rememberDraggableState { delta -> - offsetX.value += delta - }, - orientation = Orientation.Horizontal, - onDragStopped = { velocity -> - gestureTracker.onDragFinished(offsetX.value, offsetY.value, velocity) - dragVelocity.value = 0f - offsetY.value = 0f - offsetX.value = 0f - } - ) - .draggable( - state = rememberDraggableState { delta -> - offsetY.value += delta - }, - orientation = Orientation.Vertical, - onDragStopped = { velocity -> - gestureTracker.onDragFinished(offsetX.value, offsetY.value, velocity) - dragVelocity.value = 0f - offsetY.value = 0f - offsetX.value = 0f - } - ).testTag(MessageTestTags.MESSAGE_CONTENT) - + .testTag(MessageTestTags.MESSAGE_CONTENT) ) } diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt index cc5a0062a..bf852aa87 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt @@ -15,6 +15,9 @@ import android.webkit.WebView import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset @@ -22,6 +25,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -71,6 +75,11 @@ internal fun MessageFrame( ) } + val allowGestures = remember { inAppMessageSettings.gestureMap.isNotEmpty() } + val offsetX = remember { mutableStateOf(0f) } + val offsetY = remember { mutableStateOf(0f) } + val dragVelocity = remember { mutableStateOf(0f) } + AnimatedVisibility( visibleState = visibility, enter = MessageAnimationMapper.getEnterTransitionFor(inAppMessageSettings.displayAnimation), @@ -83,6 +92,40 @@ internal fun MessageFrame( .fillMaxSize() .offset(x = horizontalOffset, y = verticalOffset) .background(Color.Transparent) + .draggable( + enabled = allowGestures, + state = rememberDraggableState { delta -> + offsetX.value += delta + }, + orientation = Orientation.Horizontal, + onDragStopped = { velocity -> + gestureTracker.onDragFinished( + offsetX.value, + offsetY.value, + velocity + ) + dragVelocity.value = 0f + offsetY.value = 0f + offsetX.value = 0f + } + ) + .draggable( + enabled = allowGestures, + state = rememberDraggableState { delta -> + offsetY.value += delta + }, + orientation = Orientation.Vertical, + onDragStopped = { velocity -> + gestureTracker.onDragFinished( + offsetX.value, + offsetY.value, + velocity + ) + dragVelocity.value = 0f + offsetY.value = 0f + offsetX.value = 0f + } + ) .testTag(MessageTestTags.MESSAGE_FRAME), horizontalArrangement = MessageArrangementMapper.getHorizontalArrangement( inAppMessageSettings.horizontalAlignment From 7c054096ad182a9bf3aa9504dd39190da17728d5 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Wed, 3 Apr 2024 13:12:04 -0700 Subject: [PATCH 02/30] Remove unused parameters --- .../mobile/services/ui/message/views/MessageContent.kt | 3 +-- .../marketing/mobile/services/ui/message/views/MessageFrame.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt index 5682f9a41..70a8c9ab9 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt @@ -41,8 +41,7 @@ import java.nio.charset.StandardCharsets @Composable internal fun MessageContent( inAppMessageSettings: InAppMessageSettings, - onCreated: (WebView) -> Unit, - gestureTracker: GestureTracker + onCreated: (WebView) -> Unit ) { // Size variables val currentConfiguration = LocalConfiguration.current diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt index bf852aa87..ec433b287 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt @@ -136,7 +136,7 @@ internal fun MessageFrame( // the WebView message is clipped to the rounded corners for API versions 22 and below. This does not // affect the appearance of the message on API versions 23 and above. Card(modifier = Modifier.clip(RoundedCornerShape(inAppMessageSettings.cornerRadius.dp)).alpha(0.99f)) { - MessageContent(inAppMessageSettings, onCreated, gestureTracker) + MessageContent(inAppMessageSettings, onCreated) } // This is a one-time effect that will be called when this composable is completely removed from the composition From 843cb75e0b16cde3071b846119fe2dda85fce6a4 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Thu, 11 Apr 2024 12:29:13 -0700 Subject: [PATCH 03/30] Move gesture detection to the CardView - Also replace deprecated test method. --- .../ui/message/views/MessageScreenTests.kt | 11 +-- .../services/ui/message/views/MessageFrame.kt | 74 ++++++++++--------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt index 8d49025fa..d92f1ddeb 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.test.junit4.StateRestorationTester import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performGesture +import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpRect @@ -236,7 +236,7 @@ class MessageScreenTests { composeTestRule.waitForIdle() validateMessageAppeared(true) // Swipe gestures - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performGesture { + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performTouchInput { // create a swipe right gesture swipeWithVelocity( start = Offset(100f, 10f), @@ -274,7 +274,7 @@ class MessageScreenTests { validateMessageAppeared(false) // Swipe gestures - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performGesture { + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performTouchInput { // create a swipe right gesture swipeWithVelocity( start = Offset(100f, 10f), @@ -312,7 +312,7 @@ class MessageScreenTests { validateMessageAppeared(true) // Swipe gestures - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performGesture { + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).assertIsDisplayed().performTouchInput { // create a swipe down gesture swipeWithVelocity( start = Offset(0f, 10f), @@ -320,9 +320,10 @@ class MessageScreenTests { endVelocity = 1000f ) } + composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).performGesture { + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertIsDisplayed().performTouchInput { click( Offset(100f, 10f) ) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt index ec433b287..cc54e4e6b 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt @@ -92,40 +92,6 @@ internal fun MessageFrame( .fillMaxSize() .offset(x = horizontalOffset, y = verticalOffset) .background(Color.Transparent) - .draggable( - enabled = allowGestures, - state = rememberDraggableState { delta -> - offsetX.value += delta - }, - orientation = Orientation.Horizontal, - onDragStopped = { velocity -> - gestureTracker.onDragFinished( - offsetX.value, - offsetY.value, - velocity - ) - dragVelocity.value = 0f - offsetY.value = 0f - offsetX.value = 0f - } - ) - .draggable( - enabled = allowGestures, - state = rememberDraggableState { delta -> - offsetY.value += delta - }, - orientation = Orientation.Vertical, - onDragStopped = { velocity -> - gestureTracker.onDragFinished( - offsetX.value, - offsetY.value, - velocity - ) - dragVelocity.value = 0f - offsetY.value = 0f - offsetX.value = 0f - } - ) .testTag(MessageTestTags.MESSAGE_FRAME), horizontalArrangement = MessageArrangementMapper.getHorizontalArrangement( inAppMessageSettings.horizontalAlignment @@ -135,7 +101,45 @@ internal fun MessageFrame( // The content of the InAppMessage. This needs to be placed inside a Card with .99 alpha to ensure that // the WebView message is clipped to the rounded corners for API versions 22 and below. This does not // affect the appearance of the message on API versions 23 and above. - Card(modifier = Modifier.clip(RoundedCornerShape(inAppMessageSettings.cornerRadius.dp)).alpha(0.99f)) { + Card( + modifier = Modifier + .clip(RoundedCornerShape(inAppMessageSettings.cornerRadius.dp)) + .alpha(0.99f) + .draggable( + enabled = allowGestures, + state = rememberDraggableState { delta -> + offsetX.value += delta + }, + orientation = Orientation.Horizontal, + onDragStopped = { velocity -> + gestureTracker.onDragFinished( + offsetX.value, + offsetY.value, + velocity + ) + dragVelocity.value = 0f + offsetY.value = 0f + offsetX.value = 0f + } + ) + .draggable( + enabled = allowGestures, + state = rememberDraggableState { delta -> + offsetY.value += delta + }, + orientation = Orientation.Vertical, + onDragStopped = { velocity -> + gestureTracker.onDragFinished( + offsetX.value, + offsetY.value, + velocity + ) + dragVelocity.value = 0f + offsetY.value = 0f + offsetX.value = 0f + } + ) + ) { MessageContent(inAppMessageSettings, onCreated) } From ce3fdc561287541ed2f7977250e22832ac93d7d8 Mon Sep 17 00:00:00 2001 From: praveek Date: Fri, 12 Apr 2024 14:31:55 -0700 Subject: [PATCH 04/30] Fix event history logs --- .../internal/eventhub/history/AndroidEventHistory.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt index 01f84440d..769bf9bc4 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt @@ -89,14 +89,14 @@ internal class AndroidEventHistory : EventHistory { Log.debug( CoreConstants.LOG_TAG, LOG_TAG, - "EventHistoryRequest[%s] - (%d of %d) for hash(%s)" + + "EventHistoryRequest[%d] - (%d of %d) for hash(%d)" + " with enforceOrder(%s) returned %d events", eventHistoryRequests.hashCode(), index + 1, eventHistoryRequests.size, eventHash, if (enforceOrder) "true" else "false", - res + res?.count ?: -1 ) if (res == null) { @@ -149,7 +149,8 @@ internal class AndroidEventHistory : EventHistory { Log.debug( CoreConstants.LOG_TAG, LOG_TAG, - String.format("Exception executing event history result handler %s", ex) + "Exception executing event history result handler %s", + ex ) } } From 93c03d100bb344a4567fd82ab9fb8f7659afc240 Mon Sep 17 00:00:00 2001 From: GeorgCantor Date: Sun, 14 Apr 2024 18:01:51 +0300 Subject: [PATCH 05/30] Update MapExtensions.kt --- .../marketing/mobile/internal/util/MapExtensions.kt | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt index 8d3f7ebdd..308ec02c4 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt @@ -137,17 +137,7 @@ private fun serializeKeyValuePair(key: String?, value: String?): String? { * @return [String] containing the elements joined by delimiters */ private fun join(elements: Iterable<*>, delimiter: String?): String { - val sBuilder = java.lang.StringBuilder() - val iterator = elements.iterator() - - // TODO: consider breaking on null items, otherwise we end up with sample1,null,sample3 instead of sample1,sample3 - while (iterator.hasNext()) { - sBuilder.append(iterator.next()) - if (iterator.hasNext()) { - sBuilder.append(delimiter) - } - } - return sBuilder.toString() + return elements.filterNotNull().joinToString(delimiter ?: ",") } /** From fcfe48f0fc71f6e6911e0cdb573accb534baf0ac Mon Sep 17 00:00:00 2001 From: GeorgCantor Date: Sun, 14 Apr 2024 20:11:15 +0300 Subject: [PATCH 06/30] Update SetExtensions.kt --- .../adobe/marketing/mobile/internal/util/SetExtensions.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/SetExtensions.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/SetExtensions.kt index 7a78b720a..caa46e4b0 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/SetExtensions.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/SetExtensions.kt @@ -12,11 +12,5 @@ package com.adobe.marketing.mobile.internal.util internal fun Set<*>.isAllString(): Boolean { - if (this.isEmpty()) { - return false - } - this.forEach { - if (it !is String) return false - } - return true + return isNotEmpty() && all { it is String } } From fb54ac6adc40923a90d4720171e1e742a7f9d9a6 Mon Sep 17 00:00:00 2001 From: GeorgCantor Date: Sun, 14 Apr 2024 20:54:29 +0300 Subject: [PATCH 07/30] Update LaunchRuleTransformer.kt --- .../launch/rulesengine/LaunchRuleTransformer.kt | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/launch/rulesengine/LaunchRuleTransformer.kt b/code/core/src/main/java/com/adobe/marketing/mobile/launch/rulesengine/LaunchRuleTransformer.kt index b046cba5b..1ffc4366d 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/launch/rulesengine/LaunchRuleTransformer.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/launch/rulesengine/LaunchRuleTransformer.kt @@ -55,13 +55,7 @@ internal object LaunchRuleTransformer { private fun addTypeTransform(transformer: Transformer) { transformer.register(LaunchRulesEngineConstants.Transform.TRANSFORM_TO_INT) { value -> when (value) { - is String -> { - try { - value.toInt() - } catch (e: NumberFormatException) { - value - } - } + is String -> value.toIntOrNull() ?: value is Number -> value.toInt() is Boolean -> if (value) 1 else 0 else -> value @@ -72,13 +66,7 @@ internal object LaunchRuleTransformer { } transformer.register(LaunchRulesEngineConstants.Transform.TRANSFORM_TO_DOUBLE) { value -> when (value) { - is String -> { - try { - value.toDouble() - } catch (e: NumberFormatException) { - value - } - } + is String -> value.toDoubleOrNull() ?: value is Number -> value.toDouble() is Boolean -> if (value) 1.0 else 0.0 else -> value From beb0791c39ca4ed2682a8550071e6eda85f9959a Mon Sep 17 00:00:00 2001 From: praveek Date: Thu, 25 Apr 2024 14:34:43 -0700 Subject: [PATCH 08/30] Fix logs --- .../mobile/internal/eventhub/history/AndroidEventHistory.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt index 769bf9bc4..00e444a98 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/history/AndroidEventHistory.kt @@ -149,8 +149,7 @@ internal class AndroidEventHistory : EventHistory { Log.debug( CoreConstants.LOG_TAG, LOG_TAG, - "Exception executing event history result handler %s", - ex + "Exception executing event history result handler $ex" ) } } From 0299619b7081029b557fd2f7c28eab16bf11c486 Mon Sep 17 00:00:00 2001 From: GeorgCantor Date: Fri, 26 Apr 2024 13:54:00 +0300 Subject: [PATCH 09/30] Update MapExtensions.kt --- .../com/adobe/marketing/mobile/internal/util/MapExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt index 308ec02c4..2af23a1b4 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt @@ -137,7 +137,7 @@ private fun serializeKeyValuePair(key: String?, value: String?): String? { * @return [String] containing the elements joined by delimiters */ private fun join(elements: Iterable<*>, delimiter: String?): String { - return elements.filterNotNull().joinToString(delimiter ?: ",") + return elements.joinToString(delimiter ?: ",") } /** From c19a0d8d26708c0955111ae0323a95d8e4528338 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 6 May 2024 12:08:08 -0700 Subject: [PATCH 10/30] Notify PresentationDelegate on all webview uri's Currently, presentation delegate notifies only the url's opened by the UriService which is a regression from 2.x. Change the logic to make UiService handle both uri's and url's if the event listener is unable to handle them. Further, notify PresentationDelegate of all uri's opened as part of the interaction. --- .../ui/message/InAppMessagePresentable.kt | 43 +++++------ .../mobile/services/uri/UriService.kt | 2 +- .../ui/message/InAppMessagePresentableTest.kt | 75 +++++++++++++++++++ 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt index 4d4cb8a2a..393be60c5 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt @@ -15,6 +15,7 @@ import android.content.Context import android.view.ViewGroup import android.webkit.WebSettings import android.webkit.WebView +import androidx.annotation.VisibleForTesting import androidx.compose.ui.platform.ComposeView import com.adobe.marketing.mobile.services.ui.Alert import com.adobe.marketing.mobile.services.ui.InAppMessage @@ -108,9 +109,9 @@ internal class InAppMessagePresentable( // So always dismiss the message when a gesture is detected dismiss() - // If a gesture mapping exists, the notify the listener about the url associated with the gesture + // If a gesture mapping exists, the notify the listener about the uri associated with the gesture inAppMessage.settings.gestureMap[gesture]?.let { link -> - handleInAppUrl(link) + handleInAppUri(link) } } ) @@ -169,32 +170,28 @@ internal class InAppMessagePresentable( return InAppMessageWebViewClient( inAppMessage.settings, presentationUtilityProvider - ) { url -> handleInAppUrl(url) } + ) { url -> handleInAppUri(url) } } /** - * Handles the in-app url. Does so by first checking if the component that created this message - * is able to handle the url. - * @param url the url to handle + * Handles the in-app uri. Does so by first checking if the component that created this message + * is able to handle the uri. + * @param uri the uri to handle * @return true if the url was handled internally by the web-view client, false otherwise */ - private fun handleInAppUrl(url: String): Boolean { - // Check if the component that created this message is able to handle the url - val handledByListener = - inAppMessage.eventListener.onUrlLoading(this@InAppMessagePresentable, url) - - // Check if this URL can be opened by the URLOpening - val handled = handledByListener || if (InAppMessageWebViewClient.isValidUrl(url)) { - val uriOpened = presentationUtilityProvider.openUri(url) - if (uriOpened) { - presentationDelegate?.onContentLoaded( - this@InAppMessagePresentable, - PresentationListener.PresentationContent.UrlContent(url) - ) - } - true - } else { - false + @VisibleForTesting + internal fun handleInAppUri(uri: String): Boolean { + // First check if the component that created this message is able to handle the uri. + // Otherwise check if this URI can be opened by the utility provider via UriOpening service + val handled = inAppMessage.eventListener.onUrlLoading(this@InAppMessagePresentable, uri) || + presentationUtilityProvider.openUri(uri) + + // Notify the presentation delegate only if the url was handled + if (handled) { + presentationDelegate?.onContentLoaded( + this@InAppMessagePresentable, + PresentationListener.PresentationContent.UrlContent(uri) + ) } return handled diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/uri/UriService.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/uri/UriService.kt index a21ced5b3..bf1b074d0 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/uri/UriService.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/uri/UriService.kt @@ -47,7 +47,7 @@ internal class UriService : UriOpening { currentActivity.startActivity(intent) true } catch (e: Exception) { - Log.debug(ServiceConstants.LOG_TAG, LOG_TAG, "Failed to open URI: $uri") + Log.debug(ServiceConstants.LOG_TAG, LOG_TAG, "Failed to open URI: $uri. ${e.message}") false } } diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentableTest.kt b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentableTest.kt index e7b5b4e56..7b3642c0a 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentableTest.kt +++ b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentableTest.kt @@ -15,6 +15,7 @@ import com.adobe.marketing.mobile.services.ui.Alert import com.adobe.marketing.mobile.services.ui.FloatingButton import com.adobe.marketing.mobile.services.ui.InAppMessage import com.adobe.marketing.mobile.services.ui.PresentationDelegate +import com.adobe.marketing.mobile.services.ui.PresentationListener import com.adobe.marketing.mobile.services.ui.PresentationUtilityProvider import com.adobe.marketing.mobile.services.ui.common.AppLifecycleProvider import kotlinx.coroutines.CoroutineScope @@ -22,7 +23,14 @@ import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.times +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -40,6 +48,9 @@ class InAppMessagePresentableTest { @Mock private lateinit var mockAppLifecycleProvider: AppLifecycleProvider + @Mock + private lateinit var mockInAppMessageEventListener: InAppMessageEventListener + @Mock private lateinit var mockScope: CoroutineScope @@ -56,6 +67,8 @@ class InAppMessagePresentableTest { mockAppLifecycleProvider, mockScope ) + + `when`(mockInAppMessage.eventListener).thenReturn(mockInAppMessageEventListener) } @Test @@ -78,4 +91,66 @@ class InAppMessagePresentableTest { assertTrue(inAppMessagePresentable.hasConflicts(listOf(mock(Alert::class.java)))) assertFalse(inAppMessagePresentable.hasConflicts(listOf(mock(FloatingButton::class.java)))) } + + @Test + fun `Test #handleInAppUri invokes InAppMessageEventListener first`() { + val uri = "adbinapp://dismiss?interaction=customInteraction" + `when`(mockInAppMessageEventListener.onUrlLoading(inAppMessagePresentable, uri)).thenReturn( + true + ) + + assertTrue(inAppMessagePresentable.handleInAppUri(uri)) + + verify(mockInAppMessageEventListener, times(1)).onUrlLoading(inAppMessagePresentable, uri) + verify(mockPresentationUtilityProvider, times(0)).openUri(uri) + + val presentationContentCaptor = argumentCaptor() + verify(mockPresentationDelegate).onContentLoaded( + eq(inAppMessagePresentable), + presentationContentCaptor.capture() + ) + assertTrue(presentationContentCaptor.firstValue is PresentationListener.PresentationContent.UrlContent) + assertEquals( + (presentationContentCaptor.firstValue as PresentationListener.PresentationContent.UrlContent).url, + uri + ) + } + + @Test + fun `Test that #handleInAppUri attempts to open uri if InAppMessageEventListener does not handle it`() { + val uri = "adbinapp://dismiss?interaction=customInteraction" + `when`(mockInAppMessageEventListener.onUrlLoading(inAppMessagePresentable, uri)).thenReturn( + false + ) + `when`(mockPresentationUtilityProvider.openUri(uri)).thenReturn(true) + + assertTrue(inAppMessagePresentable.handleInAppUri(uri)) + verify(mockInAppMessageEventListener, times(1)).onUrlLoading(inAppMessagePresentable, uri) + verify(mockPresentationUtilityProvider, times(1)).openUri(uri) + + val presentationContentCaptor = argumentCaptor() + verify(mockPresentationDelegate).onContentLoaded( + eq(inAppMessagePresentable), + presentationContentCaptor.capture() + ) + assertTrue(presentationContentCaptor.firstValue is PresentationListener.PresentationContent.UrlContent) + assertEquals( + (presentationContentCaptor.firstValue as PresentationListener.PresentationContent.UrlContent).url, + uri + ) + } + + @Test + fun `Test that #handleInAppUri returns false the uri cannot be handled by uri opening or the listener `() { + val uri = "adbinapp://dismiss?interaction=customInteraction" + `when`(mockInAppMessageEventListener.onUrlLoading(inAppMessagePresentable, uri)).thenReturn( + false + ) + `when`(mockPresentationUtilityProvider.openUri(uri)).thenReturn(false) + + assertFalse(inAppMessagePresentable.handleInAppUri(uri)) + verify(mockInAppMessageEventListener, times(1)).onUrlLoading(inAppMessagePresentable, uri) + verify(mockPresentationUtilityProvider, times(1)).openUri(uri) + verify(mockPresentationDelegate, times(0)).onContentLoaded(any(), any()) + } } From a7764d8adf333a6bc8ee0c42bf8078b3c708d443 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 6 May 2024 09:47:51 -0700 Subject: [PATCH 11/30] Add a supervisorjob and exception hander to UIService Currently AEPUIService creates a coroutine scope for every presentable to avoid one workflow distuption cancelling another. Instead we can use a combination of SupervisorJob and a Coroutine execption handler to use the same coroutine scope for all operations on main thread by UIService, while ensuring that the disjoint interactions do not interfere with each other. --- .../mobile/services/ui/AEPUIService.kt | 29 +++++++++++++++++-- .../services/ui/alert/AlertPresentable.kt | 7 +++-- .../services/ui/common/AEPPresentable.kt | 8 ++--- .../FloatingButtonPresentable.kt | 7 +++-- .../ui/message/InAppMessagePresentable.kt | 3 +- .../services/ui/alert/AlertPresentableTest.kt | 6 +++- .../FloatingButtonPresentableTest.kt | 7 ++++- 7 files changed, 53 insertions(+), 14 deletions(-) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPUIService.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPUIService.kt index 8e72fb95c..f92f30cd6 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPUIService.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/AEPUIService.kt @@ -12,20 +12,41 @@ package com.adobe.marketing.mobile.services.ui import android.app.Application +import com.adobe.marketing.mobile.services.Log +import com.adobe.marketing.mobile.services.ServiceConstants import com.adobe.marketing.mobile.services.ui.alert.AlertPresentable import com.adobe.marketing.mobile.services.ui.common.AppLifecycleProvider import com.adobe.marketing.mobile.services.ui.floatingbutton.FloatingButtonPresentable import com.adobe.marketing.mobile.services.ui.floatingbutton.FloatingButtonViewModel import com.adobe.marketing.mobile.services.ui.message.InAppMessagePresentable +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob /** * UI Service implementation for AEP SDK */ internal class AEPUIService : UIService { + companion object { + private const val LOG_TAG = "AEPUIService" + } + private var presentationDelegate: PresentationDelegate? = null + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.error( + ServiceConstants.LOG_TAG, + LOG_TAG, + "An error occurred while processing the presentation: ${throwable.message}", + throwable + ) + } + + private val mainScope by lazy { + CoroutineScope(Dispatchers.Main + SupervisorJob() + exceptionHandler) + } + @Suppress("UNCHECKED_CAST") override fun > create( presentation: T, @@ -44,7 +65,7 @@ internal class AEPUIService : UIService { presentationDelegate, presentationUtilityProvider, AppLifecycleProvider.INSTANCE, - CoroutineScope(Dispatchers.Main) + mainScope ) as Presentable } @@ -53,7 +74,8 @@ internal class AEPUIService : UIService { presentation, presentationDelegate, presentationUtilityProvider, - AppLifecycleProvider.INSTANCE + AppLifecycleProvider.INSTANCE, + mainScope ) as Presentable } @@ -63,7 +85,8 @@ internal class AEPUIService : UIService { FloatingButtonViewModel(presentation.settings), presentationDelegate, presentationUtilityProvider, - AppLifecycleProvider.INSTANCE + AppLifecycleProvider.INSTANCE, + mainScope ) as Presentable } diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentable.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentable.kt index 981a72a0b..1ff171fce 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentable.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentable.kt @@ -21,6 +21,7 @@ import com.adobe.marketing.mobile.services.ui.PresentationUtilityProvider import com.adobe.marketing.mobile.services.ui.alert.views.AlertScreen import com.adobe.marketing.mobile.services.ui.common.AEPPresentable import com.adobe.marketing.mobile.services.ui.common.AppLifecycleProvider +import kotlinx.coroutines.CoroutineScope /** * Represents an Alert presentable. @@ -33,12 +34,14 @@ internal class AlertPresentable( val alert: Alert, presentationDelegate: PresentationDelegate?, presentationUtilityProvider: PresentationUtilityProvider, - appLifecycleProvider: AppLifecycleProvider + appLifecycleProvider: AppLifecycleProvider, + mainScope: CoroutineScope ) : AEPPresentable( alert, presentationUtilityProvider, presentationDelegate, - appLifecycleProvider + appLifecycleProvider, + mainScope ) { override fun getContent(activityContext: Context): ComposeView { return ComposeView(activityContext).apply { diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt index 42ed3002b..ec83e8abf 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt @@ -33,7 +33,6 @@ import com.adobe.marketing.mobile.services.ui.Presentation import com.adobe.marketing.mobile.services.ui.PresentationDelegate import com.adobe.marketing.mobile.services.ui.PresentationUtilityProvider import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.util.Random @@ -52,10 +51,10 @@ internal abstract class AEPPresentable> : private val presentation: Presentation private val presentationUtilityProvider: PresentationUtilityProvider private val presentationDelegate: PresentationDelegate? - private val mainScope: CoroutineScope private val appLifecycleProvider: AppLifecycleProvider private val presentationObserver: PresentationObserver private val activityCompatOwnerUtils: ActivityCompatOwnerUtils + protected val mainScope: CoroutineScope protected val presentationStateManager: PresentationStateManager @VisibleForTesting internal val contentIdentifier: Int = Random().nextInt() @@ -70,7 +69,8 @@ internal abstract class AEPPresentable> : presentation: Presentation, presentationUtilityProvider: PresentationUtilityProvider, presentationDelegate: PresentationDelegate?, - appLifecycleProvider: AppLifecycleProvider + appLifecycleProvider: AppLifecycleProvider, + mainScope: CoroutineScope ) : this( presentation, presentationUtilityProvider, @@ -78,7 +78,7 @@ internal abstract class AEPPresentable> : appLifecycleProvider, PresentationStateManager(), ActivityCompatOwnerUtils(), - CoroutineScope(Dispatchers.Main), + mainScope, PresentationObserver.INSTANCE ) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentable.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentable.kt index 02685a3d7..0826a0de8 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentable.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentable.kt @@ -21,6 +21,7 @@ import com.adobe.marketing.mobile.services.ui.PresentationUtilityProvider import com.adobe.marketing.mobile.services.ui.common.AEPPresentable import com.adobe.marketing.mobile.services.ui.common.AppLifecycleProvider import com.adobe.marketing.mobile.services.ui.floatingbutton.views.FloatingButtonScreen +import kotlinx.coroutines.CoroutineScope /** * Represents a presentable floating button presentation @@ -35,12 +36,14 @@ internal class FloatingButtonPresentable( private val floatingButtonViewModel: FloatingButtonViewModel, presentationDelegate: PresentationDelegate?, presentationUtilityProvider: PresentationUtilityProvider, - appLifecycleProvider: AppLifecycleProvider + appLifecycleProvider: AppLifecycleProvider, + mainScope: CoroutineScope ) : AEPPresentable( floatingButton, presentationUtilityProvider, presentationDelegate, - appLifecycleProvider + appLifecycleProvider, + mainScope ) { // event handler for the floating button private val floatingButtonEventHandler = object : FloatingButtonEventHandler { diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt index 4d4cb8a2a..2ece6ac20 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt @@ -45,7 +45,8 @@ internal class InAppMessagePresentable( inAppMessage, presentationUtilityProvider, presentationDelegate, - appLifecycleProvider + appLifecycleProvider, + mainScope ) { companion object { diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentableTest.kt b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentableTest.kt index 0b2d51a75..b6d93753a 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentableTest.kt +++ b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/alert/AlertPresentableTest.kt @@ -17,6 +17,7 @@ import com.adobe.marketing.mobile.services.ui.InAppMessage import com.adobe.marketing.mobile.services.ui.PresentationDelegate import com.adobe.marketing.mobile.services.ui.PresentationUtilityProvider import com.adobe.marketing.mobile.services.ui.common.AppLifecycleProvider +import kotlinx.coroutines.CoroutineScope import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -40,12 +41,15 @@ class AlertPresentableTest { @Mock private lateinit var mockAppLifecycleProvider: AppLifecycleProvider + @Mock + private lateinit var mockScope: CoroutineScope + private lateinit var alertPresentable: AlertPresentable @Before fun setUp() { MockitoAnnotations.openMocks(this) - alertPresentable = AlertPresentable(mockAlert, mockPresentationDelegate, mockPresentationUtilityProvider, mockAppLifecycleProvider) + alertPresentable = AlertPresentable(mockAlert, mockPresentationDelegate, mockPresentationUtilityProvider, mockAppLifecycleProvider, mockScope) } @Test diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTest.kt b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTest.kt index 298cf4dc3..c5813d5ad 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTest.kt +++ b/code/core/src/test/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTest.kt @@ -17,6 +17,7 @@ import com.adobe.marketing.mobile.services.ui.InAppMessage import com.adobe.marketing.mobile.services.ui.PresentationDelegate import com.adobe.marketing.mobile.services.ui.PresentationUtilityProvider import com.adobe.marketing.mobile.services.ui.common.AppLifecycleProvider +import kotlinx.coroutines.CoroutineScope import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -47,6 +48,9 @@ class FloatingButtonPresentableTest { @Mock private lateinit var mockFloatingButtonSettings: FloatingButtonSettings + @Mock + private lateinit var mockScope: CoroutineScope + private lateinit var floatingButtonPresentable: FloatingButtonPresentable @Before @@ -61,7 +65,8 @@ class FloatingButtonPresentableTest { mockFloatingButtonViewModel, mockPresentationDelegate, mockPresentationUtilityProvider, - mockAppLifecycleProvider + mockAppLifecycleProvider, + mockScope ) verify(mockFloatingButtonViewModel).onGraphicUpdate(mockFloatingButtonSettings.initialGraphic) From 330dd0726a8d1b7664429351298290296b8ea920 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 7 May 2024 13:25:52 -0700 Subject: [PATCH 12/30] mainScope need not be protected --- .../adobe/marketing/mobile/services/ui/common/AEPPresentable.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt index ec83e8abf..e7b7a4910 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentable.kt @@ -54,7 +54,7 @@ internal abstract class AEPPresentable> : private val appLifecycleProvider: AppLifecycleProvider private val presentationObserver: PresentationObserver private val activityCompatOwnerUtils: ActivityCompatOwnerUtils - protected val mainScope: CoroutineScope + private val mainScope: CoroutineScope protected val presentationStateManager: PresentationStateManager @VisibleForTesting internal val contentIdentifier: Int = Random().nextInt() From 7490508fcd402549d590253991fe42d13fb3b292 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Wed, 8 May 2024 13:43:56 -0700 Subject: [PATCH 13/30] Always recompute screen size related calculations Currently, screen shize related calculations are done once and remembered on recompositions. This works well when adapting to the new size when orientation of the device is changed. However, when the activity hosting the composable restricts configuration changes, the remembered value is not recomputed preventing the presentables from adapting to the new screen size. As a solution, do not remember the size related calculation. Always calculate them on composition. --- .../ui/floatingbutton/views/FloatingButton.kt | 4 +-- .../ui/message/views/MessageContent.kt | 12 ++++--- .../services/ui/message/views/MessageFrame.kt | 33 +++++++++---------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/views/FloatingButton.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/views/FloatingButton.kt index f23d6917f..fecb21432 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/views/FloatingButton.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/floatingbutton/views/FloatingButton.kt @@ -58,8 +58,8 @@ internal fun FloatingButton( onDragFinished: (Offset) -> Unit ) { // Floating button draggable area dimensions - val heightDp = with(LocalConfiguration.current) { remember { mutableStateOf(screenHeightDp.dp) } } - val widthDp = with(LocalConfiguration.current) { remember { mutableStateOf(screenWidthDp.dp) } } + val heightDp = with(LocalConfiguration.current) { mutableStateOf(screenHeightDp.dp) } + val widthDp = with(LocalConfiguration.current) { mutableStateOf(screenWidthDp.dp) } // Floating button dimensions val fbHeightDp: Dp = remember { settings.height.dp } diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt index cbefffd22..70ca2995f 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt @@ -48,12 +48,14 @@ internal fun MessageContent( onCreated: (WebView) -> Unit, gestureTracker: GestureTracker ) { - // Size variables + // Size variables. Ideally, these values can be remembered because generally a configuration + // change will refresh the size. However, in-case the configuration changes (e.g. device rotation) + // are restricted by the Activity hosting this composable, these values will not be recomputed. + // So, we are not remembering these values to ensure that the size is recalculated on every + // composition. val currentConfiguration = LocalConfiguration.current - val heightDp: Dp = - remember { ((currentConfiguration.screenHeightDp * inAppMessageSettings.height) / 100).dp } - val widthDp: Dp = - remember { ((currentConfiguration.screenWidthDp * inAppMessageSettings.width) / 100).dp } + val heightDp: Dp = ((currentConfiguration.screenHeightDp * inAppMessageSettings.height) / 100).dp + val widthDp: Dp = ((currentConfiguration.screenWidthDp * inAppMessageSettings.width) / 100).dp // Swipe/Drag variables val offsetX = remember { mutableStateOf(0f) } diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt index cc5a0062a..30973d841 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageFrame.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip @@ -54,22 +53,22 @@ internal fun MessageFrame( onDisposed: () -> Unit ) { val currentConfiguration = LocalConfiguration.current - val horizontalOffset = - remember { - MessageOffsetMapper.getHorizontalOffset( - inAppMessageSettings.horizontalAlignment, - inAppMessageSettings.horizontalInset, - currentConfiguration.screenWidthDp.dp - ) - } - val verticalOffset = - remember { - MessageOffsetMapper.getVerticalOffset( - inAppMessageSettings.verticalAlignment, - inAppMessageSettings.verticalInset, - currentConfiguration.screenHeightDp.dp - ) - } + + // Ideally, these values can be remembered because generally a configuration + // change will refresh the size. However, in-case the configuration changes (e.g. device rotation) + // are restricted by the Activity hosting this composable, these values will not be recomputed. + // So, we are not remembering these values to ensure that the size is recalculated on every + // composition. + val horizontalOffset = MessageOffsetMapper.getHorizontalOffset( + inAppMessageSettings.horizontalAlignment, + inAppMessageSettings.horizontalInset, + currentConfiguration.screenWidthDp.dp + ) + val verticalOffset = MessageOffsetMapper.getVerticalOffset( + inAppMessageSettings.verticalAlignment, + inAppMessageSettings.verticalInset, + currentConfiguration.screenHeightDp.dp + ) AnimatedVisibility( visibleState = visibility, From 542e40d04ad3d1478090e8e3fcec92a304fd10a8 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 13 May 2024 11:23:50 -0700 Subject: [PATCH 14/30] Add UiAutomator dependancy + restricted config activity --- code/core/build.gradle.kts | 1 + code/core/src/androidTest/AndroidManifest.xml | 4 ++++ .../services/ui/RestrictedConfigActivity.kt | 16 ++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/RestrictedConfigActivity.kt diff --git a/code/core/build.gradle.kts b/code/core/build.gradle.kts index debd9d0e6..ec27ce165 100644 --- a/code/core/build.gradle.kts +++ b/code/core/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(BuildConstants.Dependencies.ANDROIDX_LIFECYCLE_KTX) androidTestImplementation(BuildConstants.Dependencies.MOCKITO_CORE) + androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0") //TODO: Consider moving this to the aep-library plugin later androidTestImplementation("com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3") } diff --git a/code/core/src/androidTest/AndroidManifest.xml b/code/core/src/androidTest/AndroidManifest.xml index 7ba487287..5e439a872 100644 --- a/code/core/src/androidTest/AndroidManifest.xml +++ b/code/core/src/androidTest/AndroidManifest.xml @@ -5,6 +5,10 @@ + diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/RestrictedConfigActivity.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/RestrictedConfigActivity.kt new file mode 100644 index 000000000..778ca38b7 --- /dev/null +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/RestrictedConfigActivity.kt @@ -0,0 +1,16 @@ +/* + 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.services.ui + +import androidx.activity.ComponentActivity + +class RestrictedConfigActivity : ComponentActivity() From 55b40597be8945b92ce53a9cb653f6c68f2012ba Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 13 May 2024 16:07:14 -0700 Subject: [PATCH 15/30] Add screen rotation tests --- code/core/src/androidTest/AndroidManifest.xml | 2 +- .../views/MessageScreenOrientationTests.kt | 240 +++++++++++ .../message/views/MessageScreenTestHelper.kt | 70 +++ .../ui/message/views/MessageScreenTests.kt | 408 ++++++++++++------ 4 files changed, 593 insertions(+), 127 deletions(-) create mode 100644 code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt create mode 100644 code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTestHelper.kt diff --git a/code/core/src/androidTest/AndroidManifest.xml b/code/core/src/androidTest/AndroidManifest.xml index 5e439a872..90c476c68 100644 --- a/code/core/src/androidTest/AndroidManifest.xml +++ b/code/core/src/androidTest/AndroidManifest.xml @@ -7,7 +7,7 @@ diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt new file mode 100644 index 000000000..5ba9a1863 --- /dev/null +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt @@ -0,0 +1,240 @@ +package com.adobe.marketing.mobile.services.ui.message.views + +import android.view.View +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.test.getUnclippedBoundsInRoot +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.adobe.marketing.mobile.services.ui.RestrictedConfigActivity +import com.adobe.marketing.mobile.services.ui.common.PresentationStateManager +import com.adobe.marketing.mobile.services.ui.message.InAppMessageSettings +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * AEPPresentable handles the orientation changes of a presentable by detaching and reattaching + * the presentable from the activity by listening to Application.ActivityLifecycleCallbacks. + * However, the test cannot be isolated to listen to ApplicationLifecycle changes at the screen level. + * So, the tests in this class are done on the MessageScreen which is attached to an activity that + * restricts orientation/screen size changes. So the rotation will not destroy the activity but it + * is expected that the MessageScreen will be recomposed to fit the new screen dimensions. + * While this is not the ideal test, it is the best we can do to test the + * orientation changes of the message screen along with [MessageScreenTests.testMessageScreenIsRestoredOnConfigurationChange] + * which tests the configuration changes of the message screen. + */ +@RunWith(AndroidJUnit4::class) +class MessageScreenOrientationTests { + @get: Rule + val composeTestRule = createAndroidComposeRule() + + private var onCreatedCalled = false + private var onDisposedCalled = false + private var onBackPressed = false + private val detectedGestures = mutableListOf() + private val presentationStateManager = PresentationStateManager() + + private val HTML_TEXT_SAMPLE = "\n" + + "\n" + + "A Sample HTML Page\n" + + "\n" + + "\n" + + "\n" + + "

This is a sample HTML page

\n" + + "\n" + + "\n" + + "" + + + // ---------------------------------------------------------------------------------------------- + // Test cases for orientation changes + // ---------------------------------------------------------------------------------------------- + @Test + fun testMessageScreenIsRestoredOnOrientationChange() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val uiDevice = UiDevice.getInstance(instrumentation) + + val heightPercentage = 95 + val widthPercentage = 60 + val settings = InAppMessageSettings.Builder() + .height(heightPercentage) + .width(widthPercentage) + .content(HTML_TEXT_SAMPLE) + .build() + + val screenHeightDp = mutableStateOf(0.dp) + val screenWidthDp = mutableStateOf(0.dp) + val activityHeightDp = mutableStateOf(0.dp) + val activityWidthDp = mutableStateOf(0.dp) + composeTestRule.setContent { // setting our composable as content for test + // Get the screen dimensions + val currentConfiguration = LocalConfiguration.current + screenHeightDp.value = currentConfiguration.screenHeightDp.dp + screenWidthDp.value = currentConfiguration.screenWidthDp.dp + + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + activityHeightDp.value = with(LocalDensity.current) { activityRoot.height.toDp() } + activityWidthDp.value = with(LocalDensity.current) { activityRoot.width.toDp() } + + + MessageScreen( + presentationStateManager = presentationStateManager, + inAppMessageSettings = settings, + onCreated = { onCreatedCalled = true }, + onDisposed = { onDisposedCalled = true }, + onGestureDetected = { gesture -> detectedGestures.add(gesture) }, + onBackPressed = { onBackPressed = true } + ) + } + + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).assertDoesNotExist() + + // Change the state of the presentation state manager to shown to display the message + presentationStateManager.onShown() + composeTestRule.waitForIdle() + MessageScreenTestHelper.validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) + + // Verify that the message content is resized to fit the screen + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() + val (expectedInitialHeight, expectedInitialWidth) = calculateDimensions( + screenHeightDp.value, + screenWidthDp.value, + activityHeightDp.value, + heightPercentage, + widthPercentage) + + MessageScreenTestHelper.validateViewSize( + contentBounds, + expectedInitialHeight, + expectedInitialWidth + ) + + assertTrue(onCreatedCalled) + assertFalse(onDisposedCalled) + assertFalse(onBackPressed) + assertTrue(detectedGestures.isEmpty()) + resetState() + + // Rotate the device to landscape + uiDevice.setOrientationLandscape() + + // Wait for the device to stabilize + uiDevice.waitForIdle() + composeTestRule.waitForIdle() + + // Verify that the message content is resized to fit the new orientation + MessageScreenTestHelper.validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) + val landscapeContentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() + val (expectedLandscapeHeight, expectedLandscapeWidth) = calculateDimensions( + screenHeightDp.value, + screenWidthDp.value, + activityHeightDp.value, + heightPercentage, + widthPercentage) + + MessageScreenTestHelper.validateViewSize( + landscapeContentBounds, + expectedLandscapeHeight, + expectedLandscapeWidth + ) + + // onCreated should not be called again due to orientation change restrictions + assertFalse(onCreatedCalled) + assertFalse(onDisposedCalled) + assertFalse(onBackPressed) + assertTrue(detectedGestures.isEmpty()) + resetState() + + // Rotate the device back to its original orientation + uiDevice.setOrientationNatural() + + // Wait for the device to stabilize + uiDevice.waitForIdle() + composeTestRule.waitForIdle() + MessageScreenTestHelper.validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) + + // Verify that the message content is restored to its original size + val (expectedNaturalHeight, expectedNaturalWidth) = calculateDimensions( + screenHeightDp.value, + screenWidthDp.value, + activityHeightDp.value, + heightPercentage, + widthPercentage) + + val naturalContentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() + + MessageScreenTestHelper.validateViewSize( + naturalContentBounds, + expectedNaturalHeight, + expectedNaturalWidth + ) + } + + /** + * Calculates the expected height and width of the message content based on the screen dimensions + * If the height exceeds what is allowed by the activity (due to actionbar), it takes + * up the full height of the activity + * @param screenHeightDp the screen height in dp + * @param screenWidthDp the screen width in dp + * @param activityHeightDp the height of the activity in dp + * @param heightPercentage the percentage of the screen height the message content should take + * @param widthPercentage the percentage of the screen width the message content should take + * @return a pair of the expected height and width of the message content + */ + private fun calculateDimensions( + screenHeightDp: Dp, + screenWidthDp: Dp, + activityHeightDp: Dp, + heightPercentage: Int, + widthPercentage: Int + ): Pair { + val expectedHeight = if ((screenHeightDp * (heightPercentage / 100f)) > activityHeightDp) { + activityHeightDp + } else { + screenHeightDp * (heightPercentage / 100f) + } + val expectedWidth = screenWidthDp * (widthPercentage / 100f) + + return Pair(expectedHeight, expectedWidth) + } + + private fun resetState() { + onCreatedCalled = false + onDisposedCalled = false + onBackPressed = false + detectedGestures.clear() + } + + @After + fun tearDown() { + resetState() + } + +} \ No newline at end of file diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTestHelper.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTestHelper.kt new file mode 100644 index 000000000..dc72ac89c --- /dev/null +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTestHelper.kt @@ -0,0 +1,70 @@ +/* + 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.services.ui.message.views + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEqualTo +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpRect +import androidx.compose.ui.unit.height +import androidx.compose.ui.unit.width +import androidx.test.ext.junit.rules.ActivityScenarioRule + +internal object MessageScreenTestHelper { + + /** + * Validates the size of the view bounds with the given height and width + */ + internal fun validateViewSize(viewBounds: DpRect, height: Dp, width: Dp) { + viewBounds.height.assertIsEqualTo(height, "failed", Dp(2f)) + viewBounds.width.assertIsEqualTo(width, "failed", Dp(2f)) + } + + /** + * Validates the bounds of the view with the given top, bottom, left and right values + */ + internal fun validateBounds(viewBounds: DpRect, top: Dp, bottom: Dp, left: Dp, right: Dp) { + viewBounds.top.assertIsEqualTo(top, "failed", Dp(2f)) + viewBounds.bottom.assertIsEqualTo(bottom, "failed", Dp(2f)) + viewBounds.left.assertIsEqualTo(left, "failed", Dp(2f)) + viewBounds.right.assertIsEqualTo(right, "failed", Dp(2f)) + } + + /** + * Validates the message content with the given backdrop and clipped values + * @param composeTestRule the compose test rule + * @param withBackdrop whether the backdrop is present + * @param clipped whether the message is expected to be clipped + */ + internal fun validateMessageAppeared( + composeTestRule: AndroidComposeTestRule, T>, + withBackdrop: Boolean, + clipped: Boolean + ) { + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).assertExists().also { + if (!clipped) it.assertIsDisplayed() + } + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).assertExists().also { + if (!clipped) it.assertIsDisplayed() + } + if (withBackdrop) { + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertExists().also { + if (!clipped) it.assertIsDisplayed() + } + } else { + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertDoesNotExist() + } + } +} diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt index d92f1ddeb..a05286691 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenTests.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEqualTo import androidx.compose.ui.test.click import androidx.compose.ui.test.getBoundsInRoot import androidx.compose.ui.test.getUnclippedBoundsInRoot @@ -27,15 +26,14 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeWithVelocity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.height -import androidx.compose.ui.unit.width import androidx.test.espresso.Espresso import androidx.test.ext.junit.runners.AndroidJUnit4 import com.adobe.marketing.mobile.services.ui.common.PresentationStateManager import com.adobe.marketing.mobile.services.ui.message.InAppMessageSettings +import com.adobe.marketing.mobile.services.ui.message.views.MessageScreenTestHelper.validateBounds +import com.adobe.marketing.mobile.services.ui.message.views.MessageScreenTestHelper.validateMessageAppeared +import com.adobe.marketing.mobile.services.ui.message.views.MessageScreenTestHelper.validateViewSize import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -108,7 +106,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) assertTrue(onCreatedCalled) assertFalse(onDisposedCalled) @@ -135,7 +137,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) // Change the state of the presentation state manager to hidden to remove the message presentationStateManager.onHidden() @@ -173,7 +179,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = true, + clipped = false + ) assertTrue(onCreatedCalled) assertFalse(onDisposedCalled) @@ -200,7 +210,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = true, + clipped = false + ) // Press back Espresso.pressBack() @@ -234,7 +248,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = true, + clipped = false + ) // Swipe gestures composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performTouchInput { // create a swipe right gesture @@ -271,7 +289,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) // Swipe gestures composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performTouchInput { @@ -309,25 +331,31 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = true, + clipped = false + ) // Swipe gestures - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).assertIsDisplayed().performTouchInput { - // create a swipe down gesture - swipeWithVelocity( - start = Offset(0f, 10f), - end = Offset(0f, 600f), - endVelocity = 1000f - ) - } + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).assertIsDisplayed() + .performTouchInput { + // create a swipe down gesture + swipeWithVelocity( + start = Offset(0f, 10f), + end = Offset(0f, 600f), + endVelocity = 1000f + ) + } composeTestRule.waitForIdle() - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertIsDisplayed().performTouchInput { - click( - Offset(100f, 10f) - ) - } + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertIsDisplayed() + .performTouchInput { + click( + Offset(100f, 10f) + ) + } composeTestRule.waitForIdle() assertTrue(onCreatedCalled) @@ -338,10 +366,10 @@ class MessageScreenTests { } // ---------------------------------------------------------------------------------------------- - // Test cases for orientation changes + // Test cases for configuration changes // ---------------------------------------------------------------------------------------------- @Test - fun testMessageScreenIsRestoredOnOrientationChange() { + fun testMessageScreenIsRestoredOnConfigurationChange() { val restorationTester = StateRestorationTester(composeTestRule) restorationTester.setContent { // setting our composable as content for test MessageScreen( @@ -360,7 +388,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) assertTrue(onCreatedCalled) assertFalse(onDisposedCalled) @@ -371,7 +403,11 @@ class MessageScreenTests { restorationTester.emulateSavedInstanceStateRestore() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) } // ---------------------------------------------------------------------------------------------- @@ -394,7 +430,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } @@ -414,14 +451,20 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) // Message Frame all the available height and width allowed by the parent (in this case ComponentActivity) - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() validateViewSize(frameBounds, activityHeightDp, screenWidthDp) // Message Content(WebView) is 100% of height and width, as allowed by the activity - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() validateViewSize(contentBounds, activityHeightDp, screenWidthDp) } @@ -444,7 +487,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -463,7 +507,11 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) // Message Frame all the available height and width allowed by the parent (in this case ComponentActivity) val frameBounds = @@ -475,9 +523,17 @@ class MessageScreenTests { val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) .getUnclippedBoundsInRoot() if ((screenHeightDp * (heightPercentage / 100f)) > activityHeightDp) { - validateViewSize(contentBounds, activityHeightDp, screenWidthDp * (widthPercentage / 100f)) + validateViewSize( + contentBounds, + activityHeightDp, + screenWidthDp * (widthPercentage / 100f) + ) } else { - validateViewSize(contentBounds, screenHeightDp * (heightPercentage / 100f), screenWidthDp * (widthPercentage / 100f)) + validateViewSize( + contentBounds, + screenHeightDp * (heightPercentage / 100f), + screenWidthDp * (widthPercentage / 100f) + ) } } @@ -504,7 +560,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -523,16 +580,28 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() + val contentBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val horizontalContentPaddingDp = (screenWidthDp * (100 - widthPercent).toFloat() / 100f) / 2 // Frame is same size as its parent so bounds should match the root bounds - validateBounds(frameBounds, rootBounds.top, rootBounds.bottom, rootBounds.left, rootBounds.right) + validateBounds( + frameBounds, + rootBounds.top, + rootBounds.bottom, + rootBounds.left, + rootBounds.right + ) // Content is top aligned vertically and centered horizontally and takes 80% of screen width validateBounds( @@ -564,7 +633,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -583,16 +653,28 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() + val contentBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val horizontalContentPaddingDp = (screenWidthDp * (100 - widthPercent).toFloat() / 100f) / 2 // Frame is same size as its parent so bounds should match the root bounds - validateBounds(frameBounds, rootBounds.top, rootBounds.bottom, rootBounds.left, rootBounds.right) + validateBounds( + frameBounds, + rootBounds.top, + rootBounds.bottom, + rootBounds.left, + rootBounds.right + ) // Content is bottom aligned vertically and centered horizontally and takes 80% of screen width validateBounds( @@ -624,7 +706,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -643,16 +726,28 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() + val contentBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidth = screenWidthDp * widthPercent.toFloat() / 100f // Frame is same size as its parent so bounds should match the root bounds - validateBounds(frameBounds, rootBounds.top, rootBounds.bottom, rootBounds.left, rootBounds.right) + validateBounds( + frameBounds, + rootBounds.top, + rootBounds.bottom, + rootBounds.left, + rootBounds.right + ) // Content is center aligned vertically and left aligned horizontally and takes heightPercent% of screen height validateBounds( @@ -684,7 +779,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -703,16 +799,28 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() + val contentBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidth = screenWidthDp * widthPercent.toFloat() / 100f // Frame is same size as its parent so bounds should match the root bounds - validateBounds(frameBounds, rootBounds.top, rootBounds.bottom, rootBounds.left, rootBounds.right) + validateBounds( + frameBounds, + rootBounds.top, + rootBounds.bottom, + rootBounds.left, + rootBounds.right + ) // Content is center aligned vertically and right aligned horizontally and takes heightPercent% of screen height validateBounds( @@ -745,7 +853,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -764,16 +873,28 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = false + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getBoundsInRoot() + val contentBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidth = screenWidthDp * widthPercent.toFloat() / 100f // Frame is same size as its parent so bounds should match the root bounds - validateBounds(frameBounds, rootBounds.top, rootBounds.bottom, rootBounds.left, rootBounds.right) + validateBounds( + frameBounds, + rootBounds.top, + rootBounds.bottom, + rootBounds.left, + rootBounds.right + ) // Content is center aligned vertically and right aligned horizontally and takes heightPercent% of screen height validateBounds( @@ -810,7 +931,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -829,11 +951,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val horizontalContentPaddingDp = (screenWidthDp * (100 - widthPercent).toFloat() / 100f) / 2 val offsetDp = screenHeightDp * offsetPercent.toFloat() / 100f @@ -879,7 +1007,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -898,11 +1027,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val horizontalContentPaddingDp = (screenWidthDp * (100 - widthPercent).toFloat() / 100f) / 2 val offsetDp = screenHeightDp * offsetPercent.toFloat() / 100f @@ -948,7 +1083,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -967,11 +1103,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val horizontalContentPaddingDp = (screenWidthDp * (100 - widthPercent).toFloat() / 100f) / 2 val offsetDp = screenHeightDp * offsetPercent.toFloat() / 100f @@ -1017,7 +1159,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -1036,11 +1179,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val horizontalContentPaddingDp = (screenWidthDp * (100 - widthPercent).toFloat() / 100f) / 2 val offsetDp = screenHeightDp * offsetPercent.toFloat() / 100f @@ -1086,7 +1235,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -1105,11 +1255,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidthDp = screenWidthDp * widthPercent.toFloat() / 100f val verticalContentPaddingDp = (activityHeightDp - contentHeightDp) / 2 @@ -1156,7 +1312,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -1175,11 +1332,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidthDp = screenWidthDp * widthPercent.toFloat() / 100f val verticalContentPaddingDp = (activityHeightDp - contentHeightDp) / 2 @@ -1226,7 +1389,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -1245,11 +1409,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidthDp = screenWidthDp * widthPercent.toFloat() / 100f val verticalContentPaddingDp = (activityHeightDp - contentHeightDp) / 2 @@ -1296,7 +1466,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -1315,11 +1486,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidthDp = screenWidthDp * widthPercent.toFloat() / 100f val verticalContentPaddingDp = (activityHeightDp - contentHeightDp) / 2 @@ -1368,7 +1545,8 @@ class MessageScreenTests { screenHeightDp = currentConfiguration.screenHeightDp.dp screenWidthDp = currentConfiguration.screenWidthDp.dp - val activityRoot = composeTestRule.activity.window.decorView.findViewById(android.R.id.content) + val activityRoot = + composeTestRule.activity.window.decorView.findViewById(android.R.id.content) activityHeightDp = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp = with(LocalDensity.current) { activityRoot.width.toDp() } MessageScreen( @@ -1387,11 +1565,17 @@ class MessageScreenTests { // Change the state of the presentation state manager to shown to display the message presentationStateManager.onShown() composeTestRule.waitForIdle() - validateMessageAppeared(false, clipped = true) + validateMessageAppeared( + composeTestRule = composeTestRule, + withBackdrop = false, + clipped = true + ) val rootBounds = composeTestRule.onRoot().getBoundsInRoot() - val frameBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() - val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).getUnclippedBoundsInRoot() + val frameBounds = + composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).getUnclippedBoundsInRoot() + val contentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) + .getUnclippedBoundsInRoot() val contentHeightDp = screenHeightDp * heightPercent.toFloat() / 100f val contentWidthDp = screenWidthDp * widthPercent.toFloat() / 100f val heightOffset = screenHeightDp * offsetPercent.toFloat() / 100f @@ -1426,22 +1610,6 @@ class MessageScreenTests { detectedGestures.clear() } - private fun validateMessageAppeared(withBackdrop: Boolean, clipped: Boolean = false) { - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_FRAME).assertExists().also { - if (!clipped) it.assertIsDisplayed() - } - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).assertExists().also { - if (!clipped) it.assertIsDisplayed() - } - if (withBackdrop) { - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertExists().also { - if (!clipped) it.assertIsDisplayed() - } - } else { - composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertDoesNotExist() - } - } - private fun getSettings(withUiTakeOver: Boolean): InAppMessageSettings { return InAppMessageSettings.Builder() .backdropOpacity(0.5f) @@ -1457,16 +1625,4 @@ class MessageScreenTests { .gestureMap(acceptedGestures) .build() } - - private fun validateViewSize(viewBounds: DpRect, height: Dp, width: Dp) { - viewBounds.height.assertIsEqualTo(height, "failed", Dp(2f)) - viewBounds.width.assertIsEqualTo(width, "failed", Dp(2f)) - } - - private fun validateBounds(viewBounds: DpRect, top: Dp, bottom: Dp, left: Dp, right: Dp) { - viewBounds.top.assertIsEqualTo(top, "failed", Dp(2f)) - viewBounds.bottom.assertIsEqualTo(bottom, "failed", Dp(2f)) - viewBounds.left.assertIsEqualTo(left, "failed", Dp(2f)) - viewBounds.right.assertIsEqualTo(right, "failed", Dp(2f)) - } } From 91292ea6c4aec4266fc126ce2832d4e79158ce7e Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 13 May 2024 16:17:27 -0700 Subject: [PATCH 16/30] Fix formatting --- .../views/MessageScreenOrientationTests.kt | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt index 5ba9a1863..f17862128 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt @@ -1,3 +1,14 @@ +/* + 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.services.ui.message.views import android.view.View @@ -45,16 +56,15 @@ class MessageScreenOrientationTests { private val presentationStateManager = PresentationStateManager() private val HTML_TEXT_SAMPLE = "\n" + - "\n" + - "A Sample HTML Page\n" + - "\n" + - "\n" + - "\n" + - "

This is a sample HTML page

\n" + - "\n" + - "\n" + - "" - + "\n" + + "A Sample HTML Page\n" + + "\n" + + "\n" + + "\n" + + "

This is a sample HTML page

\n" + + "\n" + + "\n" + + "" // ---------------------------------------------------------------------------------------------- // Test cases for orientation changes @@ -87,7 +97,6 @@ class MessageScreenOrientationTests { activityHeightDp.value = with(LocalDensity.current) { activityRoot.height.toDp() } activityWidthDp.value = with(LocalDensity.current) { activityRoot.width.toDp() } - MessageScreen( presentationStateManager = presentationStateManager, inAppMessageSettings = settings, @@ -118,7 +127,8 @@ class MessageScreenOrientationTests { screenWidthDp.value, activityHeightDp.value, heightPercentage, - widthPercentage) + widthPercentage + ) MessageScreenTestHelper.validateViewSize( contentBounds, @@ -152,7 +162,8 @@ class MessageScreenOrientationTests { screenWidthDp.value, activityHeightDp.value, heightPercentage, - widthPercentage) + widthPercentage + ) MessageScreenTestHelper.validateViewSize( landscapeContentBounds, @@ -185,7 +196,8 @@ class MessageScreenOrientationTests { screenWidthDp.value, activityHeightDp.value, heightPercentage, - widthPercentage) + widthPercentage + ) val naturalContentBounds = composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT) .getUnclippedBoundsInRoot() @@ -236,5 +248,4 @@ class MessageScreenOrientationTests { fun tearDown() { resetState() } - -} \ No newline at end of file +} From d01ca505192c4873eb63c2583ab02153bc9839a9 Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Mon, 13 May 2024 21:43:14 -0700 Subject: [PATCH 17/30] Add tests to verify AnalyticsForIdentityRequest event --- .../identity/IdentityFunctionalTests.kt | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt index f5360f21d..aba28c1d3 100644 --- a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt +++ b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt @@ -12,7 +12,10 @@ package com.adobe.marketing.mobile.identity import com.adobe.marketing.mobile.Event +import com.adobe.marketing.mobile.EventSource +import com.adobe.marketing.mobile.EventType import com.adobe.marketing.mobile.ExtensionApi +import com.adobe.marketing.mobile.MobilePrivacyStatus import com.adobe.marketing.mobile.SharedStateResult import com.adobe.marketing.mobile.SharedStateStatus import com.adobe.marketing.mobile.VisitorID @@ -52,6 +55,7 @@ import java.net.HttpURLConnection import java.util.Random import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.test.assertNotEquals private typealias NetworkMonitor = (url: String) -> Unit @@ -2037,4 +2041,147 @@ class IdentityFunctionalTests { countDownLatch.await() } + + @Test(timeout = 10000) + fun test_resetIdentities_doesNotDispatch_analyticsForIdentityRequest() { + val configuration = mapOf( + "experienceCloud.org" to "orgid", + "experienceCloud.server" to "test.com", + "global.privacy" to "optedin" + ) + val identityExtension = initializeIdentityExtensionWithPreset( + FRESH_INSTALL_WITHOUT_CACHE, + configuration + ) + + val oldMid = identityExtension.mid + + doAnswer { invocation -> + fail("Did not expect any dispatched events but found ${invocation.arguments.size}!") + }.`when`(mockedExtensionApi).dispatch(any()) + + identityExtension.handleIdentityRequestReset( + Event.Builder( + "event", + "com.adobe.eventType.generic.identity", + "com.adobe.eventSource.requestReset" + ).build() + ) + + assertNotEquals(oldMid, identityExtension.mid) + } + + @Test(timeout = 10000) + fun test_setPushIdentifier_validToken_dispatchesAnalyticsForIdentityRequest() { + val configuration = mapOf( + "experienceCloud.org" to "orgid", + "experienceCloud.server" to "test.com", + "global.privacy" to "optedin" + ) + val identityExtension = initializeIdentityExtensionWithPreset( + FRESH_INSTALL_WITHOUT_CACHE, + configuration + ) + + val countDownLatch = CountDownLatch(1) + doAnswer { invocation -> + val event: Event? = invocation.arguments[0] as Event? + assertEquals(EventType.ANALYTICS, event?.type) + assertEquals(EventSource.REQUEST_CONTENT, event?.source) + val eventData = event?.eventData + assertNotNull(eventData) + val jsonObject = JSONObject(eventData) + assertEquals("Push", jsonObject.getString("action")) + assertTrue(jsonObject.getBoolean("trackinternal")) + val jsonContextObject = jsonObject.getJSONObject("contextdata") + assertEquals("true", jsonContextObject.getString("a.push.optin")) + countDownLatch.countDown() + }.`when`(mockedExtensionApi).dispatch(any()) + + identityExtension.processIdentityRequest( + Event.Builder( + "event", + "com.adobe.eventType.generic.identity", + "com.adobe.eventSource.requestContent" + ).setEventData( + mapOf( + "pushidentifier" to "D52DB39EEE21395B2B67B895FC478301CE6E936D82521E095902A5E0F57EE0B3" + ) + ).build() + ) + + countDownLatch.await() + } + + @Test(timeout = 10000) + fun test_setPushIdentifier_null_dispatchesAnalyticsForIdentityRequest() { + val configuration = mapOf( + "experienceCloud.org" to "orgid", + "experienceCloud.server" to "test.com", + "global.privacy" to "optedin" + ) + val identityExtension = initializeIdentityExtensionWithPreset( + FRESH_INSTALL_WITHOUT_CACHE, + configuration + ) + + val countDownLatch = CountDownLatch(1) + doAnswer { invocation -> + val event: Event? = invocation.arguments[0] as Event? + assertEquals(EventType.ANALYTICS, event?.type) + assertEquals(EventSource.REQUEST_CONTENT, event?.source) + val eventData = event?.eventData + assertNotNull(eventData) + val jsonObject = JSONObject(eventData) + assertEquals("Push", jsonObject.getString("action")) + assertTrue(jsonObject.getBoolean("trackinternal")) + val jsonContextObject = jsonObject.getJSONObject("contextdata") + assertEquals("false", jsonContextObject.getString("a.push.optin")) + countDownLatch.countDown() + }.`when`(mockedExtensionApi).dispatch(any()) + + identityExtension.processIdentityRequest( + Event.Builder( + "event", + "com.adobe.eventType.generic.identity", + "com.adobe.eventSource.requestContent" + ).setEventData( + mapOf( + "pushidentifier" to null + ) + ).build() + ) + + countDownLatch.await() + } + @Test(timeout = 10000) + fun test_updatePrivacyStatusOptedOut_doesNotDispatch_analyticsForIdentityRequest() { + val configuration = mapOf( + "experienceCloud.org" to "orgid", + "experienceCloud.server" to "test.com", + "global.privacy" to "optedin" + ) + val identityExtension = initializeIdentityExtensionWithPreset( + FRESH_INSTALL_WITHOUT_CACHE, + configuration + ) + + assertNotNull(identityExtension.mid) + + doAnswer { invocation -> + fail("Did not expect any dispatched events but found ${invocation.arguments.size}!") + }.`when`(mockedExtensionApi).dispatch(any()) + + identityExtension.handleConfiguration( + Event.Builder( + "configuration response", + "com.adobe.eventType.configuration", + "com.adobe.eventSource.responseContent" + ).setEventData( + mapOf("global.privacy" to MobilePrivacyStatus.OPT_OUT.value) + ).build() + ) + + assertNull(identityExtension.mid) + } } From da77bf7c6c9795723d10ce1ade067905e32e832c Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Mon, 13 May 2024 21:50:21 -0700 Subject: [PATCH 18/30] On opt-out clear push ID and flags but don't call updatePushIdentifer which triggers Analytics event. --- .../mobile/identity/IdentityExtension.java | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java index 36e8d4bf5..8d0115518 100644 --- a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java +++ b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java @@ -499,18 +499,8 @@ void handleIdentityRequestReset(@NonNull final Event event) { "handleIdentityRequestReset: Privacy is opt-out, ignoring event."); return; } - mid = null; - advertisingIdentifier = null; - blob = null; - locationHint = null; - customerIds = null; - pushIdentifier = null; - - if (namedCollection != null) { - namedCollection.remove(DataStoreKeys.AID_SYNCED_KEY); - namedCollection.remove(DataStoreKeys.PUSH_ENABLED); - } + clearIdentifiers(); savePersistently(); // clear datastore // When resetting identifiers, need to generate new Experience Cloud ID for the user @@ -1857,17 +1847,7 @@ void processPrivacyChange(final Event event, final Map eventData privacyStatus.getValue()); if (privacyStatus == MobilePrivacyStatus.OPT_OUT) { - mid = null; - advertisingIdentifier = null; - blob = null; - locationHint = null; - customerIds = null; - - if (namedCollection != null) { - namedCollection.remove(DataStoreKeys.AID_SYNCED_KEY); - } - - updatePushIdentifier(null); + clearIdentifiers(); savePersistently(); // clear datastore getApi().createSharedState(packageEventData(), event); } else if (StringUtils.isNullOrEmpty(mid)) { @@ -2451,6 +2431,21 @@ private void loadPrivacyStatusFromConfigurationState(final Map c privacyStatus = MobilePrivacyStatus.fromString(privacyString); } + /** Clears in-memory identifiers and persisted push ID flags. */ + private void clearIdentifiers() { + mid = null; + advertisingIdentifier = null; + blob = null; + locationHint = null; + customerIds = null; + pushIdentifier = null; + + if (namedCollection != null) { + namedCollection.remove(DataStoreKeys.AID_SYNCED_KEY); + namedCollection.remove(DataStoreKeys.PUSH_ENABLED); + } + } + @VisibleForTesting String getMid() { return mid; From 165a9ad0af10eecee1c332f341f77b1e15b7c574 Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Tue, 14 May 2024 16:30:11 -0700 Subject: [PATCH 19/30] Update tests to assert persisted push token --- .../identity/IdentityFunctionalTests.kt | 92 +++++++++++++------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt index aba28c1d3..96aac9d8b 100644 --- a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt +++ b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt @@ -38,6 +38,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito @@ -2042,35 +2043,6 @@ class IdentityFunctionalTests { countDownLatch.await() } - @Test(timeout = 10000) - fun test_resetIdentities_doesNotDispatch_analyticsForIdentityRequest() { - val configuration = mapOf( - "experienceCloud.org" to "orgid", - "experienceCloud.server" to "test.com", - "global.privacy" to "optedin" - ) - val identityExtension = initializeIdentityExtensionWithPreset( - FRESH_INSTALL_WITHOUT_CACHE, - configuration - ) - - val oldMid = identityExtension.mid - - doAnswer { invocation -> - fail("Did not expect any dispatched events but found ${invocation.arguments.size}!") - }.`when`(mockedExtensionApi).dispatch(any()) - - identityExtension.handleIdentityRequestReset( - Event.Builder( - "event", - "com.adobe.eventType.generic.identity", - "com.adobe.eventSource.requestReset" - ).build() - ) - - assertNotEquals(oldMid, identityExtension.mid) - } - @Test(timeout = 10000) fun test_setPushIdentifier_validToken_dispatchesAnalyticsForIdentityRequest() { val configuration = mapOf( @@ -2085,6 +2057,7 @@ class IdentityFunctionalTests { val countDownLatch = CountDownLatch(1) doAnswer { invocation -> + assertEquals(1, invocation.arguments.size) val event: Event? = invocation.arguments[0] as Event? assertEquals(EventType.ANALYTICS, event?.type) assertEquals(EventSource.REQUEST_CONTENT, event?.source) @@ -2098,6 +2071,10 @@ class IdentityFunctionalTests { countDownLatch.countDown() }.`when`(mockedExtensionApi).dispatch(any()) + val persistedData = capturePersistedData() + + val pushToken = "D52DB39EEE21395B2B67B895FC478301CE6E936D82521E095902A5E0F57EE0B3" + identityExtension.processIdentityRequest( Event.Builder( "event", @@ -2105,12 +2082,16 @@ class IdentityFunctionalTests { "com.adobe.eventSource.requestContent" ).setEventData( mapOf( - "pushidentifier" to "D52DB39EEE21395B2B67B895FC478301CE6E936D82521E095902A5E0F57EE0B3" + "pushidentifier" to pushToken ) ).build() ) countDownLatch.await() + + assertEquals(pushToken, persistedData["ADOBEMOBILE_PUSH_IDENTIFIER"] as? String) + assertTrue(persistedData["ADOBEMOBILE_ANALYTICS_PUSH_SYNC"] as? Boolean ?: false) + assertTrue(persistedData["ADOBEMOBILE_PUSH_ENABLED"] as? Boolean ?: false) } @Test(timeout = 10000) @@ -2127,6 +2108,7 @@ class IdentityFunctionalTests { val countDownLatch = CountDownLatch(1) doAnswer { invocation -> + assertEquals(1, invocation.arguments.size) val event: Event? = invocation.arguments[0] as Event? assertEquals(EventType.ANALYTICS, event?.type) assertEquals(EventSource.REQUEST_CONTENT, event?.source) @@ -2140,6 +2122,8 @@ class IdentityFunctionalTests { countDownLatch.countDown() }.`when`(mockedExtensionApi).dispatch(any()) + val persistedData = capturePersistedData() + identityExtension.processIdentityRequest( Event.Builder( "event", @@ -2153,6 +2137,10 @@ class IdentityFunctionalTests { ) countDownLatch.await() + + assertNull(persistedData["ADOBEMOBILE_PUSH_IDENTIFIER"] as? String) + assertTrue(persistedData["ADOBEMOBILE_ANALYTICS_PUSH_SYNC"] as? Boolean ?: false) + assertFalse(persistedData["ADOBEMOBILE_PUSH_ENABLED"] as? Boolean ?: true) } @Test(timeout = 10000) fun test_updatePrivacyStatusOptedOut_doesNotDispatch_analyticsForIdentityRequest() { @@ -2184,4 +2172,48 @@ class IdentityFunctionalTests { assertNull(identityExtension.mid) } + + @Test(timeout = 10000) + fun test_resetIdentities_doesNotDispatch_analyticsForIdentityRequest() { + val configuration = mapOf( + "experienceCloud.org" to "orgid", + "experienceCloud.server" to "test.com", + "global.privacy" to "optedin" + ) + val identityExtension = initializeIdentityExtensionWithPreset( + FRESH_INSTALL_WITHOUT_CACHE, + configuration + ) + + val oldMid = identityExtension.mid + + doAnswer { invocation -> + fail("Did not expect any dispatched events but found ${invocation.arguments.size}!") + }.`when`(mockedExtensionApi).dispatch(any()) + + identityExtension.handleIdentityRequestReset( + Event.Builder( + "event", + "com.adobe.eventType.generic.identity", + "com.adobe.eventSource.requestReset" + ).build() + ) + + assertNotEquals(oldMid, identityExtension.mid) + } + + private fun capturePersistedData(): Map { + val persistedData = mutableMapOf() + doAnswer { + val key = it.arguments[0] as String + val value = it.arguments[1] as String + persistedData[key] = value + }.`when`(mockedNamedCollection).setString(anyString(), anyString()) + doAnswer { + val key = it.arguments[0] as String + val value = it.arguments[1] as Boolean + persistedData[key] = value + }.`when`(mockedNamedCollection).setBoolean(anyString(), anyBoolean()) + return persistedData + } } From efbd90008cc6ede7f310513e2dcd193c941efcdb Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Tue, 14 May 2024 20:50:00 -0700 Subject: [PATCH 20/30] Remove timeout from functional tests which don't wait on async operations. --- .../adobe/marketing/mobile/identity/IdentityFunctionalTests.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt index 96aac9d8b..692149654 100644 --- a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt +++ b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityFunctionalTests.kt @@ -2142,7 +2142,6 @@ class IdentityFunctionalTests { assertTrue(persistedData["ADOBEMOBILE_ANALYTICS_PUSH_SYNC"] as? Boolean ?: false) assertFalse(persistedData["ADOBEMOBILE_PUSH_ENABLED"] as? Boolean ?: true) } - @Test(timeout = 10000) fun test_updatePrivacyStatusOptedOut_doesNotDispatch_analyticsForIdentityRequest() { val configuration = mapOf( "experienceCloud.org" to "orgid", @@ -2173,7 +2172,6 @@ class IdentityFunctionalTests { assertNull(identityExtension.mid) } - @Test(timeout = 10000) fun test_resetIdentities_doesNotDispatch_analyticsForIdentityRequest() { val configuration = mapOf( "experienceCloud.org" to "orgid", From 41743585247398deb87ac26a8d1a1471b34ec014 Mon Sep 17 00:00:00 2001 From: Yansong Yang Date: Wed, 15 May 2024 14:59:49 -0500 Subject: [PATCH 21/30] Lifecycle v2 enhancement (app upgrade) (#650) --- .../lifecycle/LifecycleFunctionalTest.java | 94 +++++++++++++++++++ .../lifecycle/LifecycleV2FunctionalTest.java | 75 +++++++++++++++ .../mobile/lifecycle/LifecycleUtil.java | 24 +++++ .../lifecycle/LifecycleV2Extension.java | 5 +- .../lifecycle/LifecycleV2MetricsBuilder.java | 25 +---- .../lifecycle/LifecycleV2ExtensionTest.java | 51 +++++++++- 6 files changed, 245 insertions(+), 29 deletions(-) diff --git a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java index 36bd9e9a3..cbad0519c 100644 --- a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java +++ b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleFunctionalTest.java @@ -622,6 +622,100 @@ public void testLifecycle__When__SecondLaunch_VersionNumberChanged__Then__GetUpg lifecycleDataStore.getLong("UpgradeDate", 0L)); } + @Test + public void + testLifecycle__When__SecondLaunch_VersionCodeChanged__Then__GetApplicationLaunchEvent__withIsUpgradeFalse() { + // setup + long firstSessionStartTimeMillis = currentTimestampMillis; + long firstSessionPauseTimeMillis = + firstSessionStartTimeMillis + TimeUnit.SECONDS.toMillis(10); + long secondSessionStartTimeMillis = firstSessionPauseTimeMillis + TimeUnit.DAYS.toMillis(1); + + Map configurationMap = new HashMap<>(); + configurationMap.put(LIFECYCLE_CONFIG_SESSION_TIMEOUT, 30L); + + TestableExtensionApi mockExtensionApi2 = new TestableExtensionApi(); + mockExtensionApi2.ignoreEvent(EventType.LIFECYCLE, EventSource.APPLICATION_CLOSE); + mockExtensionApi2.ignoreEvent(EventType.LIFECYCLE, EventSource.APPLICATION_LAUNCH); + mockExtensionApi2.simulateSharedState( + "com.adobe.module.configuration", SharedStateStatus.SET, configurationMap); + + // test + mockExtensionApi.simulateComingEvent(createStartEvent(null, firstSessionStartTimeMillis)); + mockExtensionApi.simulateComingEvent(createPauseEvent(firstSessionPauseTimeMillis)); + + mockDeviceInfoService.applicationVersionCode = "123456"; + LifecycleExtension lifecycleSession2 = + new LifecycleExtension( + mockExtensionApi2, lifecycleDataStore, mockDeviceInfoService); + lifecycleSession2.onRegistered(); + mockExtensionApi2.resetDispatchedEventAndCreatedSharedState(); + mockExtensionApi2.simulateComingEvent(createStartEvent(null, secondSessionStartTimeMillis)); + + // verify + Map expectedContextData = + new HashMap() { + { + put(LAUNCH_EVENT, "LaunchEvent"); + put(HOUR_OF_DAY, hourOfDay); + put(DAY_OF_WEEK, getDayOfWeek(secondSessionStartTimeMillis)); + put(LAUNCHES, "2"); + put(OPERATING_SYSTEM, "TEST_OS 5.55"); + put(LOCALE, "en-US"); + put(SYSTEM_LOCALE, "fr-FR"); + put(DEVICE_RESOLUTION, "100x100"); + put(CARRIER_NAME, "TEST_CARRIER"); + put(DEVICE_NAME, "deviceName"); + put(APP_ID, "TEST_APPLICATION_NAME 1.1 (123456)"); + put(RUN_MODE, "APPLICATION"); + put(PREVIOUS_SESSION_LENGTH, "10"); + put(DAYS_SINCE_FIRST_LAUNCH, "1"); + put(DAYS_SINCE_LAST_LAUNCH, "1"); + put(PREVIOUS_APPID, "TEST_APPLICATION_NAME 1.1 (12345)"); + put(PREVIOUS_OS, "TEST_OS 5.55"); + put(DAILY_ENGAGED_EVENT, "DailyEngUserEvent"); + } + }; + Map expectedEventData = + new HashMap() { + { + put( + PREVIOUS_SESSION_START_TIMESTAMP, + roundedToNearestSecond(firstSessionStartTimeMillis)); + put( + PREVIOUS_SESSION_PAUSE_TIMESTAMP, + roundedToNearestSecond(firstSessionPauseTimeMillis)); + put(MAX_SESSION_LENGTH, MAX_SESSION_LENGTH_SECONDS); + put( + SESSION_START_TIMESTAMP, + roundedToNearestSecond(secondSessionStartTimeMillis)); + put(SESSION_EVENT, LIFECYCLE_START); + put(LIFECYCLE_CONTEXT_DATA, expectedContextData); + } + }; + Map expectedSharedState = + new HashMap() { + { + put(MAX_SESSION_LENGTH, MAX_SESSION_LENGTH_SECONDS); + put( + SESSION_START_TIMESTAMP, + roundedToNearestSecond(secondSessionStartTimeMillis)); + put(LIFECYCLE_CONTEXT_DATA, expectedContextData); + } + }; + + Map secondSessionStartResponseEventData = + mockExtensionApi2.dispatchedEvents.get(0).getEventData(); + assertEquals(expectedEventData, secondSessionStartResponseEventData); + Map secondSessionStarSharedState = + mockExtensionApi2.createdSharedState.get(0); + assertEquals(expectedSharedState, secondSessionStarSharedState); + + assertEquals( + TimeUnit.MILLISECONDS.toSeconds(firstSessionStartTimeMillis), + lifecycleDataStore.getLong("InstallDate", 0L)); + } + @Test public void testLifecycle__When__SecondLaunch_ThreeDaysAfterInstall__Then__DaysSinceFirstUseIs3() { diff --git a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java index 31a87d704..6b12d37b2 100644 --- a/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java +++ b/code/lifecycle/src/androidTest/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2FunctionalTest.java @@ -341,6 +341,81 @@ public void testLifecycleV2__When__Pause__Then__DispatchLifecycleApplicationClos secondSessionStartApplicationLaunchEvent.getParentID()); } + @Test + public void + testLifecycleV2__When__SecondLaunch_VersionNumberCode__Then__GetApplicationLaunchEvent__withIsUpgradeTrue() + throws InterruptedException { + // setup + // both session starts dispatch application launch event + setExpectationEvent( + EventType.LIFECYCLE, EventSource.APPLICATION_LAUNCH, 2, mockExtensionApi); + // session pauses dispatches application close event + setExpectationEvent( + EventType.LIFECYCLE, EventSource.APPLICATION_CLOSE, 1, mockExtensionApi); + + // test + mockExtensionApi.simulateComingEvent(createStartEvent(null, currentTimestampMillis)); + mockExtensionApi.simulateComingEvent( + createPauseEvent(currentTimestampMillis + TimeUnit.SECONDS.toMillis(10))); + + sleep(1000); + mockDeviceInfoService.applicationVersionCode = "123456"; + + long secondSessionStartTimeInMillis = currentTimestampMillis + TimeUnit.DAYS.toMillis(1); + final Event secondStartEvent = createStartEvent(null, secondSessionStartTimeInMillis); + mockExtensionApi.simulateComingEvent(secondStartEvent); + + // verify + assertExpectedEvents(mockExtensionApi); + + Map expectedApplicationInfo = new HashMap<>(); + expectedApplicationInfo.put("name", "TEST_APPLICATION_NAME"); + expectedApplicationInfo.put("version", "1.1 (123456)"); + expectedApplicationInfo.put("isUpgrade", true); + expectedApplicationInfo.put("isLaunch", true); + expectedApplicationInfo.put("id", "TEST_PACKAGE_NAME"); + expectedApplicationInfo.put( + "_dc", + new HashMap() { + { + put("language", "en-US"); + } + }); + + Map expectedXDMData = + new HashMap() { + { + put("environment", expectedEnvironmentInfo); + put("device", expectedDeviceInfo); + put("application", expectedApplicationInfo); + put( + "timestamp", + LifecycleUtil.dateTimeISO8601String( + new Date(secondSessionStartTimeInMillis))); + put("eventType", "application.launch"); + } + }; + Map expectedEventData = + new HashMap() { + { + put(XDM, expectedXDMData); + } + }; + + Event secondSessionStartApplicationLaunchEvent = mockExtensionApi.dispatchedEvents.get(2); + assertEquals( + "Application Launch (Foreground)", + secondSessionStartApplicationLaunchEvent.getName()); + assertEquals(EventType.LIFECYCLE, secondSessionStartApplicationLaunchEvent.getType()); + assertEquals( + EventSource.APPLICATION_LAUNCH, + secondSessionStartApplicationLaunchEvent.getSource()); + assertEquals(expectedEventData, secondSessionStartApplicationLaunchEvent.getEventData()); + assertEquals( + secondStartEvent.getUniqueIdentifier(), + secondSessionStartApplicationLaunchEvent.getParentID()); + } + @Test public void testLifecycleV2__When__SecondLaunch_VersionNumberNotChanged__Then__GetApplicationLaunchEvent() diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java index 71ec9b249..e660e6b3f 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleUtil.java @@ -11,6 +11,7 @@ package com.adobe.marketing.mobile.lifecycle; +import com.adobe.marketing.mobile.services.DeviceInforming; import com.adobe.marketing.mobile.util.StringUtils; import java.text.SimpleDateFormat; import java.util.Date; @@ -87,4 +88,27 @@ static String formatLocaleXDM(final Locale locale) { return locale.toLanguageTag(); } + + /** + * Returns the application version in the format of "appVersion (versionCode)". Example: 2.3 + * (10) + * + * @param deviceInfoService DeviceInfoService instance + * @return application version + */ + static String getV2AppVersion(final DeviceInforming deviceInfoService) { + if (deviceInfoService == null) { + return null; + } + final String applicationVersion = deviceInfoService.getApplicationVersion(); + final String applicationVersionCode = deviceInfoService.getApplicationVersionCode(); + return String.format( + "%s%s", + !StringUtils.isNullOrEmpty(applicationVersion) + ? String.format("%s", applicationVersion) + : "", + !StringUtils.isNullOrEmpty(applicationVersionCode) + ? String.format(" (%s)", applicationVersionCode) + : ""); + } } diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2Extension.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2Extension.java index 351bd586c..f90ef9e69 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2Extension.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2Extension.java @@ -201,7 +201,8 @@ private boolean isUpgrade() { return (deviceInfoService != null && !StringUtils.isNullOrEmpty(previousAppVersion) - && !previousAppVersion.equalsIgnoreCase(deviceInfoService.getApplicationVersion())); + && !previousAppVersion.equalsIgnoreCase( + LifecycleUtil.getV2AppVersion(deviceInfoService))); } /** Persist the application version into datastore */ @@ -210,7 +211,7 @@ private void persistAppVersion() { if (dataStore != null && deviceInfoService != null) { dataStore.setString( LifecycleV2Constants.DataStoreKeys.LAST_APP_VERSION, - deviceInfoService.getApplicationVersion()); + LifecycleUtil.getV2AppVersion(deviceInfoService)); } } diff --git a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java index 78498b349..f027e306b 100644 --- a/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java +++ b/code/lifecycle/src/main/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2MetricsBuilder.java @@ -13,7 +13,6 @@ import com.adobe.marketing.mobile.services.DeviceInforming; import com.adobe.marketing.mobile.services.Log; -import com.adobe.marketing.mobile.util.StringUtils; import java.util.Date; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -134,7 +133,7 @@ private XDMLifecycleApplication computeAppLaunchData( xdmApplicationInfoLaunch.setName(deviceInfoService.getApplicationName()); xdmApplicationInfoLaunch.setId(deviceInfoService.getApplicationPackageName()); - xdmApplicationInfoLaunch.setVersion(getAppVersion()); + xdmApplicationInfoLaunch.setVersion(LifecycleUtil.getV2AppVersion(deviceInfoService)); xdmApplicationInfoLaunch.setLanguage( LifecycleUtil.formatLocaleXDM(deviceInfoService.getActiveLocale())); @@ -239,28 +238,6 @@ private XDMLifecycleDevice computeDeviceData() { return xdmDeviceInfo; } - /** - * Returns the application version in the format appVersion (versionCode). Example: 2.3 (10) - * - * @return the app version as a {@link String} formatted in the specified format. - */ - private String getAppVersion() { - if (deviceInfoService == null) { - return null; - } - - final String applicationVersion = deviceInfoService.getApplicationVersion(); - final String applicationVersionCode = deviceInfoService.getApplicationVersionCode(); - return String.format( - "%s%s", - !StringUtils.isNullOrEmpty(applicationVersion) - ? String.format("%s", applicationVersion) - : "", - !StringUtils.isNullOrEmpty(applicationVersionCode) - ? String.format(" (%s)", applicationVersionCode) - : ""); - } - /** * Computes the session length based on the previous app session launch and close timestamp. The * returned session length is in seconds. If the session length is larger than can be diff --git a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2ExtensionTest.java b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2ExtensionTest.java index 8109adcea..adf6b6441 100644 --- a/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2ExtensionTest.java +++ b/code/lifecycle/src/test/java/com/adobe/marketing/mobile/lifecycle/LifecycleV2ExtensionTest.java @@ -149,6 +149,50 @@ public void testStart_happy_regularLaunch() { .buildAppCloseXDMData(anyLong(), anyLong(), anyLong(), anyBoolean()); } + @Test + public void testStart_onUpgrade() { + mockPersistence( + timestampOneHourEarlierInMilliseconds, + timestampTenMinEarlierInMilliseconds, + timestampTenMinEarlierInMilliseconds, + true); + lifecycleV2 = + new LifecycleV2Extension( + lifecycleDataStore, mockDeviceInfoService, mockBuilder, extensionApi); + + Map additionalContextData = new HashMap<>(); + additionalContextData.put("TEST_KEY1", "TEXT_VAL1"); + + Map eventData = new HashMap<>(); + eventData.put(EVENT_DATA_KEY_ADDITIONAL_CONTEXT_DATA, additionalContextData); + + Event testEvent = createLifecycleEvent(eventData, currentTimestampInMilliseconds); + lifecycleV2.start(testEvent, false); + + ArgumentCaptor appStartTimestampCaptor = ArgumentCaptor.forClass(Long.class); + verify(lifecycleDataStore, times(1)) + .setLong( + eq(DATASTORE_KEY_APP_START_TIMESTAMP_MILLIS), + appStartTimestampCaptor.capture()); + assertEquals( + currentTimestampInMilliseconds, appStartTimestampCaptor.getValue().longValue()); + + ArgumentCaptor launchTimestampCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor isInstallCaptor = ArgumentCaptor.forClass(Boolean.class); + ArgumentCaptor isUpgradeCaptor = ArgumentCaptor.forClass(Boolean.class); + verify(mockBuilder, times(1)) + .buildAppLaunchXDMData( + launchTimestampCaptor.capture(), + isInstallCaptor.capture(), + isUpgradeCaptor.capture()); + assertEquals(currentTimestampInMilliseconds, launchTimestampCaptor.getValue().longValue()); + assertFalse(isInstallCaptor.getValue()); + assertTrue(isUpgradeCaptor.getValue()); + + verify(mockBuilder, never()) + .buildAppCloseXDMData(anyLong(), anyLong(), anyLong(), anyBoolean()); + } + @Test public void testStart_consecutiveStartEvents_updatesOnlyFirstTime() { mockPersistence( @@ -517,12 +561,13 @@ private void mockPersistence( when(lifecycleDataStore.getLong(eq(DATASTORE_KEY_INSTALL_DATE), anyLong())) .thenReturn(timestampOneDayEarlierMilliseconds); - when(lifecycleDataStore.getString(eq(DATASTORE_KEY_LAST_APP_VERSION), anyString())) - .thenReturn("1.0"); - if (!isUpgrade) { + if (isUpgrade) { when(lifecycleDataStore.getString(eq(DATASTORE_KEY_LAST_APP_VERSION), anyString())) .thenReturn("1.1"); + } else { + when(lifecycleDataStore.getString(eq(DATASTORE_KEY_LAST_APP_VERSION), anyString())) + .thenReturn("1.1 (12345)"); } } } From f332b82adbbbbc1cf9591ea8dff842285bb3031a Mon Sep 17 00:00:00 2001 From: GeorgCantor Date: Wed, 15 May 2024 23:50:47 +0300 Subject: [PATCH 22/30] Update MapExtensions.kt --- .../com/adobe/marketing/mobile/internal/util/MapExtensions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt index 2af23a1b4..51a4f3d77 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/util/MapExtensions.kt @@ -136,8 +136,8 @@ private fun serializeKeyValuePair(key: String?, value: String?): String? { * @param delimiter the `String` to be used as the delimiter between all elements * @return [String] containing the elements joined by delimiters */ -private fun join(elements: Iterable<*>, delimiter: String?): String { - return elements.joinToString(delimiter ?: ",") +private fun join(elements: Iterable<*>, delimiter: String): String { + return elements.joinToString(delimiter) } /** From 4e540fe7e8624bd84f0e5d70a4e9ca55ba8afe0c Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Thu, 16 May 2024 21:15:26 -0700 Subject: [PATCH 23/30] Clear analytics push sync flag when processing reset identities and privacy opt-out events. --- .../com/adobe/marketing/mobile/identity/IdentityExtension.java | 1 + 1 file changed, 1 insertion(+) diff --git a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java index 8d0115518..7182c565c 100644 --- a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java +++ b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java @@ -2443,6 +2443,7 @@ private void clearIdentifiers() { if (namedCollection != null) { namedCollection.remove(DataStoreKeys.AID_SYNCED_KEY); namedCollection.remove(DataStoreKeys.PUSH_ENABLED); + namedCollection.remove(DataStoreKeys.ANALYTICS_PUSH_SYNC); } } From 873e81baf600e72dfcc3d857c0cbfec4f4d9acd2 Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Thu, 16 May 2024 21:15:58 -0700 Subject: [PATCH 24/30] Add unit tests to verify reset identities and privacy opt-out clears identifiers. --- .../mobile/identity/IdentityExtensionTests.kt | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityExtensionTests.kt b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityExtensionTests.kt index 5edf17735..7c5dded67 100644 --- a/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityExtensionTests.kt +++ b/code/identity/src/test/java/com/adobe/marketing/mobile/identity/IdentityExtensionTests.kt @@ -28,6 +28,7 @@ import com.adobe.marketing.mobile.services.ServiceProvider import com.adobe.marketing.mobile.util.DataReader import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue @@ -37,6 +38,7 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.never @@ -636,6 +638,79 @@ class IdentityExtensionTests { verify(spiedIdentityExtension, never()).processIdentityRequest(any()) } + @Test + fun `handleIdentityRequestReset() - clears identifiers`() { + val identityExtension = initializeSpiedIdentityExtension() + identityExtension.latestValidConfig = ConfigurationSharedStateIdentity( + mapOf( + "experienceCloud.org" to "orgid", + "global.privacy" to "optunknown" + ) + ) + + identityExtension.mid = "test-mid" + + val persistedData = capturePersistedData() + val clearedPersistedDataList = captureRemovedPersistedData() + + identityExtension.handleIdentityRequestReset( + Event.Builder("event", EventType.IDENTITY, EventSource.REQUEST_RESET).build() + ) + + // Verify expected keys passed to NamedCollection.remove() + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PERSISTED_MID")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_ADVERTISING_IDENTIFIER")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PUSH_IDENTIFIER")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_VISITORID_IDS")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PERSISTED_MID_HINT")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PERSISTED_MID_BLOB")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_AID_SYNCED")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PUSH_ENABLED")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_ANALYTICS_PUSH_SYNC")) + + // Verify new ECID set to persistence + assertEquals(1, persistedData.size) + assertNotNull(persistedData["ADOBEMOBILE_PERSISTED_MID"]) + assertNotEquals("test-mid", persistedData["ADOBEMOBILE_PERSISTED_MID"]) + assertEquals(persistedData["ADOBEMOBILE_PERSISTED_MID"], identityExtension.mid) + } + + @Test + fun `processPrivacyChange() - on optedout clears identifiers`() { + val identityExtension = initializeSpiedIdentityExtension() + identityExtension.latestValidConfig = ConfigurationSharedStateIdentity( + mapOf( + "experienceCloud.org" to "orgid", + "global.privacy" to "optunknown" + ) + ) + + identityExtension.mid = "test-mid" + + val persistedData = capturePersistedData() + val clearedPersistedDataList = captureRemovedPersistedData() + + identityExtension.processPrivacyChange( + Event.Builder("event", EventType.CONFIGURATION, EventSource.RESPONSE_CONTENT).build(), + mapOf("global.privacy" to "optedout") + ) + + // Verify expected keys passed to NamedCollection.remove() + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PERSISTED_MID")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_ADVERTISING_IDENTIFIER")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PUSH_IDENTIFIER")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_VISITORID_IDS")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PERSISTED_MID_HINT")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PERSISTED_MID_BLOB")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_AID_SYNCED")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_PUSH_ENABLED")) + assertTrue(clearedPersistedDataList.contains("ADOBEMOBILE_ANALYTICS_PUSH_SYNC")) + + // Verify ECID is not set + assertTrue(persistedData.isEmpty()) + assertNull(identityExtension.mid) + } + // ============================================================================================================== // void handleAnalyticsResponseIdentity() // ============================================================================================================== @@ -1862,4 +1937,35 @@ class IdentityExtensionTests { } return customerIdString.toString() } + + private fun capturePersistedData(): Map { + val persistedData = mutableMapOf() + doAnswer { + val key = it.arguments[0] as String + val value = it.arguments[1] as String + persistedData[key] = value + }.`when`(mockedNamedCollection).setString( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString() + ) + doAnswer { + val key = it.arguments[0] as String + val value = it.arguments[1] as Boolean + persistedData[key] = value + }.`when`(mockedNamedCollection).setBoolean( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyBoolean() + ) + return persistedData + } + + private fun captureRemovedPersistedData(): List { + val removedData = mutableListOf() + doAnswer { + val key = it.arguments[0] as String + removedData.add(key) + }.`when`(mockedNamedCollection).remove(ArgumentMatchers.anyString()) + + return removedData + } } From 173d1dff87019fbc6b4eebc6095a8bbe8b9106b5 Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Fri, 17 May 2024 11:13:10 -0700 Subject: [PATCH 25/30] Remove unused 'pushEnabled' variable from Identity --- .../mobile/identity/IdentityExtension.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java index 7182c565c..a2f4aa0e1 100644 --- a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java +++ b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java @@ -80,7 +80,6 @@ public final class IdentityExtension extends Extension { private static final String LOG_SOURCE = "IdentityExtension"; private HitQueuing hitQueue; - private static boolean pushEnabled = false; private static final Object pushEnabledMutex = new Object(); @VisibleForTesting ConfigurationSharedStateIdentity latestValidConfig; private final NamedCollection namedCollection; @@ -1673,7 +1672,7 @@ private void updateAdvertisingIdentifier(final String adid) { } /** - * Updates the {@link #pushEnabled} field and dispatches an event to generate a corresponding + * Updates the persisted {@code ADOBEMOBILE_PUSH_ENABLED} field and dispatches an event to generate a corresponding * Analytics request * * @param isEnabled whether the user is opted in to receive push notifications @@ -1726,14 +1725,12 @@ private boolean isPushEnabled() { return false; } - pushEnabled = namedCollection.getBoolean(DataStoreKeys.PUSH_ENABLED, false); + return namedCollection.getBoolean(DataStoreKeys.PUSH_ENABLED, false); } - - return pushEnabled; } /** - * Updates the {@link #pushEnabled} flag in DataStore with the provided value. + * Updates the persisted {@code ADOBEMOBILE_PUSH_ENABLED} flag in DataStore with the provided value. * * @param enabled new push status value to be updated */ @@ -1741,6 +1738,11 @@ private void setPushStatus(final boolean enabled) { synchronized (pushEnabledMutex) { if (namedCollection != null) { namedCollection.setBoolean(DataStoreKeys.PUSH_ENABLED, enabled); + Log.trace( + IdentityConstants.LOG_TAG, + LOG_SOURCE, + "setPushStatus : Push notifications status is now: " + + (enabled ? "Enabled" : "Disabled")); } else { Log.trace( IdentityConstants.LOG_TAG, @@ -1748,13 +1750,6 @@ private void setPushStatus(final boolean enabled) { "setPushStatus : Unable to update push flag because the" + " LocalStorageService was not available."); } - - pushEnabled = enabled; - Log.trace( - IdentityConstants.LOG_TAG, - LOG_SOURCE, - "setPushStatus : Push notifications status is now: " - + (pushEnabled ? "Enabled" : "Disabled")); } } From 476cea1833cb9af2d459db19aa494dc248fa4246 Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Fri, 17 May 2024 11:16:04 -0700 Subject: [PATCH 26/30] Consolidate calls to 'isPushEnabled' to reduce I/O reads --- .../adobe/marketing/mobile/identity/IdentityExtension.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java index a2f4aa0e1..7aaef6d37 100644 --- a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java +++ b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java @@ -1086,7 +1086,9 @@ void updatePushIdentifier(final String pushId) { return; } - if (pushId == null && !isPushEnabled()) { + final boolean pushEnabled = isPushEnabled(); + + if (pushId == null && !pushEnabled) { changePushStatusAndHitAnalytics(false); Log.debug( IdentityConstants.LOG_TAG, @@ -1094,7 +1096,7 @@ void updatePushIdentifier(final String pushId) { "updatePushIdentifier : First time sending a.push.optin false"); } else if (pushId == null) { // push is enabled changePushStatusAndHitAnalytics(false); - } else if (!isPushEnabled()) { // push ID is not null + } else if (!pushEnabled) { // push ID is not null changePushStatusAndHitAnalytics(true); } } From 7de156b4909d15911d6d8e0f23f74117065d44b8 Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Fri, 17 May 2024 11:40:33 -0700 Subject: [PATCH 27/30] Use mutex when updating persisted push enabled flag. --- .../adobe/marketing/mobile/identity/IdentityExtension.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java index 7aaef6d37..b42a64d0c 100644 --- a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java +++ b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java @@ -2439,8 +2439,10 @@ private void clearIdentifiers() { if (namedCollection != null) { namedCollection.remove(DataStoreKeys.AID_SYNCED_KEY); - namedCollection.remove(DataStoreKeys.PUSH_ENABLED); namedCollection.remove(DataStoreKeys.ANALYTICS_PUSH_SYNC); + synchronized (pushEnabledMutex) { + namedCollection.remove(DataStoreKeys.PUSH_ENABLED); + } } } From d6e91e9273c98567db507357f5e4dc6b930bce64 Mon Sep 17 00:00:00 2001 From: Kevin Lind Date: Fri, 17 May 2024 12:49:54 -0700 Subject: [PATCH 28/30] Fix formatting in IdentityExtension --- .../adobe/marketing/mobile/identity/IdentityExtension.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java index b42a64d0c..8b8345b9d 100644 --- a/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java +++ b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java @@ -1674,8 +1674,8 @@ private void updateAdvertisingIdentifier(final String adid) { } /** - * Updates the persisted {@code ADOBEMOBILE_PUSH_ENABLED} field and dispatches an event to generate a corresponding - * Analytics request + * Updates the persisted {@code ADOBEMOBILE_PUSH_ENABLED} field and dispatches an event to + * generate a corresponding Analytics request * * @param isEnabled whether the user is opted in to receive push notifications */ @@ -1732,7 +1732,8 @@ private boolean isPushEnabled() { } /** - * Updates the persisted {@code ADOBEMOBILE_PUSH_ENABLED} flag in DataStore with the provided value. + * Updates the persisted {@code ADOBEMOBILE_PUSH_ENABLED} flag in DataStore with the provided + * value. * * @param enabled new push status value to be updated */ From e55042537f475487fdbfd5d2e179d0b0e3906083 Mon Sep 17 00:00:00 2001 From: Yansong Yang Date: Thu, 23 May 2024 11:53:13 -0500 Subject: [PATCH 29/30] Catch exceptions when retrieving data from Activity (#669) --- .../mobile/internal/DataMarshallerTests.kt | 24 ++++++++++++++++++- .../mobile/internal/DataMarshaller.kt | 10 +++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/internal/DataMarshallerTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/internal/DataMarshallerTests.kt index e8f2adcf6..8a6d81ef2 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/internal/DataMarshallerTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/internal/DataMarshallerTests.kt @@ -21,6 +21,7 @@ import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.io.Serializable @RunWith(AndroidJUnit4::class) class DataMarshallerTests { @@ -176,6 +177,23 @@ class DataMarshallerTests { @Test fun marshalInvalidUrl_NoCrash() { + val throwsException = ObjectThrowsOnToString() + val intent = + Intent(ApplicationProvider.getApplicationContext(), TestActivity::class.java).apply { + putExtra("key", "value") + putExtra("exceptionKey", throwsException) + } + + val activity = activityTestRule.launchActivity(intent) + val result = DataMarshaller.marshal(activity) + assertEquals( + mapOf("key" to "value"), + result + ) + } + + @Test + fun marshal_whenBundleThrowException_NoCrash() { val intent = Intent(ApplicationProvider.getApplicationContext(), TestActivity::class.java).apply { data = Uri.parse("abc:abc") @@ -188,7 +206,11 @@ class DataMarshallerTests { result ) } - + private class ObjectThrowsOnToString : Serializable { + override fun toString(): String { + throw IllegalStateException("This is a test exception") + } + } companion object { const val LEGACY_PUSH_MESSAGE_ID = "adb_m_id" const val PUSH_MESSAGE_ID_KEY = "pushmessageid" diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/DataMarshaller.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/DataMarshaller.kt index 6ee449780..293e992b1 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/DataMarshaller.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/DataMarshaller.kt @@ -70,9 +70,13 @@ internal object DataMarshaller { NOTIFICATION_IDENTIFIER_KEY -> LOCAL_NOTIFICATION_ID_KEY else -> key } - val value = extraBundle[key] - if (value?.toString()?.isNotEmpty() == true) { - marshalledData[newKey] = value + try { + val value = extraBundle[key] + if (value?.toString()?.isNotEmpty() == true) { + marshalledData[newKey] = value + } + } catch (e: Exception) { + Log.error(CoreConstants.LOG_TAG, LOG_TAG, "Failed to retrieve data (key = $key) from Activity, error is: ${e.message}") } } From bc08e97daae3f6bcf299ae041499f41327f64b30 Mon Sep 17 00:00:00 2001 From: praveek Date: Thu, 23 May 2024 20:04:58 +0000 Subject: [PATCH 30/30] Update versions [Core-3.0.1] [Identity-3.0.1] [Lifecycle-3.0.1] --- .../com/adobe/marketing/mobile/internal/CoreConstants.kt | 2 +- code/gradle.properties | 6 +++--- .../src/phone/java/com/adobe/marketing/mobile/Identity.java | 2 +- .../phone/java/com/adobe/marketing/mobile/Lifecycle.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt index 9f5a7bd9a..7e39e5a02 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt @@ -13,7 +13,7 @@ package com.adobe.marketing.mobile.internal internal object CoreConstants { const val LOG_TAG = "MobileCore" - const val VERSION = "3.0.0" + const val VERSION = "3.0.1" object EventDataKeys { /** diff --git a/code/gradle.properties b/code/gradle.properties index 368451df0..3fa0ad0e9 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -5,7 +5,7 @@ android.useAndroidX=true #Maven artifacts #Core extension -coreExtensionVersion=3.0.0 +coreExtensionVersion=3.0.1 coreExtensionName=core coreMavenRepoName=AdobeMobileCoreSdk coreMavenRepoDescription=Android Core Extension for Adobe Mobile Marketing @@ -15,12 +15,12 @@ signalExtensionName=signal signalMavenRepoName=AdobeMobileSignalSdk signalMavenRepoDescription=Android Signal Extension for Adobe Mobile Marketing #Lifecycle extension -lifecycleExtensionVersion=3.0.0 +lifecycleExtensionVersion=3.0.1 lifecycleExtensionName=lifecycle lifecycleMavenRepoName=AdobeMobileLifecycleSdk lifecycleMavenRepoDescription=Android Lifecycle Extension for Adobe Mobile Marketing #Identity extension -identityExtensionVersion=3.0.0 +identityExtensionVersion=3.0.1 identityExtensionName=identity identityMavenRepoName=AdobeMobileIdentitySdk identityMavenRepoDescription=Android Identity Extension for Adobe Mobile Marketing diff --git a/code/identity/src/phone/java/com/adobe/marketing/mobile/Identity.java b/code/identity/src/phone/java/com/adobe/marketing/mobile/Identity.java index 380084165..a57dc7aee 100644 --- a/code/identity/src/phone/java/com/adobe/marketing/mobile/Identity.java +++ b/code/identity/src/phone/java/com/adobe/marketing/mobile/Identity.java @@ -26,7 +26,7 @@ public class Identity { private static final String CLASS_NAME = "Identity"; - private static final String EXTENSION_VERSION = "3.0.0"; + private static final String EXTENSION_VERSION = "3.0.1"; private static final String REQUEST_IDENTITY_EVENT_NAME = "IdentityRequestIdentity"; private static final int PUBLIC_API_TIME_OUT_MILLISECOND = 500; // ms private static final String LOG_TAG = "Identity"; diff --git a/code/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java b/code/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java index 6cbe813e2..6cc441a40 100644 --- a/code/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java +++ b/code/lifecycle/src/phone/java/com/adobe/marketing/mobile/Lifecycle.java @@ -15,7 +15,7 @@ public class Lifecycle { - private static final String EXTENSION_VERSION = "3.0.0"; + private static final String EXTENSION_VERSION = "3.0.1"; public static final Class EXTENSION = LifecycleExtension.class;