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..90c476c68 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/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/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()
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..f17862128
--- /dev/null
+++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/message/views/MessageScreenOrientationTests.kt
@@ -0,0 +1,251 @@
+/*
+ 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
+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()
+ }
+}
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 8d49025fa..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
@@ -25,17 +24,16 @@ 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
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,9 +248,13 @@ 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).performGesture {
+ composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performTouchInput {
// create a swipe right gesture
swipeWithVelocity(
start = Offset(100f, 10f),
@@ -271,10 +289,14 @@ 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).performGesture {
+ composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_CONTENT).performTouchInput {
// create a swipe right gesture
swipeWithVelocity(
start = Offset(100f, 10f),
@@ -309,24 +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).performGesture {
- // 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).performGesture {
- click(
- Offset(100f, 10f)
- )
- }
+ composeTestRule.onNodeWithTag(MessageTestTags.MESSAGE_BACKDROP).assertIsDisplayed()
+ .performTouchInput {
+ click(
+ Offset(100f, 10f)
+ )
+ }
composeTestRule.waitForIdle()
assertTrue(onCreatedCalled)
@@ -337,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(
@@ -359,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)
@@ -370,7 +403,11 @@ class MessageScreenTests {
restorationTester.emulateSavedInstanceStateRestore()
composeTestRule.waitForIdle()
- validateMessageAppeared(false)
+ validateMessageAppeared(
+ composeTestRule = composeTestRule,
+ withBackdrop = false,
+ clipped = false
+ )
}
// ----------------------------------------------------------------------------------------------
@@ -393,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() }
@@ -413,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)
}
@@ -443,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(
@@ -462,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 =
@@ -474,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)
+ )
}
}
@@ -503,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(
@@ -522,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(
@@ -563,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(
@@ -582,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(
@@ -623,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(
@@ -642,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(
@@ -683,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(
@@ -702,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(
@@ -744,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(
@@ -763,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(
@@ -809,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(
@@ -828,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
@@ -878,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(
@@ -897,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
@@ -947,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(
@@ -966,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
@@ -1016,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(
@@ -1035,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
@@ -1085,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(
@@ -1104,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
@@ -1155,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(
@@ -1174,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
@@ -1225,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(
@@ -1244,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
@@ -1295,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(
@@ -1314,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
@@ -1367,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(
@@ -1386,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
@@ -1425,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)
@@ -1456,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))
- }
}
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/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}")
}
}
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..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
@@ -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,7 @@ 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 $ex"
)
}
}
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..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,18 +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 {
- 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()
+private fun join(elements: Iterable<*>, delimiter: String): String {
+ return elements.joinToString(delimiter)
}
/**
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 }
}
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
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..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
@@ -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
+ private 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/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/InAppMessagePresentable.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/InAppMessagePresentable.kt
index 4d4cb8a2a..34d680ba9 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
@@ -45,7 +46,8 @@ internal class InAppMessagePresentable(
inAppMessage,
presentationUtilityProvider,
presentationDelegate,
- appLifecycleProvider
+ appLifecycleProvider,
+ mainScope
) {
companion object {
@@ -108,9 +110,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 +171,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/ui/message/views/MessageContent.kt b/code/core/src/phone/java/com/adobe/marketing/mobile/services/ui/message/views/MessageContent.kt
index cbefffd22..47379a4b4 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,15 +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
import androidx.compose.ui.platform.LocalConfiguration
@@ -45,20 +40,16 @@ import java.nio.charset.StandardCharsets
@Composable
internal fun MessageContent(
inAppMessageSettings: InAppMessageSettings,
- onCreated: (WebView) -> Unit,
- gestureTracker: GestureTracker
+ onCreated: (WebView) -> Unit
) {
- // 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 }
-
- // Swipe/Drag variables
- val offsetX = remember { mutableStateOf(0f) }
- val offsetY = remember { mutableStateOf(0f) }
- val dragVelocity = remember { mutableStateOf(0f) }
+ val heightDp: Dp = ((currentConfiguration.screenHeightDp * inAppMessageSettings.height) / 100).dp
+ val widthDp: Dp = ((currentConfiguration.screenWidthDp * inAppMessageSettings.width) / 100).dp
AndroidView(
factory = {
@@ -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..b20be3062 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
@@ -54,22 +58,27 @@ 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
+ )
+
+ val allowGestures = remember { inAppMessageSettings.gestureMap.isNotEmpty() }
+ val offsetX = remember { mutableStateOf(0f) }
+ val offsetY = remember { mutableStateOf(0f) }
+ val dragVelocity = remember { mutableStateOf(0f) }
AnimatedVisibility(
visibleState = visibility,
@@ -92,8 +101,46 @@ 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)) {
- MessageContent(inAppMessageSettings, onCreated, gestureTracker)
+ 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)
}
// This is a one-time effect that will be called when this composable is completely removed from the composition
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/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)
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())
+ }
}
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/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java b/code/identity/src/main/java/com/adobe/marketing/mobile/identity/IdentityExtension.java
index 36e8d4bf5..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
@@ -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;
@@ -499,18 +498,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
@@ -1097,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,
@@ -1105,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);
}
}
@@ -1683,8 +1674,8 @@ private void updateAdvertisingIdentifier(final String adid) {
}
/**
- * Updates the {@link #pushEnabled} 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
*/
@@ -1736,14 +1727,13 @@ 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
*/
@@ -1751,6 +1741,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,
@@ -1758,13 +1753,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"));
}
}
@@ -1857,17 +1845,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 +2429,24 @@ 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.ANALYTICS_PUSH_SYNC);
+ synchronized (pushEnabledMutex) {
+ namedCollection.remove(DataStoreKeys.PUSH_ENABLED);
+ }
+ }
+ }
+
@VisibleForTesting
String getMid() {
return mid;
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/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
+ }
}
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..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
@@ -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
@@ -35,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
@@ -52,6 +56,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 +2042,176 @@ class IdentityFunctionalTests {
countDownLatch.await()
}
+
+ @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 ->
+ assertEquals(1, invocation.arguments.size)
+ 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())
+
+ val persistedData = capturePersistedData()
+
+ val pushToken = "D52DB39EEE21395B2B67B895FC478301CE6E936D82521E095902A5E0F57EE0B3"
+
+ identityExtension.processIdentityRequest(
+ Event.Builder(
+ "event",
+ "com.adobe.eventType.generic.identity",
+ "com.adobe.eventSource.requestContent"
+ ).setEventData(
+ mapOf(
+ "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)
+ 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 ->
+ assertEquals(1, invocation.arguments.size)
+ 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())
+
+ val persistedData = capturePersistedData()
+
+ identityExtension.processIdentityRequest(
+ Event.Builder(
+ "event",
+ "com.adobe.eventType.generic.identity",
+ "com.adobe.eventSource.requestContent"
+ ).setEventData(
+ mapOf(
+ "pushidentifier" to null
+ )
+ ).build()
+ )
+
+ 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)
+ }
+ 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)
+ }
+
+ 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
+ }
}
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/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 extends Extension> EXTENSION = LifecycleExtension.class;
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)");
}
}
}