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 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)"); } } }