diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AEPOptimizeError.kt b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AEPOptimizeError.kt index 5a092da4..63a60355 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AEPOptimizeError.kt +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/AEPOptimizeError.kt @@ -84,6 +84,7 @@ data class AEPOptimizeError( return getAdobeErrorFromStatus(data[STATUS] as Int?) } + @JvmStatic fun getTimeoutError(): AEPOptimizeError { return AEPOptimizeError( null, diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java index 5033e55c..a0c58bc0 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/Optimize.java @@ -49,7 +49,7 @@ private Optimize() {} * Experience Edge network. * *

The returned decision propositions are cached in-memory in the Optimize SDK extension and - * can be retrieved using {@link #getPropositions(List, AdobeCallback)} API. + * can be retrieved using {@link #getPropositions(List, long, AdobeCallback)} API. * * @param decisionScopes {@code List} containing scopes for which offers need to * be updated. @@ -62,8 +62,8 @@ public static void updatePropositions( @NonNull final List decisionScopes, @Nullable final Map xdm, @Nullable final Map data) { - - updatePropositions(decisionScopes, xdm, data, null); + final long defaultTimeout = OptimizeConstants.EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT; + updatePropositions(decisionScopes, xdm, data, defaultTimeout, null); } /** @@ -88,6 +88,44 @@ public static void updatePropositions( @Nullable final Map xdm, @Nullable final Map data, @Nullable final AdobeCallback> callback) { + final long defaultTimeout = OptimizeConstants.EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT; + updatePropositionsInternal(decisionScopes, xdm, data, defaultTimeout, callback); + } + + /** + * This API dispatches an Event for the Edge network extension to fetch decision propositions, + * for the provided decision scopes list, from the decisioning services enabled in the + * Experience Edge network. + * + *

The returned decision propositions are cached in-memory in the Optimize SDK extension and + * can be retrieved using {@link #getPropositions(List, long, AdobeCallback)} API. + * + * @param decisionScopes {@code List} containing scopes for which offers need to + * be updated. + * @param xdm {@code Map} containing additional XDM-formatted data to be sent in + * the personalization query request. + * @param data {@code Map} containing additional free-form data to be sent in + * the personalization query request. + * @param timeoutMillis {@code Long} containing additional configurable timeout to be sent in + * the personalization query request. + * @param callback {@code AdobeCallback>} which will be + * invoked when decision propositions are received from the Edge network. + */ + public static void updatePropositions( + @NonNull final List decisionScopes, + @Nullable final Map xdm, + @Nullable final Map data, + final long timeoutMillis, + @Nullable final AdobeCallback> callback) { + updatePropositionsInternal(decisionScopes, xdm, data, timeoutMillis, callback); + } + + private static void updatePropositionsInternal( + @NonNull final List decisionScopes, + @Nullable final Map xdm, + @Nullable final Map data, + final long timeoutMillis, + @Nullable final AdobeCallback> callback) { if (OptimizeUtils.isNullOrEmpty(decisionScopes)) { Log.warning( @@ -137,6 +175,7 @@ public static void updatePropositions( if (!OptimizeUtils.isNullOrEmpty(data)) { eventData.put(OptimizeConstants.EventDataKeys.DATA, data); } + eventData.put(OptimizeConstants.EventDataKeys.TIMEOUT, timeoutMillis); final Event event = new Event.Builder( @@ -148,7 +187,7 @@ public static void updatePropositions( MobileCore.dispatchEventWithResponseCallback( event, - OptimizeConstants.EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT, + timeoutMillis, new AdobeCallbackWithError() { @Override public void fail(final AdobeError adobeError) { @@ -236,6 +275,30 @@ public void call(final Event event) { public static void getPropositions( @NonNull final List decisionScopes, @NonNull final AdobeCallback> callback) { + long defaultTimeout = OptimizeConstants.GET_RESPONSE_CALLBACK_TIMEOUT; + getPropositionsInternal(decisionScopes, defaultTimeout, callback); + } + + /** + * This API retrieves the previously fetched propositions, for the provided decision scopes, + * from the in-memory extension propositions cache. + * + * @param decisionScopes {@code List} containing scopes for which offers need to + * be requested. + * @param callback {@code AdobeCallbackWithError>} which + * will be invoked when decision propositions are retrieved from the local cache. + */ + public static void getPropositions( + @NonNull final List decisionScopes, + final long timeoutMillis, + @NonNull final AdobeCallback> callback) { + getPropositionsInternal(decisionScopes, timeoutMillis, callback); + } + + private static void getPropositionsInternal( + @NonNull final List decisionScopes, + final long timeoutMillis, + @NonNull final AdobeCallback> callback) { if (OptimizeUtils.isNullOrEmpty(decisionScopes)) { Log.warning( OptimizeConstants.LOG_TAG, @@ -286,7 +349,7 @@ public static void getPropositions( // requests have enough time to complete. MobileCore.dispatchEventWithResponseCallback( event, - OptimizeConstants.GET_RESPONSE_CALLBACK_TIMEOUT, + timeoutMillis, new AdobeCallbackWithError() { @Override public void fail(final AdobeError adobeError) { diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java index 4633cf71..ff27229b 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeConstants.java @@ -74,6 +74,7 @@ static final class EventDataKeys { static final String DECISION_SCOPE_NAME = "name"; static final String XDM = "xdm"; static final String DATA = "data"; + static final String TIMEOUT = "timeout"; static final String PROPOSITIONS = "propositions"; static final String RESPONSE_ERROR = "responseerror"; static final String PROPOSITION_INTERACTIONS = "propositioninteractions"; diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java index 4a9f168d..8147e125 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeExtension.java @@ -449,10 +449,11 @@ void handleUpdatePropositions(@NonNull final Event event) { // add the Edge event to update propositions in the events queue. eventsDispatcher.offer(edgeEvent); - + long timeoutMillis = + DataReader.getLong(eventData, OptimizeConstants.EventDataKeys.TIMEOUT); MobileCore.dispatchEventWithResponseCallback( edgeEvent, - OptimizeConstants.EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT, + timeoutMillis, new AdobeCallbackWithError() { @Override public void fail(final AdobeError error) { diff --git a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java index a880b206..92544132 100644 --- a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java +++ b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeTests.java @@ -11,6 +11,12 @@ package com.adobe.marketing.mobile.optimize; +import static com.adobe.marketing.mobile.optimize.Optimize.failWithOptimizeError; +import static com.adobe.marketing.mobile.optimize.Optimize.getPropositions; +import static com.adobe.marketing.mobile.optimize.Optimize.updatePropositions; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + import android.util.Base64; import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; @@ -454,7 +460,7 @@ public void testGetPropositions_validDecisionScope() throws Exception { new DecisionScope( "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==")); - Optimize.getPropositions( + getPropositions( scopes, new AdobeCallbackWithError>() { @Override @@ -561,7 +567,7 @@ public void testGetPropositions_multipleValidDecisionScopes() { "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==")); scopes.add(new DecisionScope("myMbox")); - Optimize.getPropositions( + getPropositions( scopes, new AdobeCallbackWithError>() { @Override @@ -631,7 +637,7 @@ public void testGetPropositions_invalidDecisionScopeInList() { new DecisionScope( "eyJhY3Rpdml0eUlkIjoiIiwicGxhY2VtZW50SWQiOiJ4Y29yZTpvZmZlci1wbGFjZW1lbnQ6MTExMTExMTExMTExMTExMSJ9")); - Optimize.getPropositions( + getPropositions( scopes, new AdobeCallbackWithError>() { @Override @@ -660,7 +666,7 @@ public void call(Map propositionsMap) { public void testGetPropositions_emptyDecisionScopesList() { try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { // test - Optimize.getPropositions( + getPropositions( new ArrayList(), new AdobeCallbackWithError>() { @Override @@ -689,7 +695,7 @@ public void call(Map propositionsMap) { public void testGetPropositions_nullDecisionScopesList() { try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { // test - Optimize.getPropositions( + getPropositions( null, new AdobeCallbackWithError>() { @Override @@ -896,4 +902,120 @@ public void test_clearCachedPropositions() { Assert.assertNull(event.getEventData()); } } + + @Test + public void testUpdatePropositions_timeoutError() { + + long timeoutMillis = 100; + Map xdm = new HashMap<>(); + Map data = new HashMap<>(); + final List scopes = new ArrayList<>(); + scopes.add( + new DecisionScope( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==")); + + // Mock the callback + AdobeCallbackWithError> callbackMock = + Mockito.mock(AdobeCallbackWithError.class); + + AdobeCallbackWithOptimizeError callbackMockEvent = + Mockito.mock(AdobeCallbackWithOptimizeError.class); + + try (MockedStatic mobileCoreMockedStatic = + Mockito.mockStatic(MobileCore.class); + MockedStatic base64MockedStatic = Mockito.mockStatic(Base64.class)) { + + base64MockedStatic + .when( + () -> + Base64.decode( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt())) + .thenAnswer( + (Answer) + invocation -> + java.util.Base64.getDecoder() + .decode((String) invocation.getArguments()[0])); + + mobileCoreMockedStatic + .when( + () -> + MobileCore.dispatchEventWithResponseCallback( + ArgumentMatchers.any(Event.class), + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(AdobeCallbackWithError.class))) + .thenAnswer( + (Answer) + invocation -> { + failWithOptimizeError( + callbackMockEvent, + AEPOptimizeError.Companion.getTimeoutError()); + return null; + }); + + updatePropositions(scopes, xdm, data, timeoutMillis, callbackMock); + ArgumentCaptor errorCaptor = + ArgumentCaptor.forClass(AEPOptimizeError.class); + verify(callbackMockEvent, times(1)).fail(errorCaptor.capture()); + Assert.assertEquals( + AEPOptimizeError.Companion.getTimeoutError(), errorCaptor.getValue()); + } + } + + @Test + public void testGetPropositions_timeoutError() { + + long timeoutMillis = 100; + final List scopes = new ArrayList<>(); + scopes.add( + new DecisionScope( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==")); + + // Mock the callback + AdobeCallbackWithError> callbackMock = + Mockito.mock(AdobeCallbackWithError.class); + + AdobeCallbackWithOptimizeError callbackMockEvent = + Mockito.mock(AdobeCallbackWithOptimizeError.class); + + try (MockedStatic mobileCoreMockedStatic = + Mockito.mockStatic(MobileCore.class); + MockedStatic base64MockedStatic = Mockito.mockStatic(Base64.class)) { + + base64MockedStatic + .when( + () -> + Base64.decode( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt())) + .thenAnswer( + (Answer) + invocation -> + java.util.Base64.getDecoder() + .decode((String) invocation.getArguments()[0])); + + mobileCoreMockedStatic + .when( + () -> + MobileCore.dispatchEventWithResponseCallback( + ArgumentMatchers.any(Event.class), + ArgumentMatchers.anyLong(), + ArgumentMatchers.any(AdobeCallbackWithError.class))) + .thenAnswer( + (Answer) + invocation -> { + failWithOptimizeError( + callbackMockEvent, + AEPOptimizeError.Companion.getTimeoutError()); + return null; + }); + + getPropositions(scopes, timeoutMillis, callbackMock); + ArgumentCaptor errorCaptor = + ArgumentCaptor.forClass(AEPOptimizeError.class); + verify(callbackMockEvent, times(1)).fail(errorCaptor.capture()); + Assert.assertEquals( + AEPOptimizeError.Companion.getTimeoutError(), errorCaptor.getValue()); + } + } } diff --git a/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/OffersScreen.kt b/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/OffersScreen.kt index 37c6d060..173bf29c 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/OffersScreen.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/OffersScreen.kt @@ -181,7 +181,8 @@ fun OffersView(viewModel: MainViewModel) { viewModel.updatePropositions( decisionScopes = decisionScopeList, xdm = mapOf(Pair("xdmKey", "1234")), - data = data + data = data, + timeout = 200 ) }) { Text( @@ -200,7 +201,10 @@ fun OffersView(viewModel: MainViewModel) { viewModel.jsonDecisionScope?.also { decisionScopeList.add(it) } viewModel.targetMboxDecisionScope?.also { decisionScopeList.add(it) } - viewModel.getPropositions(decisionScopes = decisionScopeList) + viewModel.getPropositions( + decisionScopes = decisionScopeList, + timeout = 200 + ) }) { Text( text = "Get \n Propositions", diff --git a/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt b/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt index a7832f5e..3768678f 100644 --- a/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt +++ b/code/testapp/src/main/java/com/adobe/marketing/optimizeapp/viewmodels/MainViewModel.kt @@ -71,10 +71,11 @@ class MainViewModel: ViewModel() { * Calls the Optimize SDK API to get the Propositions see [Optimize.getPropositions] * * @param [decisionScopes] a [List] of [DecisionScope] + * @param [timeout] a [Long] in milliseconds */ - fun getPropositions(decisionScopes: List) { + fun getPropositions(decisionScopes: List, timeout: Long? = null) { optimizePropositionStateMap.clear() - Optimize.getPropositions(decisionScopes, object: AdobeCallbackWithError>{ + val callback = object : AdobeCallbackWithError> { override fun call(propositions: Map?) { propositions?.forEach { optimizePropositionStateMap[it.key.name] = it.value @@ -84,8 +85,10 @@ class MainViewModel: ViewModel() { override fun fail(error: AdobeError?) { print("Error in getting Propositions.") } - - }) + } + timeout?.let { milliseconds -> + Optimize.getPropositions(decisionScopes, milliseconds, callback) + } ?: Optimize.getPropositions(decisionScopes, callback) } /** @@ -94,19 +97,37 @@ class MainViewModel: ViewModel() { * @param decisionScopes a [List] of [DecisionScope] * @param xdm a [Map] of xdm params * @param data a [Map] of data + * @param timeout a [Long] in milliseconds */ - fun updatePropositions(decisionScopes: List , xdm: Map , data: Map) { - optimizePropositionStateMap.clear() - Optimize.updatePropositions(decisionScopes, xdm, data, object: AdobeCallbackWithOptimizeError>{ + fun updatePropositions( + decisionScopes: List, + xdm: Map, + data: Map, + timeout: Long? = null + ) { + val callback = object : AdobeCallbackWithOptimizeError> { override fun call(propositions: Map?) { - Log.i("Optimize Test App","Propositions updated successfully.") + Log.i("Optimize Test App", "Propositions updated successfully.") } override fun fail(error: AEPOptimizeError?) { - Log.i("Optimize Test App","Error in updating Propositions:: ${error?.title ?: "Undefined"}.") + Log.i( + "Optimize Test App", + "Error in updating Propositions:: ${error?.title ?: "Undefined"}." + ) } - }) + } + optimizePropositionStateMap.clear() + timeout?.let { milliseconds -> + Optimize.updatePropositions( + decisionScopes, + xdm, + data, + milliseconds, + callback + ) + } ?: Optimize.updatePropositions(decisionScopes, xdm, data, callback) } /**