diff --git a/Documentation/api-reference.md b/Documentation/api-reference.md index 468e11a1..37772282 100644 --- a/Documentation/api-reference.md +++ b/Documentation/api-reference.md @@ -12,6 +12,7 @@ Refer to the [Getting Started Guide](getting-started.md). - [onPropositionsUpdate](#onPropositionsUpdate) - [resetIdentities](#resetIdentities) - [updatePropositions](#updatePropositions) +- [updatePropositionsWithCompletionHandler](#updatePropositionsWithCompletionHandler) ## Public classes @@ -184,6 +185,63 @@ Optimize.updatePropositions(decisionScopes, }); ``` +## updatePropositionsWithCompletionHandler + +This API dispatches an event for the Edge network extension to fetch decision propositions, for the provided decision scopes array, from the decisioning services enabled in the Experience Edge. The returned decision propositions are cached in-memory in the Optimize SDK extension and can be retrieved using `getPropositions` API. + +> [!TIP] +> Completion callback passed to `updatePropositions` supports network timeout and fatal errors returned by edge network along with fetched propositions data. The SDK's internal retry mechanism handles the recoverable HTTP errors. As a result, recoverable HTTP errors are not returned through this callback. + +### Java + +#### Syntax + +```java +public static void updatePropositions(final List decisionScopes, + final Map xdm, + final Map data, + final AdobeCallback> callback) +``` + +* _decisionScopes_ is a list of decision scopes for which propositions need updating. +* _xdm_ is a map containing additional xdm formatted data to be attached to the Experience Event. +* _data_ is a map containing additional freeform data to be attached to the Experience Event. +* _callback_ is an optional completion handler that is invoked at the completion of the edge request. `call` method is invoked with propositions map of type `Map`. If the callback is an instance of `AdobeCallbackWithOptimizeError`, and if the operation times out or an error occurs in retrieving propositions, the `fail` method is invoked with the appropriate [AEPOptimizeError](https://developer.adobe.com/client-sdks/edge/adobe-journey-optimizer-decisioning/api-reference/#aepoptimizeerror). _Note:_ In certain cases, both the success and failure callbacks may be triggered. To handle these cases, ensure that your implementation checks for both successful propositions and errors within the callback, as both may be present simultaneously. + +#### Example + +```java +final DecisionScope decisionScope1 = DecisionScope("xcore:offer-activity:1111111111111111", "xcore:offer-placement:1111111111111111", 2); +final DecisionScope decisionScope2 = new DecisionScope("myScope"); + +final List decisionScopes = new ArrayList<>(); +decisionScopes.add(decisionScope1); +decisionScopes.add(decisionScope2); + +Optimize.updatePropositions(decisionScopes, + new HashMap() { + { + put("xdmKey", "xdmValue"); + } + }, + new HashMap() { + { + put("dataKey", "dataValue"); + } + }, + new AdobeCallbackWithOptimizeError>() { + @Override + public void fail(AEPOptimizeError optimizeError) { + responseError = optimizeError; + } + + @Override + public void call(Map propositionsMap) { + responseMap = propositionsMap; + } + }); +``` + ## Public classes ### DecisionScope diff --git a/code/gradle.properties b/code/gradle.properties index 5cdddcfb..0825d0fc 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -16,7 +16,7 @@ org.gradle.caching=true android.useAndroidX=true moduleName=optimize -moduleVersion=3.1.0 +moduleVersion=3.2.2 #Maven artifact mavenRepoName=AdobeMobileOptimizeSdk diff --git a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java index 3d4e6f7a..f8fdf9af 100644 --- a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java +++ b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeFunctionalTests.java @@ -2231,6 +2231,432 @@ public void testPropositionGenerateReferenceXdm() throws IOException { "de03ac85-802a-4331-a905-a57053164d35", decisioning.get("propositionID")); } + // 19 + @Test + public void testGetPropositions_multipleUpdatePropositonsCallsBeforeGetPropositions() + throws InterruptedException, IOException { + // setup + final Map configData = new HashMap<>(); + configData.put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); + updateConfiguration(configData); + + final String decisionScopeString = + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ=="; + + // Setting up the cache with a decision scope and a proposition. + Optimize.updatePropositions( + Collections.singletonList(new DecisionScope(decisionScopeString)), null, null); + List eventsListEdge = + TestHelper.getDispatchedEventsWith( + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.REQUEST_CONTENT, + 1000); + + Event edgeEvent = eventsListEdge.get(0); + final String requestEventId = edgeEvent.getUniqueIdentifier(); + final String edgeResponseData = + "{\n" + + " \"payload\": [\n" + + " {\n" + + " \"id\":" + + " \"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\",\n" + + " \"scope\": \"" + + decisionScopeString + + "\",\n" + + " \"activity\": {\n" + + " \"etag\": \"8\",\n" + + " \"id\":" + + " \"xcore:offer-activity:1111111111111111\"\n" + + " },\n" + + " \"placement\": {\n" + + " \"etag\": \"1\",\n" + + " \"id\":" + + " \"xcore:offer-placement:1111111111111111\"\n" + + " },\n" + + " \"items\": [\n" + + " {\n" + + " \"id\":" + + " \"xcore:personalized-offer:1111111111111111\",\n" + + " \"etag\": \"10\",\n" + + " \"score\": 1,\n" + + " \"schema\":" + + " \"https://ns.adobe.com/experience/offer-management/content-component-html\",\n" + + " \"data\": {\n" + + " \"id\":" + + " \"xcore:personalized-offer:1111111111111111\",\n" + + " \"format\":" + + " \"text/html\",\n" + + " \"content\":" + + " \"

This is HTML content

\",\n" + + " \"characteristics\":" + + " {\n" + + " \"testing\":" + + " \"true\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"requestEventId\":\"" + + requestEventId + + "\",\n" + + " \"requestId\":" + + " \"BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB\",\n" + + " \"type\":" + + " \"personalization:decisions\"\n" + + " }"; + + ObjectMapper objectMapper = new ObjectMapper(); + Map eventData = + objectMapper.readValue( + edgeResponseData, new TypeReference>() {}); + + Event event = + new Event.Builder( + "AEP Response Event Handle", + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.PERSONALIZATION) + .setEventData(eventData) + .build(); + MobileCore.dispatchEvent(event); + Thread.sleep(1000); + + // Send completion event + Map completionEventData = + new HashMap() { + { + put("completedUpdateRequestForEventId", requestEventId); + } + }; + Event completionEvent = + new Event.Builder( + "Optimize Update Propositions Complete", + OptimizeTestConstants.EventType.OPTIMIZE, + OptimizeTestConstants.EventSource.CONTENT_COMPLETE) + .setEventData(completionEventData) + .build(); + // Cache is now updated with a proposition. + MobileCore.dispatchEvent(completionEvent); + + Thread.sleep(1000); + TestHelper.resetTestExpectations(); + + // Firing another update event with same decision scope but different proposition data. + Optimize.updatePropositions( + Collections.singletonList(new DecisionScope(decisionScopeString)), null, null); + + List secondEventsListEdge = + TestHelper.getDispatchedEventsWith( + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.REQUEST_CONTENT, + 1000); + + Event secondEdgeEvent = secondEventsListEdge.get(0); + + final String secondRequestEventId = secondEdgeEvent.getUniqueIdentifier(); + // Send Edge Response event + final String secondEdgeResponseData = + "{\n" + + " \"payload\": [\n" + + " {\n" + + " \"id\":" + + " \"cccccccc-cccc-cccc-cccc-cccccccc\",\n" + + " \"scope\": \"" + + decisionScopeString + + "\",\n" + + " \"activity\": {\n" + + " \"etag\": \"8\",\n" + + " \"id\":" + + " \"xcore:offer-activity:1111111111111111\"\n" + + " },\n" + + " \"placement\": {\n" + + " \"etag\": \"1\",\n" + + " \"id\":" + + " \"xcore:offer-placement:1111111111111111\"\n" + + " },\n" + + " \"items\": [\n" + + " {\n" + + " \"id\":" + + " \"xcore:personalized-offer:1111111111111111\",\n" + + " \"etag\": \"10\",\n" + + " \"score\": 1,\n" + + " \"schema\":" + + " \"https://ns.adobe.com/experience/offer-management/content-component-html\",\n" + + " \"data\": {\n" + + " \"id\":" + + " \"xcore:personalized-offer:1111111111111111\",\n" + + " \"format\":" + + " \"text/html\",\n" + + " \"content\":" + + " \"

This is HTML content

\",\n" + + " \"characteristics\":" + + " {\n" + + " \"testing\":" + + " \"true\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"requestEventId\":\"" + + secondRequestEventId + + "\",\n" + + " \"requestId\":" + + " \"CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCC\",\n" + + " \"type\":" + + " \"personalization:decisions\"\n" + + " }"; + + ObjectMapper secondObjectMapper = new ObjectMapper(); + Map secondEventData = + secondObjectMapper.readValue( + secondEdgeResponseData, new TypeReference>() {}); + + Event secondEvent = + new Event.Builder( + "AEP Response Event Handle", + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.PERSONALIZATION) + .setEventData(secondEventData) + .build(); + + // Completing the second event with updated proposition. + MobileCore.dispatchEvent(secondEvent); + Thread.sleep(1000); + + // Executing Get proposition event before the update event is completed. + DecisionScope decisionScope = new DecisionScope(decisionScopeString); + final Map propositionMap = new HashMap<>(); + + Optimize.getPropositions( + Collections.singletonList(decisionScope), + new AdobeCallbackWithError>() { + @Override + public void fail(AdobeError adobeError) { + Assert.fail("Error in getting cached propositions"); + } + + @Override + public void call( + Map decisionScopePropositionMap) { + propositionMap.putAll(decisionScopePropositionMap); + + // Assertions + // Map should contain the updated proposition data. + OptimizeProposition optimizeProposition = propositionMap.get(decisionScope); + Assert.assertNotNull(optimizeProposition); + Assert.assertEquals( + "cccccccc-cccc-cccc-cccc-cccccccc", optimizeProposition.getId()); + Assert.assertEquals( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==", + optimizeProposition.getScope()); + Assert.assertEquals(1, optimizeProposition.getOffers().size()); + } + }); + + // Send completion for second event + Map secondCompletionEventData = + new HashMap() { + { + put("completedUpdateRequestForEventId", secondRequestEventId); + } + }; + + Event secondCompletionEvent = + new Event.Builder( + "Optimize Update Propositions Complete", + OptimizeTestConstants.EventType.OPTIMIZE, + OptimizeTestConstants.EventSource.CONTENT_COMPLETE) + .setEventData(secondCompletionEventData) + .build(); + + MobileCore.dispatchEvent(secondCompletionEvent); + } + + // 20 + @Test + public void testGetPropositions_FewDecisionScopesNotInCacheAndGetToBeQueued() + throws InterruptedException, IOException { + // setup + final Map configData = new HashMap<>(); + configData.put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); + updateConfiguration(configData); + + final String decisionScopeAString = + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ=="; + final String decisionScopeBString = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3Rpdml0eUlkIjoic2NvcGUtYiIsInBsYWNlbWVudElkIjoic2NvcGUtYl9wbGFjZW1lbnQifQ.QzNxT1dBZ1Z1M0Z5dW84SjdKak1nY2c1"; + + // Setting up the cache with decisionScopeA and a proposition. + Optimize.updatePropositions( + Collections.singletonList(new DecisionScope(decisionScopeAString)), null, null); + List eventsListEdge = + TestHelper.getDispatchedEventsWith( + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.REQUEST_CONTENT, + 1000); + + Event edgeEvent = eventsListEdge.get(0); + final String requestEventId = edgeEvent.getUniqueIdentifier(); + final String edgeResponseData = + "{\n" + + " \"payload\": [\n" + + " {\n" + + " \"id\": \"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\",\n" + + " \"scope\": \"" + + decisionScopeAString + + "\"\n" + + " }\n" + + " ],\n" + + " \"requestEventId\": \"" + + requestEventId + + "\",\n" + + " \"requestId\": \"AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAA\",\n" + + " \"type\": \"personalization:decisions\"\n" + + "}"; + + ObjectMapper objectMapper = new ObjectMapper(); + Map eventData = + objectMapper.readValue( + edgeResponseData, new TypeReference>() {}); + + Event event = + new Event.Builder( + "AEP Response Event Handle", + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.PERSONALIZATION) + .setEventData(eventData) + .build(); + MobileCore.dispatchEvent(event); + Thread.sleep(1000); + + // Send completion event + Map completionEventData = + new HashMap() { + { + put("completedUpdateRequestForEventId", requestEventId); + } + }; + Event completionEvent = + new Event.Builder( + "Optimize Update Propositions Complete", + OptimizeTestConstants.EventType.OPTIMIZE, + OptimizeTestConstants.EventSource.CONTENT_COMPLETE) + .setEventData(completionEventData) + .build(); + MobileCore.dispatchEvent(completionEvent); + + Thread.sleep(1000); + TestHelper.resetTestExpectations(); + + // Update event with decisionScopeB + Optimize.updatePropositions( + Collections.singletonList(new DecisionScope(decisionScopeBString)), null, null); + + List secondEventsListEdge = + TestHelper.getDispatchedEventsWith( + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.REQUEST_CONTENT, + 1000); + + Event secondEdgeEvent = secondEventsListEdge.get(0); + final String secondRequestEventId = secondEdgeEvent.getUniqueIdentifier(); + final String secondEdgeResponseData = + "{\n" + + " \"payload\": [\n" + + " {\n" + + " \"id\": \"BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB\",\n" + + " \"scope\": \"" + + decisionScopeBString + + "\"\n" + + " }\n" + + " ],\n" + + " \"requestEventId\": \"" + + secondRequestEventId + + "\",\n" + + " \"requestId\": \"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbb\",\n" + + " \"type\": \"personalization:decisions\"\n" + + "}"; + + ObjectMapper secondObjectMapper = new ObjectMapper(); + Map secondEventData = + secondObjectMapper.readValue( + secondEdgeResponseData, new TypeReference>() {}); + + Event secondEvent = + new Event.Builder( + "AEP Response Event Handle", + OptimizeTestConstants.EventType.EDGE, + OptimizeTestConstants.EventSource.PERSONALIZATION) + .setEventData(secondEventData) + .build(); + MobileCore.dispatchEvent(secondEvent); + Thread.sleep(1000); + + // Execute get proposition event with both decisionScopeA and decisionScopeB + Optimize.getPropositions( + Arrays.asList( + new DecisionScope(decisionScopeAString), + new DecisionScope(decisionScopeBString)), + new AdobeCallbackWithError>() { + @Override + public void fail(AdobeError adobeError) { + Assert.fail("Error in getting cached propositions"); + } + + @Override + public void call( + Map decisionScopePropositionMap) { + // Assertions + // Verify that the proposition for decisionScopeA is present and validate. + Assert.assertTrue( + decisionScopePropositionMap.containsKey( + new DecisionScope(decisionScopeAString))); + OptimizeProposition optimizePropositionA = + decisionScopePropositionMap.get( + new DecisionScope(decisionScopeAString)); + Assert.assertNotNull(optimizePropositionA); + Assert.assertEquals( + "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + optimizePropositionA.getId()); + + // Verify that the proposition for decisionScopeB is present and validate + // that the event GET event was queued. + Assert.assertTrue( + decisionScopePropositionMap.containsKey( + new DecisionScope(decisionScopeBString))); + OptimizeProposition optimizePropositionB = + decisionScopePropositionMap.get( + new DecisionScope(decisionScopeBString)); + Assert.assertNotNull(optimizePropositionB); + Assert.assertEquals( + "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", + optimizePropositionB.getId()); + } + }); + + // Send completion for second update event after the Get event is fired + Map secondCompletionEventData = + new HashMap() { + { + put("completedUpdateRequestForEventId", secondRequestEventId); + } + }; + Event secondCompletionEvent = + new Event.Builder( + "Optimize Update Propositions Complete", + OptimizeTestConstants.EventType.OPTIMIZE, + OptimizeTestConstants.EventSource.CONTENT_COMPLETE) + .setEventData(secondCompletionEventData) + .build(); + MobileCore.dispatchEvent(secondCompletionEvent); + + Thread.sleep(1000); + TestHelper.resetTestExpectations(); + } + private void updateConfiguration(final Map config) throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); MonitorExtension.configurationAwareness(configurationState -> latch.countDown()); diff --git a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java index b3a5ca74..534ed82a 100644 --- a/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java +++ b/code/optimize/src/androidTest/java/com/adobe/marketing/mobile/optimize/OptimizeTestConstants.java @@ -13,7 +13,7 @@ public class OptimizeTestConstants { - static final String EXTENSION_VERSION = "3.1.0"; + static final String EXTENSION_VERSION = "3.2.2"; public static final String LOG_TAG = "OptimizeTest"; static final String CONFIG_DATA_STORE = "AdobeMobile_ConfigState"; 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..ae536b6c 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 @@ -58,6 +58,7 @@ private Optimize() {} * @param data {@code Map} containing additional free-form data to be sent in * the personalization query request. */ + @Deprecated public static void updatePropositions( @NonNull final List decisionScopes, @Nullable final Map xdm, @@ -88,6 +89,45 @@ public static void updatePropositions( @Nullable final Map xdm, @Nullable final Map data, @Nullable final AdobeCallback> callback) { + final double defaultTimeoutSeconds = + OptimizeConstants.EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT; + updatePropositionsInternal(decisionScopes, xdm, data, defaultTimeoutSeconds, 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, double, 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 timeoutSeconds {@code Double} containing additional configurable timeout(seconds) 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 double timeoutSeconds, + @Nullable final AdobeCallback> callback) { + updatePropositionsInternal(decisionScopes, xdm, data, timeoutSeconds, callback); + } + + private static void updatePropositionsInternal( + @NonNull final List decisionScopes, + @Nullable final Map xdm, + @Nullable final Map data, + final double timeoutSeconds, + @Nullable final AdobeCallback> callback) { if (OptimizeUtils.isNullOrEmpty(decisionScopes)) { Log.warning( @@ -138,6 +178,10 @@ public static void updatePropositions( eventData.put(OptimizeConstants.EventDataKeys.DATA, data); } + long timeoutMillis = (long) (timeoutSeconds * OptimizeConstants.TIMEOUT_CONVERSION_FACTOR); + + eventData.put(OptimizeConstants.EventDataKeys.TIMEOUT, timeoutMillis); + final Event event = new Event.Builder( OptimizeConstants.EventNames.UPDATE_PROPOSITIONS_REQUEST, @@ -148,7 +192,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 +280,30 @@ public void call(final Event event) { public static void getPropositions( @NonNull final List decisionScopes, @NonNull final AdobeCallback> callback) { + final double defaultTimeoutSeconds = OptimizeConstants.GET_RESPONSE_CALLBACK_TIMEOUT; + getPropositionsInternal(decisionScopes, defaultTimeoutSeconds, 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 double timeoutSeconds, + @NonNull final AdobeCallback> callback) { + getPropositionsInternal(decisionScopes, timeoutSeconds, callback); + } + + private static void getPropositionsInternal( + @NonNull final List decisionScopes, + final double timeoutSeconds, + @NonNull final AdobeCallback> callback) { if (OptimizeUtils.isNullOrEmpty(decisionScopes)) { Log.warning( OptimizeConstants.LOG_TAG, @@ -282,11 +350,11 @@ public static void getPropositions( .setEventData(eventData) .build(); - // Increased default response callback timeout to 10s to ensure prior update propositions - // requests have enough time to complete. + long timeoutMillis = (long) (timeoutSeconds * OptimizeConstants.TIMEOUT_CONVERSION_FACTOR); + 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..449e2e3a 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 @@ -13,12 +13,12 @@ class OptimizeConstants { static final String LOG_TAG = "Optimize"; - static final String EXTENSION_VERSION = "3.1.0"; + static final String EXTENSION_VERSION = "3.2.2"; static final String EXTENSION_NAME = "com.adobe.optimize"; static final String FRIENDLY_NAME = "Optimize"; - static final long DEFAULT_RESPONSE_CALLBACK_TIMEOUT = 500L; - static final long GET_RESPONSE_CALLBACK_TIMEOUT = 10000L; - static final long EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT = 10000L; + static final double GET_RESPONSE_CALLBACK_TIMEOUT = 10; + static final double EDGE_CONTENT_COMPLETE_RESPONSE_TIMEOUT = 10; + static final long TIMEOUT_CONVERSION_FACTOR = 1000; static final String ACTIVITY_ID = "activityId"; static final String XDM_ACTIVITY_ID = "xdm:activityId"; @@ -64,6 +64,7 @@ static final class EventSource { static final String NOTIFICATION = "com.adobe.eventSource.notification"; static final String EDGE_PERSONALIZATION_DECISIONS = "personalization:decisions"; static final String CONTENT_COMPLETE = "com.adobe.eventSource.contentComplete"; + static final String DEBUG = "com.adobe.eventSource.debug"; private EventSource() {} } @@ -74,6 +75,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 c7457206..174a6345 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 @@ -16,6 +16,7 @@ import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionApi; import com.adobe.marketing.mobile.MobileCore; @@ -29,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -43,6 +45,10 @@ class OptimizeExtension extends Extension { // This is accessed from multiple threads. private Map cachedPropositions = new ConcurrentHashMap<>(); + // Concurrent Map containing propositions simulated for preview and cached in-memory in the SDK + private Map previewCachedPropositions = + new ConcurrentHashMap<>(); + // Events dispatcher used to maintain the processing order of update and get propositions // events. // It ensures any update propositions requests issued before a get propositions call are @@ -124,7 +130,8 @@ public boolean doWork(final Event event) { * OptimizeConstants.EventType#GENERIC_IDENTITY} and source {@value * OptimizeConstants.EventSource#REQUEST_RESET} Listener for {@code Event} type {@value * OptimizeConstants.EventType#OPTIMIZE} and source {@value - * OptimizeConstants.EventSource#CONTENT_COMPLETE} + * OptimizeConstants.EventSource#CONTENT_COMPLETE} Listener for {@code Event} type {@value + * EventType#SYSTEM} and source {@value OptimizeConstants.EventSource#DEBUG} * * * @param extensionApi {@link ExtensionApi} instance. @@ -167,6 +174,11 @@ protected void onRegistered() { OptimizeConstants.EventSource.CONTENT_COMPLETE, this::handleUpdatePropositionsCompleted); + getApi().registerEventListener( + EventType.SYSTEM, + OptimizeConstants.EventSource.DEBUG, + this::handleDebugEvent); + eventsDispatcher.start(); } @@ -245,10 +257,78 @@ void handleOptimizeRequestContent(@NonNull final Event event) { handleUpdatePropositions(event); break; case OptimizeConstants.EventDataValues.REQUEST_TYPE_GET: - // Queue the get propositions event in the events dispatcher to ensure any prior - // update requests are completed - // before it is processed. - eventsDispatcher.offer(event); + try { + // Fetch decision scopes from the event + List> decisionScopesData = + DataReader.getTypedListOfMap( + Object.class, + eventData, + OptimizeConstants.EventDataKeys.DECISION_SCOPES); + List eventDecisionScopes = + retrieveValidDecisionScopes(decisionScopesData); + + if (OptimizeUtils.isNullOrEmpty(eventDecisionScopes)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - Cannot process the get propositions" + + " request event, provided list of decision scopes has no" + + " valid scope."); + getApi().dispatch( + createResponseEventWithError( + event, AdobeError.UNEXPECTED_ERROR)); + return; + } + + // Fetch propositions for the decision scopes from the cache + Map fetchedPropositions = new HashMap<>(); + for (DecisionScope scope : eventDecisionScopes) { + if (cachedPropositions.containsKey(scope)) { + fetchedPropositions.put(scope, cachedPropositions.get(scope)); + } + } + + // Check if all scopes are cached and none are in progress + boolean anyScopeInProgress = false; + HashSet scopesInProgress = new HashSet<>(); + for (List updatingScope : + updateRequestEventIdsInProgress.values()) { + scopesInProgress.addAll(updatingScope); + } + for (DecisionScope scope : eventDecisionScopes) { + if (scopesInProgress.contains(scope)) { + anyScopeInProgress = true; + break; + } + } + + if ((fetchedPropositions.size() == eventDecisionScopes.size()) + && !anyScopeInProgress) { + Log.trace( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - All scopes are cached and none are" + + " in progress, dispatching event directly."); + + // Dispatch the event directly + handleGetPropositions(event); + } else { + Log.trace( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - Scopes are not fully cached or are" + + " in progress, adding event to dispatcher."); + eventsDispatcher.offer(event); + } + break; + } catch (final Exception e) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleOptimizeRequestContent - Failed to process get propositions" + + " request event due to an exception (%s)!", + e.getLocalizedMessage()); + } break; case OptimizeConstants.EventDataValues.REQUEST_TYPE_TRACK: handleTrackPropositions(event); @@ -380,10 +460,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) { @@ -784,8 +865,25 @@ void handleGetPropositions(@NonNull final Event event) { } } + final List> previewPropositionsList = new ArrayList<>(); + for (final DecisionScope scope : validScopes) { + if (previewCachedPropositions.containsKey(scope)) { + final OptimizeProposition optimizeProposition = + previewCachedPropositions.get(scope); + previewPropositionsList.add(optimizeProposition.toEventData()); + } + } + final Map responseEventData = new HashMap<>(); - responseEventData.put(OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); + + if (!previewPropositionsList.isEmpty()) { + Log.debug(OptimizeConstants.LOG_TAG, SELF_TAG, "Preview Mode is enabled."); + responseEventData.put( + OptimizeConstants.EventDataKeys.PROPOSITIONS, previewPropositionsList); + } else { + responseEventData.put( + OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); + } final Event responseEvent = new Event.Builder( @@ -894,6 +992,98 @@ void handleTrackPropositions(@NonNull final Event event) { */ void handleClearPropositions(@NonNull final Event event) { cachedPropositions.clear(); + previewCachedPropositions.clear(); + } + + /** + * Handles the event with type {@value EventType#SYSTEM} and source {@value + * OptimizeConstants.EventSource#DEBUG}. + * + *

A debug event allows the optimize extension to processes non-production workflows. + * + * @param event the debug {@link Event} to be handled. + */ + void handleDebugEvent(@NonNull final Event event) { + try { + if (OptimizeUtils.isNullOrEmpty(event.getEventData())) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Ignoring the Optimize Debug event, either event is null" + + " or event data is null/ empty."); + return; + } + + if (!OptimizeUtils.isDebugEvent(event)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Ignoring Optimize Debug event, either handle type is" + + " not com.adobe.eventType.system or source is not" + + " com.adobe.eventSource.debug"); + return; + } + + final Map eventData = event.getEventData(); + + final List> payload = + DataReader.getTypedListOfMap( + Object.class, eventData, OptimizeConstants.Edge.PAYLOAD); + if (OptimizeUtils.isNullOrEmpty(payload)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Cannot process the Debug event, propositions list is" + + " either null or empty in the response."); + return; + } + + final Map propositionsMap = new HashMap<>(); + for (final Map propositionData : payload) { + final OptimizeProposition optimizeProposition = + OptimizeProposition.fromEventData(propositionData); + if (optimizeProposition != null + && !OptimizeUtils.isNullOrEmpty(optimizeProposition.getOffers())) { + final DecisionScope scope = new DecisionScope(optimizeProposition.getScope()); + propositionsMap.put(scope, optimizeProposition); + } + } + + if (OptimizeUtils.isNullOrEmpty(propositionsMap)) { + Log.debug( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Cannot process the Debug event, no propositions with" + + " valid offers are present in the response."); + return; + } + + previewCachedPropositions.putAll(propositionsMap); + + final List> propositionsList = new ArrayList<>(); + for (final OptimizeProposition optimizeProposition : propositionsMap.values()) { + propositionsList.add(optimizeProposition.toEventData()); + } + final Map notificationData = new HashMap<>(); + notificationData.put(OptimizeConstants.EventDataKeys.PROPOSITIONS, propositionsList); + + final Event notificationEvent = + new Event.Builder( + OptimizeConstants.EventNames.OPTIMIZE_NOTIFICATION, + OptimizeConstants.EventType.OPTIMIZE, + OptimizeConstants.EventSource.NOTIFICATION) + .setEventData(notificationData) + .build(); + + // Dispatch notification event + getApi().dispatch(notificationEvent); + } catch (final Exception e) { + Log.warning( + OptimizeConstants.LOG_TAG, + SELF_TAG, + "handleDebugEvent - Cannot process the Debug event due to an exception (%s)!", + e.getLocalizedMessage()); + } } /** @@ -997,6 +1187,17 @@ void setCachedPropositions(final Map cachedP this.cachedPropositions = cachedPropositions; } + @VisibleForTesting + Map getPreviewCachedPropositions() { + return previewCachedPropositions; + } + + @VisibleForTesting + void setPreviewCachedPropositions( + final Map previewCachedPropositions) { + this.previewCachedPropositions = previewCachedPropositions; + } + @VisibleForTesting Map getPropositionsInProgress() { return propositionsInProgress; diff --git a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java index 505e0214..a24e0d66 100644 --- a/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java +++ b/code/optimize/src/main/java/com/adobe/marketing/mobile/optimize/OptimizeUtils.java @@ -14,6 +14,7 @@ import android.util.Base64; import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; import java.util.Collection; @@ -153,6 +154,17 @@ static boolean isEdgeErrorResponseContent(final Event event) { event.getSource()); } + /** + * Checks whether the given event is a Debug Event returned from the Edge network. + * + * @param event instance of {@link Event} + * @return {@code boolean} return true if event is a debug event, false otherwise. + */ + static boolean isDebugEvent(final Event event) { + return EventType.SYSTEM.equalsIgnoreCase(event.getType()) + && OptimizeConstants.EventSource.DEBUG.equalsIgnoreCase(event.getSource()); + } + /** * Checks whether the given event is an Optimize request content event for retrieving cached * propositions. diff --git a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java index 6d3bc72a..9ef0a8b9 100644 --- a/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java +++ b/code/optimize/src/test/java/com/adobe/marketing/mobile/optimize/OptimizeExtensionTests.java @@ -12,6 +12,7 @@ package com.adobe.marketing.mobile.optimize; import android.util.Base64; +import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.ExtensionApi; import com.adobe.marketing.mobile.ExtensionEventListener; @@ -27,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -43,6 +45,8 @@ @SuppressWarnings("unchecked") public class OptimizeExtensionTests { private OptimizeExtension extension; + private Map responseMap; + private AdobeError responseError; // Mocks @Mock ExtensionApi mockExtensionApi; @@ -57,6 +61,12 @@ public void setup() { Mockito.clearInvocations(mockExtensionApi); } + @After + public void teardown() { + responseMap = null; + responseError = null; + } + @Test public void test_getName() { // test @@ -1368,61 +1378,74 @@ public void testHandleEdgeErrorResponse_emptyEventData() { @Test public void testHandleOptimizeRequestContent_GetPropositionsEvent_shouldAddToSerialDispatcher() throws Exception { - extension.setEventsDispatcher(mockEventsDispatcher); - setConfigurationSharedState( - SharedStateStatus.SET, - new HashMap() { - { - put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); - } - }); + try (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])); + extension.setEventsDispatcher(mockEventsDispatcher); + setConfigurationSharedState( + SharedStateStatus.SET, + new HashMap() { + { + put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); + } + }); - final DecisionScope testScope = - new DecisionScope( - "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ=="); - final Map testEventData = new HashMap<>(); - testEventData.put("requesttype", "getpropositions"); - testEventData.put( - "decisionscopes", - new ArrayList>() { - { - add(testScope.toEventData()); - } - }); - final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final DecisionScope testScope = + new DecisionScope( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ=="); + final Map testEventData = new HashMap<>(); + testEventData.put("requesttype", "getpropositions"); + testEventData.put( + "decisionscopes", + new ArrayList>() { + { + add(testScope.toEventData()); + } + }); + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - final Event testEvent = - new Event.Builder( - "Optimize Get Propositions Request", - "com.adobe.eventType.optimize", - "com.adobe.eventSource.requestContent") - .setEventData(testEventData) - .build(); + final Event testEvent = + new Event.Builder( + "Optimize Get Propositions Request", + "com.adobe.eventType.optimize", + "com.adobe.eventSource.requestContent") + .setEventData(testEventData) + .build(); - // test - extension.handleOptimizeRequestContent(testEvent); + // test + extension.handleOptimizeRequestContent(testEvent); - // verify - Mockito.verify(mockEventsDispatcher, Mockito.times(1)).offer(eventCaptor.capture()); - - final Event queuedEvent = eventCaptor.getValue(); - Assert.assertEquals("Optimize Get Propositions Request", queuedEvent.getName()); - Assert.assertEquals("com.adobe.eventType.optimize", queuedEvent.getType()); - Assert.assertEquals("com.adobe.eventSource.requestContent", queuedEvent.getSource()); - - final String requestType = (String) queuedEvent.getEventData().get("requesttype"); - Assert.assertEquals("getpropositions", requestType); - - final List> scopesData = - (List>) queuedEvent.getEventData().get("decisionscopes"); - Assert.assertNotNull(scopesData); - final List scopes = new ArrayList<>(); - for (final Map scopeData : scopesData) { - final DecisionScope scope = DecisionScope.fromEventData(scopeData); - scopes.add(scope); + // verify + Mockito.verify(mockEventsDispatcher, Mockito.times(1)).offer(eventCaptor.capture()); + + final Event queuedEvent = eventCaptor.getValue(); + Assert.assertEquals("Optimize Get Propositions Request", queuedEvent.getName()); + Assert.assertEquals("com.adobe.eventType.optimize", queuedEvent.getType()); + Assert.assertEquals("com.adobe.eventSource.requestContent", queuedEvent.getSource()); + + final String requestType = (String) queuedEvent.getEventData().get("requesttype"); + Assert.assertEquals("getpropositions", requestType); + + final List> scopesData = + (List>) queuedEvent.getEventData().get("decisionscopes"); + Assert.assertNotNull(scopesData); + final List scopes = new ArrayList<>(); + for (final Map scopeData : scopesData) { + final DecisionScope scope = DecisionScope.fromEventData(scopeData); + scopes.add(scope); + } + Assert.assertEquals(1, scopes.size()); + Assert.assertEquals(testScope, scopes.get(0)); } - Assert.assertEquals(1, scopes.size()); - Assert.assertEquals(testScope, scopes.get(0)); } @Test @@ -2209,15 +2232,616 @@ public void testHandleUpdatePropositionsComplete_missingRequestEventIdInData() Assert.assertEquals(1, extension.getUpdateRequestEventIdsInProgress().size()); } - // Helper methods - private void setConfigurationSharedState( - final SharedStateStatus status, final Map data) { - Mockito.when( - mockExtensionApi.getSharedState( - ArgumentMatchers.eq(OptimizeConstants.Configuration.EXTENSION_NAME), - ArgumentMatchers.any(), - ArgumentMatchers.eq(false), - ArgumentMatchers.eq(SharedStateResolution.ANY))) - .thenReturn(new SharedStateResult(status, data)); + @Test + public void testHandleDebugEvent_validProposition() throws Exception { + // setup + final Map edgeResponseData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource("json/EVENT_DATA_EDGE_RESPONSE_VALID.json"), + HashMap.class); + final Event testEvent = + new Event.Builder( + "AEP Response Event Handle (Spoof)", + "com.adobe.eventType.system", + "com.adobe.eventSource.debug") + .setEventData(edgeResponseData) + .build(); + + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + + // test + extension.handleDebugEvent(testEvent); + + // verify + Mockito.verify(mockExtensionApi, Mockito.times(1)).dispatch(eventCaptor.capture()); + + final Event dispatchedEvent = eventCaptor.getValue(); + Assert.assertEquals("com.adobe.eventType.optimize", dispatchedEvent.getType()); + Assert.assertEquals("com.adobe.eventSource.notification", dispatchedEvent.getSource()); + + final List> propositionsList = + (List>) dispatchedEvent.getEventData().get("propositions"); + Assert.assertNotNull(propositionsList); + Assert.assertEquals(1, propositionsList.size()); + + final Map propositionsData = propositionsList.get(0); + Assert.assertNotNull(propositionsData); + final OptimizeProposition optimizeProposition = + OptimizeProposition.fromEventData(propositionsData); + Assert.assertNotNull(optimizeProposition); + + // for debug events propositions should be cached + // in the preview cache and not in the main cache + Assert.assertEquals(0, extension.getCachedPropositions().size()); + Assert.assertEquals(1, extension.getPreviewCachedPropositions().size()); + + Assert.assertEquals("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", optimizeProposition.getId()); + + Map.Entry cachedPropositionEntry = + extension.getPreviewCachedPropositions().entrySet().iterator().next(); + Assert.assertEquals(optimizeProposition.getId(), cachedPropositionEntry.getValue().getId()); + + Assert.assertEquals( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==", + optimizeProposition.getScope()); + Assert.assertTrue(optimizeProposition.getScopeDetails().isEmpty()); + Assert.assertEquals(1, optimizeProposition.getOffers().size()); + + final Offer offer = optimizeProposition.getOffers().get(0); + Assert.assertEquals("xcore:personalized-offer:1111111111111111", offer.getId()); + Assert.assertEquals("10", offer.getEtag()); + Assert.assertEquals( + "https://ns.adobe.com/experience/offer-management/content-component-html", + offer.getSchema()); + Assert.assertEquals(OfferType.HTML, offer.getType()); + Assert.assertEquals("

This is a HTML content

", offer.getContent()); + Assert.assertEquals(1, offer.getCharacteristics().size()); + Assert.assertEquals("true", offer.getCharacteristics().get("testing")); + Assert.assertNull(offer.getLanguage()); + } + + @Test + public void testHandleDebugEvent_getPropositionsForMultipleScopes() throws Exception { + // setup + final Map testPropositionDataA = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource("json/PROPOSITION_VALID.json"), + HashMap.class); + final OptimizeProposition testOptimizePropositionA = + OptimizeProposition.fromEventData(testPropositionDataA); + Assert.assertNotNull(testOptimizePropositionA); + final Map cachedPropositions = new HashMap<>(); + cachedPropositions.put( + new DecisionScope(testOptimizePropositionA.getScope()), testOptimizePropositionA); + + final Map testPropositionDataB = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource("json/PROPOSITION_VALID_B.json"), + HashMap.class); + final OptimizeProposition testOptimizePropositionB = + OptimizeProposition.fromEventData(testPropositionDataB); + Assert.assertNotNull(testOptimizePropositionB); + cachedPropositions.put( + new DecisionScope(testOptimizePropositionB.getScope()), testOptimizePropositionB); + + extension.setCachedPropositions(cachedPropositions); + + // send a debug event with data of Decision Scope B + final Map edgeResponseData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource("json/EVENT_DATA_EDGE_RESPONSE_VALID.json"), + HashMap.class); + final Event testEvent = + new Event.Builder( + "AEP Response Event Handle (Spoof)", + "com.adobe.eventType.system", + "com.adobe.eventSource.debug") + .setEventData(edgeResponseData) + .build(); + + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + + // test + extension.handleDebugEvent(testEvent); + + // verify + Mockito.verify(mockExtensionApi, Mockito.times(1)).dispatch(eventCaptor.capture()); + + final Event dispatchedEvent = eventCaptor.getValue(); + Assert.assertEquals("com.adobe.eventType.optimize", dispatchedEvent.getType()); + Assert.assertEquals("com.adobe.eventSource.notification", dispatchedEvent.getSource()); + + final List> propositionsList = + (List>) dispatchedEvent.getEventData().get("propositions"); + Assert.assertNotNull(propositionsList); + Assert.assertEquals(1, propositionsList.size()); + + final Map propositionsData = propositionsList.get(0); + Assert.assertNotNull(propositionsData); + final OptimizeProposition optimizeProposition = + OptimizeProposition.fromEventData(propositionsData); + Assert.assertNotNull(optimizeProposition); + + // decision scopes A and B should be cached in normal cache + Assert.assertEquals(2, extension.getCachedPropositions().size()); + + // decision scope B should be cached in preview cache + Assert.assertEquals(1, extension.getPreviewCachedPropositions().size()); + Map.Entry previewCachedPropositionEntry = + extension.getPreviewCachedPropositions().entrySet().iterator().next(); + Assert.assertEquals( + "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + previewCachedPropositionEntry.getValue().getId()); + Assert.assertEquals( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==", + previewCachedPropositionEntry.getValue().getScope()); + } + + @Test + public void testHandleDebugEvent_InvalidEventType() throws Exception { + + try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { + // setup + final Map edgeResponseData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource( + "json/EVENT_DATA_EDGE_RESPONSE_VALID.json"), + HashMap.class); + final Event testEvent = + new Event.Builder( + "AEP Response Event Handle (Spoof)", + "com.adobe.eventType.edge", + "com.adobe.eventSource.debug") + .setEventData(edgeResponseData) + .build(); + + // test + extension.handleDebugEvent(testEvent); + + // verify + logMockedStatic.verify( + () -> + Log.debug( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.any())); + } + } + + @Test + public void testHandleDebugEvent_InvalidEventSource() throws Exception { + + try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { + // setup + final Map edgeResponseData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource( + "json/EVENT_DATA_EDGE_RESPONSE_VALID.json"), + HashMap.class); + final Event testEvent = + new Event.Builder( + "AEP Response Event Handle (Spoof)", + "com.adobe.eventType.system", + "com.adobe.eventSource.personalization:decisions") + .setEventData(edgeResponseData) + .build(); + + // test + extension.handleDebugEvent(testEvent); + + // verify + logMockedStatic.verify( + () -> + Log.debug( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.any())); + } + } + + @Test + public void testHandleDebugEvent_emptyPayload() throws Exception { + try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { + // setup + final Map edgeResponseData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource( + "json/EVENT_DATA_EDGE_RESPONSE_EMPTY_PAYLOAD.json"), + HashMap.class); + + final Event testEvent = + new Event.Builder( + "AEP Response Event Handle (Spoof)", + "com.adobe.eventType.system", + "com.adobe.eventSource.debug") + .setEventData(edgeResponseData) + .build(); + + // test + extension.handleDebugEvent(testEvent); + + // verify + logMockedStatic.verify( + () -> + Log.debug( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString())); + } + } + + @Test + public void testHandleDebugEvent_emptyProposition() throws Exception { + try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { + + final Map edgeResponseData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource( + "json/EVENT_DATA_EDGE_RESPONSE_PROPOSITION_WITH_EMPTY_OFFER.json"), + HashMap.class); + + final Event testEvent = + new Event.Builder( + "Test Event", + "com.adobe.eventType.edge", + "com.adobe.eventSource.responseContent") + .setEventData(edgeResponseData) + .build(); + + // test + extension.handleDebugEvent(testEvent); + + // verify + logMockedStatic.verify( + () -> + Log.debug( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.any())); + } + } + + @Test + public void testDebugEvent_nullEventData() { + try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { + + // setup + final Event testEvent = + new Event.Builder( + "AEP Response Event Handle (Spoof)", + "com.adobe.eventType.system", + "com.adobe.eventSource.debug") + .setEventData(null) + .build(); + + // test + extension.handleDebugEvent(testEvent); + + // verify + logMockedStatic.verify( + () -> + Log.debug( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.any())); + } + } + + @Test + public void testDebugEvent_emptyEventData() { + try (MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { + + // setup + final Event testEvent = + new Event.Builder( + "AEP Response Event Handle (Spoof)", + "com.adobe.eventType.system", + "com.adobe.eventSource.debug") + .setEventData(new HashMap<>()) + .build(); + + // test + extension.handleDebugEvent(testEvent); + + // verify + logMockedStatic.verify( + () -> + Log.debug( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.any())); + } + } + + // Helper methods + private void setConfigurationSharedState( + final SharedStateStatus status, final Map data) { + Mockito.when( + mockExtensionApi.getSharedState( + ArgumentMatchers.eq(OptimizeConstants.Configuration.EXTENSION_NAME), + ArgumentMatchers.any(), + ArgumentMatchers.eq(false), + ArgumentMatchers.eq(SharedStateResolution.ANY))) + .thenReturn(new SharedStateResult(status, data)); + } + + @Test + public void testGetPropositions_dispatchPropositionFromCacheBeforeNextUpdate() { + try (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])); + + // setup + setConfigurationSharedState( + SharedStateStatus.SET, + new HashMap() { + { + put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); + } + }); + + extension.setEventsDispatcher(mockEventsDispatcher); + + // prepare update event + final DecisionScope updateScope = + new DecisionScope( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ=="); + final Map testEventData = new HashMap<>(); + testEventData.put("requesttype", "updatepropositions"); + testEventData.put( + "decisionscopes", + new ArrayList>() { + { + add(updateScope.toEventData()); + } + }); + final Event updateEvent = + new Event.Builder( + "Optimize Update Propositions Request", + "com.adobe.eventType.optimize", + "com.adobe.eventSource.requestContent") + .setEventData(testEventData) + .build(); + + // prepare cache data + final Map testPropositionData = + new ObjectMapper() + .readValue( + getClass() + .getClassLoader() + .getResource("json/PROPOSITION_VALID.json"), + HashMap.class); + + final OptimizeProposition testOptimizeProposition = + OptimizeProposition.fromEventData(testPropositionData); + final DecisionScope getPropositionScope = + new DecisionScope(testOptimizeProposition.getScope()); + Assert.assertNotNull(testOptimizeProposition); + final Map cachedPropositions = new HashMap<>(); + cachedPropositions.put(getPropositionScope, testOptimizeProposition); + // update cache + extension.setCachedPropositions(cachedPropositions); + + // simulate update + extension.handleOptimizeRequestContent(updateEvent); + + // verify update in progress + final Map> updateEventIdsInProgress = + extension.getUpdateRequestEventIdsInProgress(); + Assert.assertNotNull(updateEventIdsInProgress); + Assert.assertEquals(1, updateEventIdsInProgress.size()); + Assert.assertTrue( + updateEventIdsInProgress.containsValue( + new ArrayList() { + { + add(updateScope); + } + })); + Mockito.clearInvocations(mockExtensionApi); + + // prepare get event + final Map testGetEventData = new HashMap<>(); + testGetEventData.put("requesttype", "getpropositions"); + testGetEventData.put( + "decisionscopes", + new ArrayList>() { + { + add(getPropositionScope.toEventData()); + } + }); + + final Event testGetEvent = + new Event.Builder( + "Optimize Get Propositions Request", + "com.adobe.eventType.optimize", + "com.adobe.eventSource.requestContent") + .setEventData(testGetEventData) + .build(); + + // prepare update complete event + final Event testUpdateCompleteEvent = + new Event.Builder( + "Optimize Update Propositions Complete", + "com.adobe.eventType.optimize", + "com.adobe.eventSource.contentComplete") + .setEventData( + new HashMap() { + { + put( + "completedUpdateRequestForEventId", + updateEventIdsInProgress.keySet().toArray()[0]); + } + }) + .build(); + + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + + // simulate test + extension.handleOptimizeRequestContent(testGetEvent); + extension.handleUpdatePropositionsCompleted(testUpdateCompleteEvent); + + // verify + Mockito.verify(mockExtensionApi, Mockito.after(2000L).times(1)) + .dispatch(eventCaptor.capture()); + final Event dispatchedEvent = eventCaptor.getValue(); + Assert.assertEquals("Optimize Response", dispatchedEvent.getName()); + Assert.assertEquals("com.adobe.eventType.optimize", dispatchedEvent.getType()); + Assert.assertEquals( + "com.adobe.eventSource.responseContent", dispatchedEvent.getSource()); + final List> propositionsList = + (List>) dispatchedEvent.getEventData().get("propositions"); + final Map cachedPropositionsAfter = + extension.getCachedPropositions(); + Assert.assertEquals(1, cachedPropositionsAfter.size()); + Assert.assertEquals( + getPropositionScope.getName(), propositionsList.get(0).get("scope")); + Assert.assertEquals( + "de03ac85-802a-4331-a905-a57053164d35", propositionsList.get(0).get("id")); + Assert.assertEquals( + cachedPropositionsAfter.get(getPropositionScope).getId(), + propositionsList.get(0).get("id")); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + public void testHandleOptimizeRequestContent_HandleGetPropositions_invalidDecisionScope() { + try (MockedStatic base64MockedStatic = Mockito.mockStatic(Base64.class); + MockedStatic logMockedStatic = Mockito.mockStatic(Log.class)) { + base64MockedStatic + .when( + () -> + Base64.decode( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt())) + .thenAnswer( + (Answer) + invocation -> + java.util.Base64.getDecoder() + .decode((String) invocation.getArguments()[0])); + + // setup + setConfigurationSharedState( + SharedStateStatus.SET, + new HashMap() { + { + put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); + } + }); + + final DecisionScope testScope = + new DecisionScope( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoiIn0="); + final Map testEventData = new HashMap<>(); + testEventData.put("requesttype", "getpropositions"); + testEventData.put( + "decisionscopes", + new ArrayList>() { + { + add(testScope.toEventData()); + } + }); + + final Event testEvent = + new Event.Builder( + "Optimize Get Propositions Request", + "com.adobe.eventType.optimize", + "com.adobe.eventSource.requestContent") + .setEventData(testEventData) + .build(); + + // test + extension.handleOptimizeRequestContent(testEvent); + + // verify + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + Mockito.verify(mockExtensionApi, Mockito.after(2000L).times(1)) + .dispatch(eventCaptor.capture()); + final Event dispatchedEvent = eventCaptor.getValue(); + Assert.assertEquals("Optimize Response", dispatchedEvent.getName()); + Assert.assertEquals("com.adobe.eventType.optimize", dispatchedEvent.getType()); + Assert.assertEquals( + "com.adobe.eventSource.responseContent", dispatchedEvent.getSource()); + Assert.assertNotNull(dispatchedEvent.getEventData().get("responseerror")); + } + } + + @Test + public void testHandleOptimizeRequestContent_HandleGetPropositions_withException() { + // setup + setConfigurationSharedState( + SharedStateStatus.SET, + new HashMap() { + { + put("edge.configId", "ffffffff-ffff-ffff-ffff-ffffffffffff"); + } + }); + + final DecisionScope testScope = + new DecisionScope( + "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ=="); + final Map testEventData = new HashMap<>(); + testEventData.put("requesttype", "getpropositions"); + testEventData.put( + "decisionscopes", + new ArrayList>() { + { + add(testScope.toEventData()); + } + }); + + final Event testEvent = + new Event.Builder( + "Optimize Get Propositions Request", + "com.adobe.eventType.optimize", + "com.adobe.eventSource.requestContent") + .setEventData(testEventData) + .build(); + + // test + extension.handleOptimizeRequestContent(testEvent); + + // verify + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + Mockito.verify(mockExtensionApi, Mockito.never()).dispatch(eventCaptor.capture()); } } 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..c26255c1 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,9 @@ package com.adobe.marketing.mobile.optimize; +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; @@ -896,4 +899,120 @@ public void test_clearCachedPropositions() { Assert.assertNull(event.getEventData()); } } + + @Test + public void testUpdatePropositions_timeoutError() { + + double timeoutSeconds = 0.1; + 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 -> { + Optimize.failWithOptimizeError( + callbackMockEvent, + AEPOptimizeError.Companion.getTimeoutError()); + return null; + }); + + Optimize.updatePropositions(scopes, xdm, data, timeoutSeconds, 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() { + + double timeoutSeconds = 0.1; + 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 -> { + Optimize.failWithOptimizeError( + callbackMockEvent, + AEPOptimizeError.Companion.getTimeoutError()); + return null; + }); + + Optimize.getPropositions(scopes, timeoutSeconds, 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/optimize/src/test/resources/json/EVENT_DATA_EDGE_RESPONSE_PROPOSITION_WITH_EMPTY_OFFER.json b/code/optimize/src/test/resources/json/EVENT_DATA_EDGE_RESPONSE_PROPOSITION_WITH_EMPTY_OFFER.json new file mode 100644 index 00000000..23c3197d --- /dev/null +++ b/code/optimize/src/test/resources/json/EVENT_DATA_EDGE_RESPONSE_PROPOSITION_WITH_EMPTY_OFFER.json @@ -0,0 +1,20 @@ +{ + "payload": [ + { + "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "scope": "eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==", + "activity": { + "etag": "8", + "id": "xcore:offer-activity:1111111111111111" + }, + "placement": { + "etag": "1", + "id": "xcore:offer-placement:1111111111111111" + }, + "items": [] + } + ], + "requestEventId": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA", + "requestId": "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", + "type": "personalization:decisions" +} \ No newline at end of file diff --git a/code/optimize/src/test/resources/json/PROPOSITION_VALID_B.json b/code/optimize/src/test/resources/json/PROPOSITION_VALID_B.json new file mode 100644 index 00000000..ef83c097 --- /dev/null +++ b/code/optimize/src/test/resources/json/PROPOSITION_VALID_B.json @@ -0,0 +1,27 @@ +{ + "id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "items":[ + { + "id":"xcore:personalized-offer:1111111111111111", + "etag":"10", + "schema":"https://ns.adobe.com/experience/offer-management/content-component-html", + "data":{ + "id":"xcore:personalized-offer:1111111111111111", + "format":"text/html", + "content":"

This is a HTML content

", + "characteristics": { + "testing": "true" + } + } + } + ], + "placement":{ + "etag":"1", + "id":"xcore:offer-placement:1111111111111111" + }, + "activity":{ + "etag":"8", + "id":"xcore:offer-activity:1111111111111111" + }, + "scope":"eyJhY3Rpdml0eUlkIjoieGNvcmU6b2ZmZXItYWN0aXZpdHk6MTExMTExMTExMTExMTExMSIsInBsYWNlbWVudElkIjoieGNvcmU6b2ZmZXItcGxhY2VtZW50OjExMTExMTExMTExMTExMTEifQ==" +} \ No newline at end of file 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..19a9ddc9 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, + timeoutSeconds = 0.2 ) }) { 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, + timeoutSeconds = 0.2 + ) }) { 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..2240944d 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 [timeoutSeconds] a [Double] in seconds */ - fun getPropositions(decisionScopes: List) { + fun getPropositions(decisionScopes: List, timeoutSeconds: Double? = 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.") } - - }) + } + timeoutSeconds?.let { seconds -> + Optimize.getPropositions(decisionScopes, seconds, 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 timeoutSeconds a [Double] in seconds */ - 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, + timeoutSeconds: Double? = 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() + timeoutSeconds?.let { seconds -> + Optimize.updatePropositions( + decisionScopes, + xdm, + data, + seconds, + callback + ) + } ?: Optimize.updatePropositions(decisionScopes, xdm, data, callback) } /**