From 8f0b5c62d27268b1037c3857a339ba6fd0538e5f Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 21 Jan 2025 12:59:29 -0800 Subject: [PATCH 1/8] PLATIR-45069: Add theme overrides for AEP Presentables --- code/core/src/main/res/values/styles.xml | 10 ++++++ .../services/ui/alert/AlertPresentable.kt | 2 +- .../services/ui/common/AEPPresentable.kt | 33 +++++++++++++++++++ .../FloatingButtonPresentable.kt | 2 +- .../ui/floatingbutton/views/FloatingButton.kt | 1 + .../ui/message/InAppMessagePresentable.kt | 2 +- 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 code/core/src/main/res/values/styles.xml diff --git a/code/core/src/main/res/values/styles.xml b/code/core/src/main/res/values/styles.xml new file mode 100644 index 000000000..573648e60 --- /dev/null +++ b/code/core/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + + + 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 1ff171fce..66875b2e2 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 @@ -44,7 +44,7 @@ internal class AlertPresentable( mainScope ) { override fun getContent(activityContext: Context): ComposeView { - return ComposeView(activityContext).apply { + return ComposeView(getThemedContext(activityContext)).apply { setContent { AlertScreen( presentationStateManager = presentationStateManager, 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 034c146b6..e585824cd 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 @@ -13,11 +13,13 @@ package com.adobe.marketing.mobile.services.ui.common import android.app.Activity import android.content.Context +import android.view.ContextThemeWrapper import android.view.View import android.view.ViewGroup import androidx.annotation.MainThread import androidx.annotation.VisibleForTesting import androidx.compose.ui.platform.ComposeView +import com.adobe.marketing.mobile.core.R import com.adobe.marketing.mobile.internal.util.ActivityCompatOwnerUtils import com.adobe.marketing.mobile.services.Log import com.adobe.marketing.mobile.services.ServiceConstants @@ -291,6 +293,37 @@ internal abstract class AEPPresentable> : onAnimationComplete() } + /** + * Returns a themed context that applies overrides to the base context theme + * to ensure that the presentable UI is consistent with the rest of the application while + * ensuring that specific attributes like view background are not inherited from the base theme. + */ + protected fun getThemedContext(context: Context): Context { + // Theme overrides are supported only for API level 23 and above. For lower API levels, + // return the base context as is. + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) { + return context + } + + try { + val newTheme = context.resources.newTheme() + // Apply the base theme attributes to the new theme + newTheme.setTo(context.theme) + val themedContext = ContextThemeWrapper(context, newTheme) + // Apply the override theme to the themed context + themedContext.theme.applyStyle(R.style.AepSdkUiService_OverrideTheme, true) + return themedContext + } catch (e: Exception) { + Log.error( + ServiceConstants.LOG_TAG, + LOG_SOURCE, + "Error while creating themed context", + e + ) + } + return context + } + /** * Fetches the [ComposeView] associated with the presentable. This ComposeView is used to * render the UI of the presentable by attaching it to the content view of the activity. 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 0826a0de8..2deccd5ad 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 @@ -62,7 +62,7 @@ internal class FloatingButtonPresentable( } override fun getContent(activityContext: Context): ComposeView { - return ComposeView(activityContext).apply { + return ComposeView(getThemedContext(activityContext)).apply { setContent { FloatingButtonScreen( presentationStateManager = presentationStateManager, 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 fecb21432..733f7ea53 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 @@ -90,6 +90,7 @@ internal fun FloatingButton( modifier = Modifier .height(heightDp.value) .width(widthDp.value) + .background(Color.Transparent) .testTag(FloatingButtonTestTags.FLOATING_BUTTON_AREA) ) { FloatingActionButton( 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 34d680ba9..cb0d00776 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 @@ -77,7 +77,7 @@ internal class InAppMessagePresentable( * @param activityContext the context of the activity */ override fun getContent(activityContext: Context): ComposeView { - return ComposeView(activityContext).apply { + return ComposeView(getThemedContext(activityContext)).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT From 7ddb0b3ba5cbedd6fce59ae0eea3421e794eea56 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 4 Feb 2025 13:12:01 -0800 Subject: [PATCH 2/8] Restrict theme override to floating button --- .../FloatingButtonPresentableTests.kt | 70 +++++++++++++++++++ .../services/ui/alert/AlertPresentable.kt | 2 +- .../ui/message/InAppMessagePresentable.kt | 2 +- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt new file mode 100644 index 000000000..d3beb0156 --- /dev/null +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt @@ -0,0 +1,70 @@ +import android.content.Context +import android.view.ContextThemeWrapper +import androidx.test.core.app.ApplicationProvider +import com.adobe.marketing.mobile.core.R +import com.adobe.marketing.mobile.services.ui.FloatingButton +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 com.adobe.marketing.mobile.services.ui.floatingbutton.FloatingButtonPresentable +import com.adobe.marketing.mobile.services.ui.floatingbutton.FloatingButtonSettings +import com.adobe.marketing.mobile.services.ui.floatingbutton.FloatingButtonViewModel +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +class FloatingButtonPresentableTests { + @Mock + private lateinit var mockFloatingButton: FloatingButton + + @Mock + private lateinit var mockFloatingButtonSettings: FloatingButtonSettings + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + // Mock the settings and initial graphic + `when`(mockFloatingButton.settings).thenReturn(mockFloatingButtonSettings) + `when`(mockFloatingButtonSettings.initialGraphic).thenReturn(mock(android.graphics.Bitmap::class.java)) + } + + @Test + fun test_getContentReturnsComposeViewWithThemeWrapper() = runTest { + // setup + val context = ApplicationProvider.getApplicationContext() + val floatingButtonViewModel = mock(FloatingButtonViewModel::class.java) + val presentationDelegate = mock(PresentationDelegate::class.java) + val presentationUtilityProvider = mock(PresentationUtilityProvider::class.java) + val appLifecycleProvider = mock(AppLifecycleProvider::class.java) + val mainScope = CoroutineScope(Dispatchers.Main) + + val presentable = FloatingButtonPresentable( + mockFloatingButton, + floatingButtonViewModel, + presentationDelegate, + presentationUtilityProvider, + appLifecycleProvider, + mainScope + ) + + // test + val composeView = presentable.getContent(context) + + // verify + assertTrue(composeView.context is ContextThemeWrapper) + val themedContext = composeView.context as ContextThemeWrapper + val theme = themedContext.theme + // get android.background from the theme + val background = theme.obtainStyledAttributes(intArrayOf(android.R.attr.background)) + assertTrue(background.hasValue(0) + && background.peekValue(0).resourceId == android.R.color.transparent) + } +} \ No newline at end of file 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 66875b2e2..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 @@ -44,7 +44,7 @@ internal class AlertPresentable( mainScope ) { override fun getContent(activityContext: Context): ComposeView { - return ComposeView(getThemedContext(activityContext)).apply { + return ComposeView(activityContext).apply { setContent { AlertScreen( presentationStateManager = presentationStateManager, 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 cb0d00776..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 @@ -77,7 +77,7 @@ internal class InAppMessagePresentable( * @param activityContext the context of the activity */ override fun getContent(activityContext: Context): ComposeView { - return ComposeView(getThemedContext(activityContext)).apply { + return ComposeView(activityContext).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT From 47bb4e7710d5c092938a7b408fea95544e8fc396 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 4 Feb 2025 13:15:09 -0800 Subject: [PATCH 3/8] Formatting fixes --- .../FloatingButtonPresentableTests.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt index d3beb0156..e3c0438b7 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt @@ -1,3 +1,14 @@ +/* + Copyright 2025 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. +*/ + import android.content.Context import android.view.ContextThemeWrapper import androidx.test.core.app.ApplicationProvider @@ -16,8 +27,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations class FloatingButtonPresentableTests { @@ -64,7 +75,9 @@ class FloatingButtonPresentableTests { val theme = themedContext.theme // get android.background from the theme val background = theme.obtainStyledAttributes(intArrayOf(android.R.attr.background)) - assertTrue(background.hasValue(0) - && background.peekValue(0).resourceId == android.R.color.transparent) + assertTrue( + background.hasValue(0) && + background.peekValue(0).resourceId == android.R.color.transparent + ) } -} \ No newline at end of file +} From cab324e1cc4c4883ad56523ead93dc5e47fcc229 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 18 Feb 2025 16:23:30 -0800 Subject: [PATCH 4/8] Add instrumentation tests for AEPPresentable --- .../services/ui/common/AEPPresentableTests.kt | 176 ++++++++++++++++++ .../FloatingButtonPresentableTests.kt | 9 +- .../services/ui/common/AEPPresentable.kt | 3 +- 3 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt new file mode 100644 index 000000000..d1ce8aef6 --- /dev/null +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt @@ -0,0 +1,176 @@ +/* + Copyright 2025 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.common + +import android.content.Context +import android.view.ContextThemeWrapper +import androidx.compose.ui.platform.ComposeView +import androidx.test.core.app.ApplicationProvider +import com.adobe.marketing.mobile.internal.util.ActivityCompatOwnerUtils +import com.adobe.marketing.mobile.services.ui.InAppMessage +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 junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.CoroutineScope +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` + +class AEPPresentableTests { + + @Test + fun test_getThemedContextWhenNewThemeCreationFails() { + // setup + val context = mock(Context::class.java) + val presentationUtilityProvider = mock(PresentationUtilityProvider::class.java) + val presentationDelegate = mock(PresentationDelegate::class.java) + val appLifecycleProvider = mock(AppLifecycleProvider::class.java) + val presentationStateManager = mock(PresentationStateManager::class.java) + val activityCompatOwnerUtils = mock(ActivityCompatOwnerUtils::class.java) + val mainScope = mock(CoroutineScope::class.java) + val presentationObserver = mock(PresentationObserver::class.java) + val presentation = mock(InAppMessage::class.java) + + val presentable = SampleAEPPresentable( + presentation, + presentationUtilityProvider, + presentationDelegate, + appLifecycleProvider, + presentationStateManager, + activityCompatOwnerUtils, + mainScope, + presentationObserver + ) + + val resources = mock(android.content.res.Resources::class.java) + `when`(context.resources).thenReturn(resources) + `when`(resources.newTheme()).thenThrow(RuntimeException("Resources.newTheme() invocation failure.")) + + // test + val themedContext = presentable.getThemedContext(context) + + // verify + assertFalse(themedContext is ContextThemeWrapper) + assertEquals(context, themedContext) // No wrapping should occur if theme creation fails + } + + @Test + fun test_getThemedContextWhenApplyStyleFails() { + // setup + val context = mock(Context::class.java) + val presentationUtilityProvider = mock(PresentationUtilityProvider::class.java) + val presentationDelegate = mock(PresentationDelegate::class.java) + val appLifecycleProvider = mock(AppLifecycleProvider::class.java) + val presentationStateManager = mock(PresentationStateManager::class.java) + val activityCompatOwnerUtils = mock(ActivityCompatOwnerUtils::class.java) + val mainScope = mock(CoroutineScope::class.java) + val presentationObserver = mock(PresentationObserver::class.java) + val presentation = mock(InAppMessage::class.java) + + val presentable = SampleAEPPresentable( + presentation, + presentationUtilityProvider, + presentationDelegate, + appLifecycleProvider, + presentationStateManager, + activityCompatOwnerUtils, + mainScope, + presentationObserver + ) + + val resources = mock(android.content.res.Resources::class.java) + `when`(context.resources).thenReturn(resources) + val theme = mock(android.content.res.Resources.Theme::class.java) + `when`(resources.newTheme()).thenReturn(theme) + + `when`(theme.applyStyle(anyInt(), eq(true))).thenThrow(RuntimeException("Theme.applyStyle() invocation failure.")) + + // test + val themedContext = presentable.getThemedContext(context) + assertFalse(themedContext is ContextThemeWrapper) + assertEquals(context, themedContext) // No wrapping should occur if theme creation fails + } + + @Test + fun test_getThemedContextSucceedsWithThemeOverride() { + // setup + val context = ApplicationProvider.getApplicationContext() + val presentationUtilityProvider = mock(PresentationUtilityProvider::class.java) + val presentationDelegate = mock(PresentationDelegate::class.java) + val appLifecycleProvider = mock(AppLifecycleProvider::class.java) + val presentationStateManager = mock(PresentationStateManager::class.java) + val activityCompatOwnerUtils = mock(ActivityCompatOwnerUtils::class.java) + val mainScope = mock(CoroutineScope::class.java) + val presentationObserver = mock(PresentationObserver::class.java) + val presentation = mock(InAppMessage::class.java) + + val presentable = SampleAEPPresentable( + presentation, + presentationUtilityProvider, + presentationDelegate, + appLifecycleProvider, + presentationStateManager, + activityCompatOwnerUtils, + mainScope, + presentationObserver + ) + + // test + val themedContext = presentable.getThemedContext(context) + assertTrue(themedContext is ContextThemeWrapper) + val themedContextWrapper = themedContext as ContextThemeWrapper + val background = themedContextWrapper.theme.obtainStyledAttributes(intArrayOf(android.R.attr.background)) + assertTrue(background.hasValue(0)) + assertTrue(background.peekValue(0).resourceId == android.R.color.transparent) + } + + internal class SampleAEPPresentable( + private val presentation: InAppMessage, + presentationUtilityProvider: PresentationUtilityProvider, + presentationDelegate: PresentationDelegate?, + appLifecycleProvider: AppLifecycleProvider, + presentationStateManager: PresentationStateManager, + activityCompatOwnerUtils: ActivityCompatOwnerUtils, + mainScope: CoroutineScope, + presentationObserver: PresentationObserver, + ) : AEPPresentable( + presentation, + presentationUtilityProvider, + presentationDelegate, + appLifecycleProvider, + presentationStateManager, + activityCompatOwnerUtils, + mainScope, + presentationObserver + ) { + override fun getPresentation(): InAppMessage { + return presentation + } + + override fun getContent(activityContext: Context): ComposeView { + return ComposeView(activityContext) + } + + override fun gateDisplay(): Boolean { + return true + } + + override fun hasConflicts(visiblePresentations: List>): Boolean { + return false + } + } +} diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt index e3c0438b7..ebc32ba1f 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/floatingbutton/FloatingButtonPresentableTests.kt @@ -12,7 +12,6 @@ import android.content.Context import android.view.ContextThemeWrapper import androidx.test.core.app.ApplicationProvider -import com.adobe.marketing.mobile.core.R import com.adobe.marketing.mobile.services.ui.FloatingButton import com.adobe.marketing.mobile.services.ui.PresentationDelegate import com.adobe.marketing.mobile.services.ui.PresentationUtilityProvider @@ -75,9 +74,9 @@ class FloatingButtonPresentableTests { val theme = themedContext.theme // get android.background from the theme val background = theme.obtainStyledAttributes(intArrayOf(android.R.attr.background)) - assertTrue( - background.hasValue(0) && - background.peekValue(0).resourceId == android.R.color.transparent - ) + // Verify that the background attribute is added + assertTrue(background.hasValue(0)) + // Verify that the background attribute is transparent + assertTrue(background.peekValue(0).resourceId == android.R.color.transparent) } } 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 e585824cd..78ec8ab09 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 @@ -298,7 +298,8 @@ internal abstract class AEPPresentable> : * to ensure that the presentable UI is consistent with the rest of the application while * ensuring that specific attributes like view background are not inherited from the base theme. */ - protected fun getThemedContext(context: Context): Context { + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal fun getThemedContext(context: Context): Context { // Theme overrides are supported only for API level 23 and above. For lower API levels, // return the base context as is. if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) { From 93460baa3d6621fb4ca8862706e54c96c5a1a5da Mon Sep 17 00:00:00 2001 From: Praveen Date: Tue, 18 Feb 2025 20:50:50 -0800 Subject: [PATCH 5/8] Update api-reference.md (#743) * Update api-reference.md * Update api-reference.md --- Documentation/MobileCore/api-reference.md | 102 +++++++++++++++++++++- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/Documentation/MobileCore/api-reference.md b/Documentation/MobileCore/api-reference.md index 8fa9ea96b..8e9cce1ea 100644 --- a/Documentation/MobileCore/api-reference.md +++ b/Documentation/MobileCore/api-reference.md @@ -82,7 +82,7 @@ MobileCore.setWrapperType(WrapperType.REACT_NATIVE) ``` -#### Initializing MobileCore with Android Application instance +#### Setting Android Application instance Use the `setApplication` api to pass the Android Application instance to SDK. This allows the SDK to monitor the lifecycle of your Android application. @@ -113,7 +113,7 @@ class YourApp : Application() { ``` -#### Retrieving the registered Application +#### Retrieving the Android Application instance You can use the `getApplication()` api to get the Android Application instance that was previously set via `MobileCore.setApplication()` @@ -130,8 +130,104 @@ final Application app = MobileCore.getApplication(); val app = MobileCore.getApplication() ``` +#### Initializing the SDK by automatically registering all extensions and enabling Lifecycle data collection -#### Registering extensions and starting the SDK +> [!NOTE] +> API `initialize(String appId)` added since Core 3.3.0 + +> [!IMPORTANT] +> All Adobe Mobile SDK extensions listed as a dependency in your application are automatically registered when calling `initialize(String appId)`. +> +> Automatic Lifecycle data collection requires **Lifecycle** extension included as an app dependency. + +##### Java + +```java +public class YourApp extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + MobileCore.initialize(this, YOUR_APP_ID); + + // Optionally, if you need a callback: + // MobileCore.initialize(this, YOUR_APP_ID, , new AdobeCallback() { + // @Override + // public void call(Void result) { + // // SDK initialized. + // } + // }); + } +} +``` + +##### Kotlin + +```kotlin +class YourApp : Application() { + override fun onCreate() { + super.onCreate() + + MobileCore.initialize(this, YOUR_APP_ID) + + // Optionally, if you need a callback: + // MobileCore.initialize(this, YOUR_APP_ID) { + // // SDK initialized. + // } + } +} +``` + +#### Initializing the SDK by automatically registering all extensions while disabling Lifecycle data collection + +> [!NOTE] +> API `initialize(InitOptions options)` added since Core 3.3.0 + +> [!IMPORTANT] +> All Adobe Mobile SDK extensions listed as a dependency in your application are automatically registered when calling `initialize(InitOptions options)`. + +##### Java + +```java +public class YourApp extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + InitOptions options = InitOptions.configureWithAppID("YOUR_APP_ID"); + options.setLifecycleAutomaticTrackingEnabled(false); + + MobileCore.initialize(this, options, new AdobeCallback() { + @Override + public void call(Void result) { + // SDK initialized. + } + }); + } +} +``` + +##### Kotlin + +```kotlin +class YourApp : Application() { + override fun onCreate() { + super.onCreate() + + val options = InitOptions.configureWithAppID("YOUR_APP_ID").apply { + lifecycleAutomaticTrackingEnabled = false + } + + MobileCore.initialize(this, options) { + // SDK initialized. + } + } +} +``` + +#### Manually registering extensions and starting the SDK ##### Java From 565833d20aef213c3b90c09aac23d772d5a9458e Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Thu, 20 Feb 2025 11:57:03 -0800 Subject: [PATCH 6/8] Add docs for clarifying tests --- .../mobile/services/ui/common/AEPPresentableTests.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt index d1ce8aef6..cdfacadd5 100644 --- a/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt +++ b/code/core/src/androidTest/java/com/adobe/marketing/mobile/services/ui/common/AEPPresentableTests.kt @@ -30,6 +30,9 @@ import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.mock import org.mockito.Mockito.`when` +/** + * Tests for [AEPPresentable] that require mocking of Android framework classes. + */ class AEPPresentableTests { @Test @@ -138,6 +141,10 @@ class AEPPresentableTests { assertTrue(background.peekValue(0).resourceId == android.R.color.transparent) } + /** + * A sample implementation of [AEPPresentable]. Make an effort to keep this presentation type + * agnostic to avoid test pollution. + */ internal class SampleAEPPresentable( private val presentation: InAppMessage, presentationUtilityProvider: PresentationUtilityProvider, From 8bc8c3d2451fd1a84b5bec623fe9b9bf719ade4c Mon Sep 17 00:00:00 2001 From: Praveen Date: Mon, 24 Feb 2025 16:17:36 -0800 Subject: [PATCH 7/8] Allow disabling timeout when registering response listener (#744) * Allow disabling timeout when registering response listener --- Makefile | 4 +-- .../mobile/internal/eventhub/EventHub.kt | 25 +++++++++++++++---- .../adobe/marketing/mobile/MobileCore.java | 3 ++- .../mobile/internal/eventhub/EventHubTests.kt | 22 ++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 941b8d253..a7e690b84 100644 --- a/Makefile +++ b/Makefile @@ -23,9 +23,9 @@ assemble-phone: core-assemble-phone signal-assemble-phone lifecycle-assemble-pho assemble-phone-release: core-assemble-phone-release signal-assemble-phone-release lifecycle-assemble-phone-release identity-assemble-phone-release -unit-test: core-unit-test signal-unit-test lifecycle-unit-test testutils-unit-test +unit-test: core-unit-test signal-unit-test lifecycle-unit-test identity-unit-test testutils-unit-test -unit-test-coverage: core-unit-test-coverage signal-unit-test-coverage lifecycle-unit-test-coverage testutils-unit-test-coverage +unit-test-coverage: core-unit-test-coverage signal-unit-test-coverage lifecycle-unit-test-coverage identity-unit-test-coverage testutils-unit-test-coverage functional-test: core-functional-test signal-functional-test lifecycle-functional-test identity-functional-test diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/EventHub.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/EventHub.kt index d53a62dbe..bca38eb8e 100644 --- a/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/EventHub.kt +++ b/code/core/src/main/java/com/adobe/marketing/mobile/internal/eventhub/EventHub.kt @@ -70,7 +70,8 @@ internal class EventHub { /** * Concurrent list which stores the event listeners for response events. */ - private val responseEventListeners: ConcurrentLinkedQueue = + @VisibleForTesting + internal val responseEventListeners: ConcurrentLinkedQueue = ConcurrentLinkedQueue() /** @@ -377,10 +378,12 @@ internal class EventHub { } /** - * Registers an event listener which will be invoked when the response event to trigger event is dispatched - * @param triggerEvent An [Event] which will trigger a response event - * @param timeoutMS A timeout in milliseconds, if the response listener is not invoked within the timeout, then the `EventHub` invokes the fail method. - * @param listener An [AdobeCallbackWithError] which will be invoked whenever the `EventHub` receives the response event for trigger event + * Registers an event listener that will be invoked when the response event corresponding to the trigger event is dispatched. + * If the response listener is not invoked within the specified timeout, the `EventHub` triggers the fail method. + * + * @param triggerEvent An [Event] that triggers the response event. + * @param timeoutMS A timeout in milliseconds. Use `Long.MAX_DURATION` to wait indefinitely without triggering a timeout. + * @param listener An [AdobeCallbackWithError] that will be invoked when the `EventHub` receives the response event for the trigger event. */ fun registerResponseListener( triggerEvent: Event, @@ -389,6 +392,18 @@ internal class EventHub { ) { eventHubExecutor.submit { val triggerEventId = triggerEvent.uniqueIdentifier + + if (timeoutMS == Long.MAX_VALUE) { + responseEventListeners.add( + ResponseListenerContainer( + triggerEventId, + null, + listener + ) + ) + return@submit + } + val timeoutCallable: Callable = Callable { responseEventListeners.filterRemove { it.triggerEventId == triggerEventId } try { diff --git a/code/core/src/phone/java/com/adobe/marketing/mobile/MobileCore.java b/code/core/src/phone/java/com/adobe/marketing/mobile/MobileCore.java index 5e5149cd8..4efd0925a 100644 --- a/code/core/src/phone/java/com/adobe/marketing/mobile/MobileCore.java +++ b/code/core/src/phone/java/com/adobe/marketing/mobile/MobileCore.java @@ -275,7 +275,8 @@ public static void dispatchEvent(@NonNull final Event event) { * event} processing timeout occurs. * * @param event the {@link Event} to be dispatched, used as a trigger. It should not be null. - * @param timeoutMS the timeout specified in milliseconds. + * @param timeoutMS the timeout specified in milliseconds. Use Long.MAX_DURATION to wait + * indefinitely without triggering a timeout. * @param responseCallback the callback whose {@link AdobeCallbackWithError#call(Object)} will * be called when the response event is heard. It should not be null. */ diff --git a/code/core/src/test/java/com/adobe/marketing/mobile/internal/eventhub/EventHubTests.kt b/code/core/src/test/java/com/adobe/marketing/mobile/internal/eventhub/EventHubTests.kt index f05f0073e..6ed0adae1 100644 --- a/code/core/src/test/java/com/adobe/marketing/mobile/internal/eventhub/EventHubTests.kt +++ b/code/core/src/test/java/com/adobe/marketing/mobile/internal/eventhub/EventHubTests.kt @@ -1847,6 +1847,28 @@ internal class EventHubTests { assertEquals(capturedEvents, listOf(Pair(testResponseEvent, null), Pair(null, AdobeError.CALLBACK_TIMEOUT))) } + @Test + fun testResponseListener_InfiniteTimeout() { + val testEvent = Event.Builder("Test event", eventType, eventSource).build() + val responseCallback = object : AdobeCallbackWithError { + override fun call(value: Event?) { + assertTrue { false } + } + + override fun fail(error: AdobeError?) { + assertTrue { false } + } + } + + eventHub.registerResponseListener(testEvent, Long.MAX_VALUE, responseCallback) + + Thread.sleep(100) + + val responseListener = eventHub.responseEventListeners.first { it.triggerEventId == testEvent.uniqueIdentifier } + assertNotNull(responseListener) + assertNull(responseListener.timeoutTask) + } + @Test fun testListener_LongRunningListenerShouldNotBlockOthers() { class Extension1(api: ExtensionApi) : Extension(api) { From 2b836629753e2af9d5dda6efd0b11fe4861fd5ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:17:50 -0800 Subject: [PATCH 8/8] Updating Core version to 3.3.1 (#745) Co-authored-by: github-actions[bot] --- .../java/com/adobe/marketing/mobile/internal/CoreConstants.kt | 2 +- code/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt b/code/core/src/main/java/com/adobe/marketing/mobile/internal/CoreConstants.kt index e6a69206f..046c595dd 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.3.0" + const val VERSION = "3.3.1" object EventDataKeys { /** diff --git a/code/gradle.properties b/code/gradle.properties index 845dcfc08..527e390ee 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -5,7 +5,7 @@ android.useAndroidX=true #Maven artifacts #Core extension -coreExtensionVersion=3.3.0 +coreExtensionVersion=3.3.1 coreExtensionName=core coreMavenRepoName=AdobeMobileCoreSdk coreMavenRepoDescription=Android Core Extension for Adobe Mobile Marketing