From 3343a262580e848d56eb2df0824afb8ee7392c31 Mon Sep 17 00:00:00 2001 From: Arjun Bhadra Date: Tue, 19 Apr 2022 21:58:23 -0700 Subject: [PATCH 01/17] [MOB-15817] Implemented getUrlVariables public API --- .../mobile/edge/identity/EventUtils.java | 37 + .../mobile/edge/identity/Identity.java | 84 +++ .../edge/identity/IdentityConstants.java | 27 + .../edge/identity/IdentityExtension.java | 138 +++- .../edge/identity/IdentityProperties.java | 2 +- .../mobile/edge/identity/URLUtils.java | 658 ++++++++++++++++++ .../marketing/mobile/edge/identity/Utils.java | 11 + 7 files changed, 954 insertions(+), 3 deletions(-) create mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/URLUtils.java diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/EventUtils.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/EventUtils.java index 0302783d..954518ae 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/EventUtils.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/EventUtils.java @@ -65,6 +65,21 @@ static boolean isRequestIdentityEvent(final Event event) { ); } + /** + * Reads the url variables flag from the event data, returns false if not present + * + * @param event the event to verify + * @return true if urlVariables key is present in the event data + */ + static boolean isRequestIdentityEventForGetUrlVariable(final Event event) { + boolean isUrlVariablesEvent = false; + if (event == null || event.getEventData() == null) { + return isUrlVariablesEvent; + } + + return event.getEventData().containsKey(IdentityConstants.EventDataKeys.URL_VARIABLES); + } + /** * Checks if the provided {@code event} is of type {@link IdentityConstants.EventType#GENERIC_IDENTITY} and source {@link IdentityConstants.EventSource#REQUEST_RESET} * @@ -127,4 +142,26 @@ static ECID getECID(final Map identityDirectSharedState) { return legacyEcid; } + + static String getOrgId(final Map configurationSharedState) { + String orgId = null; + + if (configurationSharedState == null) return orgId; + + try { + orgId = + (String) configurationSharedState.get( + IdentityConstants.SharedState.Configuration.EXPERIENCE_CLOUD_ORGID + ); + } catch (ClassCastException e) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + "EventUtils - Failed to extract Experience ORG ID from Configuration shared state, expected String: " + + e.getLocalizedMessage() + ); + } + + return orgId; + } } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Identity.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Identity.java index 85de72e9..ccd8c623 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Identity.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Identity.java @@ -21,7 +21,9 @@ import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Defines the public APIs for the AEP Edge Identity extension. @@ -136,6 +138,88 @@ public void call(Event responseEvent) { ); } + /** + *Returns the identifiers in URL query parameter format for consumption in hybrid mobile applications. + * There is no leading & or ? punctuation as the caller is responsible for placing the variables in their resulting URL in the correct locations. + * If an error occurs while retrieving the URL variables, the completion handler is called with a nil value and AEPError instance. + * Otherwise, the encoded string is returned, for ex: "adobe_mc=TS%3DTIMESTAMP_VALUE%7CMCMID%3DYOUR_ECID%7CMCORGID%3D9YOUR_EXPERIENCE_CLOUD_ID" + * The `adobe_mc` attribute is an URL encoded list that contains: + * - TS a timestamp taken when the request was made + * - MCMID Experience Cloud ID (ECID) + * - MCORGID: Experience Cloud Org ID + * + * @param callback {@link AdobeCallback} of {@code String} invoked with a value value containing the identifiers in query parameter format. + * If an {@link AdobeCallbackWithError} is provided, an {@link AdobeError} can be returned in the + * eventuality of any error that occurred while getting the identifiers query string + */ + public static void getUrlVariables(final AdobeCallback callback) { + if (callback == null) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + "Identity - Unexpected null callback, provide a callback to retrieve current visitor identifiers (URLVariables) query string." + ); + return; + } + + final Event event = new Event.Builder( + IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .setEventData( + new HashMap() { + { + put(IdentityConstants.EventDataKeys.URL_VARIABLES, true); + } + } + ) + .build(); + + final ExtensionErrorCallback errorCallback = new ExtensionErrorCallback() { + @Override + public void error(final ExtensionError extensionError) { + returnError(callback, extensionError); + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + String.format( + "Identity - Failed to dispatch %s event: Error : %s.", + IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, + extensionError.getErrorName() + ) + ); + } + }; + + MobileCore.dispatchEventWithResponseCallback( + event, + new AdobeCallback() { + @Override + public void call(Event responseEvent) { + if (responseEvent == null || responseEvent.getEventData() == null) { + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + + final Map data = responseEvent.getEventData(); + try { + String urlVariableString = (String) data.get(IdentityConstants.EventDataKeys.URL_VARIABLES); + if (urlVariableString == null) { + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + callback.call(urlVariableString); + } catch (ClassCastException e) { + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + } + }, + errorCallback + ); + } + /** * Updates the currently known {@link IdentityMap} within the SDK. * The Identity extension will merge the received identifiers with the previously saved one in an additive manner, diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityConstants.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityConstants.java index b6bc6640..b6991681 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityConstants.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityConstants.java @@ -42,10 +42,19 @@ final class EventType { private EventType() {} } + final class EventDataKeys { + + static final String URL_VARIABLES = "urlvariables"; + + private EventDataKeys() {} + } + final class EventNames { static final String IDENTITY_REQUEST_IDENTITY_ECID = "Edge Identity Request ECID"; + static final String IDENTITY_REQUEST_URL_VARIABLES = "Edge Identity Request URL Variables"; static final String IDENTITY_RESPONSE_CONTENT_ONE_TIME = "Edge Identity Response Content One Time"; + static final String IDENTITY_RESPONSE_URL_VARIABLES = "Edge Identity Response URL Variables"; static final String UPDATE_IDENTITIES = "Edge Identity Update Identities"; static final String REMOVE_IDENTITIES = "Edge Identity Remove Identities"; static final String REQUEST_IDENTITIES = "Edge Identity Request Identities"; @@ -66,6 +75,14 @@ final class Hub { private Hub() {} } + final class Configuration { + + static final String NAME = "com.adobe.module.configuration"; + static final String EXPERIENCE_CLOUD_ORGID = "experienceCloud.org"; + + private Configuration() {} + } + final class IdentityDirect { static final String NAME = "com.adobe.module.identity"; @@ -106,5 +123,15 @@ final class DataStoreKey { private DataStoreKey() {} } + final class UrlKeys { + + static final String TS = "TS"; + static final String EXPERIENCE_CLOUD_ORG_ID = "MCORGID"; + static final String EXPERIENCE_CLOUD_ID = "MCMID"; + static final String PAYLOAD = "adobe_mc"; + + private UrlKeys() {} + } + private IdentityConstants() {} } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityExtension.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityExtension.java index 0a0b2ded..666d4ef2 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityExtension.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityExtension.java @@ -20,6 +20,7 @@ import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; @@ -112,6 +113,12 @@ public void error(final ExtensionError extensionError) { ListenerIdentityRequestReset.class, listenerErrorCallback ); + extensionApi.registerEventListener( + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_CONTENT, + ListenerIdentityRequestReset.class, + listenerErrorCallback + ); } /** @@ -160,7 +167,11 @@ void processCachedEvents() { final Event event = cachedEvents.peek(); if (EventUtils.isRequestIdentityEvent(event)) { - handleIdentityRequest(event); + if (EventUtils.isRequestIdentityEventForGetUrlVariable(event)) { + handleUrlVariablesRequest(event); + } else { + handleIdentityRequest(event); + } } else if (EventUtils.isUpdateIdentityEvent(event)) { handleUpdateIdentities(event); } else if (EventUtils.isRemoveIdentityEvent(event)) { @@ -236,6 +247,128 @@ public void error(ExtensionError extensionError) { return state.bootupIfReady(callback); } + /** + * Handles events requesting for formatted and encoded identifiers url for hybrid apps. + * + * @param event the identity request {@link Event} + */ + void handleUrlVariablesRequest(final Event event) { + Event emptyResponseEvent = new Event.Builder( + IdentityConstants.EventNames.IDENTITY_RESPONSE_URL_VARIABLES, + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.RESPONSE_IDENTITY + ) + .setEventData( + new HashMap() { + { + put(IdentityConstants.EventDataKeys.URL_VARIABLES, null); + } + } + ) + .build(); + + final Map configurationState = getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event + ); + + final String orgId = EventUtils.getOrgId(configurationState); + + if (orgId == null) { + MobileCore.log( + LoggingMode.WARNING, + LOG_TAG, + "IdentityExtension - Cannot process getUrlVariables request Identity event, Experience Cloud Org ID not found in configuration." + ); + MobileCore.dispatchResponseEvent( + emptyResponseEvent, + event, + new ExtensionErrorCallback() { + @Override + public void error(ExtensionError extensionError) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + "IdentityExtension - Failed to dispatch Edge Identity response event for event " + + event.getUniqueIdentifier() + + " with error " + + extensionError.getErrorName() + ); + } + } + ); + return; + } + + final long ts = Utils.getUnixTimeInSeconds(); + final String ecid = state.getIdentityProperties().getECID().toString(); + + if (Utils.isNullOrEmpty(ecid)) { + MobileCore.log( + LoggingMode.WARNING, + LOG_TAG, + "IdentityExtension - Cannot process getUrlVariables request Identity event, ECID not found." + ); + MobileCore.dispatchResponseEvent( + emptyResponseEvent, + event, + new ExtensionErrorCallback() { + @Override + public void error(ExtensionError extensionError) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + "IdentityExtension - Failed to dispatch Edge Identity response event for event " + + event.getUniqueIdentifier() + + " with error " + + extensionError.getErrorName() + ); + } + } + ); + return; + } + + final String urlVariableEncodedString = URLUtils.generateURLVariablesPayload(String.valueOf(ts), ecid, orgId); + + final Event responseEvent = new Event.Builder( + IdentityConstants.EventNames.IDENTITY_RESPONSE_URL_VARIABLES, + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.RESPONSE_IDENTITY + ) + .setEventData( + new HashMap() { + { + put(IdentityConstants.EventDataKeys.URL_VARIABLES, urlVariableEncodedString); + } + } + ) + .build(); + + MobileCore.log( + LoggingMode.WARNING, + LOG_TAG, + "IdentityExtension - Cannot process getUrlVariables request Identity event, ECID not found." + ); + MobileCore.dispatchResponseEvent( + responseEvent, + event, + new ExtensionErrorCallback() { + @Override + public void error(ExtensionError extensionError) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + "IdentityExtension - Failed to dispatch Edge Identity response event for event " + + event.getUniqueIdentifier() + + " with error " + + extensionError.getErrorName() + ); + } + } + ); + } + /** * Handles update identity requests to add/update customer identifiers. * @@ -435,7 +568,8 @@ public void error(final ExtensionError extensionError) { LoggingMode.DEBUG, LOG_TAG, String.format( - "IdentityExtension - Failed getting direct Identity shared state. Error : %s.", + "IdentityExtension - Failed getting %s shared state. Error : %s.", + stateOwner, extensionError.getErrorName() ) ); diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityProperties.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityProperties.java index 6cbafec8..d93ca430 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityProperties.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityProperties.java @@ -62,7 +62,7 @@ void setECID(final ECID newEcid) { identityMap.removeItem(previousECIDItem, IdentityConstants.Namespaces.ECID); } - // if primary ecid is null, clear off all the existing ECID's + // if primary ECID is null, clear off all the existing ECID's if (newEcid == null) { setECIDSecondary(null); identityMap.clearItemsForNamespace(IdentityConstants.Namespaces.ECID); diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/URLUtils.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/URLUtils.java new file mode 100644 index 00000000..e3bb620c --- /dev/null +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/URLUtils.java @@ -0,0 +1,658 @@ +/* + Copyright 2022 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.edge.identity; + +import com.adobe.marketing.mobile.*; +import com.adobe.marketing.mobile.MobileCore; +import java.io.UnsupportedEncodingException; + +public class URLUtils { + + static final String LOG_TAG = "URLUtils"; + + // lookup tables used by urlEncode + private static final String[] encodedChars = new String[] { + "%00", + "%01", + "%02", + "%03", + "%04", + "%05", + "%06", + "%07", + "%08", + "%09", + "%0A", + "%0B", + "%0C", + "%0D", + "%0E", + "%0F", + "%10", + "%11", + "%12", + "%13", + "%14", + "%15", + "%16", + "%17", + "%18", + "%19", + "%1A", + "%1B", + "%1C", + "%1D", + "%1E", + "%1F", + "%20", + "%21", + "%22", + "%23", + "%24", + "%25", + "%26", + "%27", + "%28", + "%29", + "%2A", + "%2B", + "%2C", + "-", + ".", + "%2F", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "%3A", + "%3B", + "%3C", + "%3D", + "%3E", + "%3F", + "%40", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "%5B", + "%5C", + "%5D", + "%5E", + "_", + "%60", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "%7B", + "%7C", + "%7D", + "~", + "%7F", + "%80", + "%81", + "%82", + "%83", + "%84", + "%85", + "%86", + "%87", + "%88", + "%89", + "%8A", + "%8B", + "%8C", + "%8D", + "%8E", + "%8F", + "%90", + "%91", + "%92", + "%93", + "%94", + "%95", + "%96", + "%97", + "%98", + "%99", + "%9A", + "%9B", + "%9C", + "%9D", + "%9E", + "%9F", + "%A0", + "%A1", + "%A2", + "%A3", + "%A4", + "%A5", + "%A6", + "%A7", + "%A8", + "%A9", + "%AA", + "%AB", + "%AC", + "%AD", + "%AE", + "%AF", + "%B0", + "%B1", + "%B2", + "%B3", + "%B4", + "%B5", + "%B6", + "%B7", + "%B8", + "%B9", + "%BA", + "%BB", + "%BC", + "%BD", + "%BE", + "%BF", + "%C0", + "%C1", + "%C2", + "%C3", + "%C4", + "%C5", + "%C6", + "%C7", + "%C8", + "%C9", + "%CA", + "%CB", + "%CC", + "%CD", + "%CE", + "%CF", + "%D0", + "%D1", + "%D2", + "%D3", + "%D4", + "%D5", + "%D6", + "%D7", + "%D8", + "%D9", + "%DA", + "%DB", + "%DC", + "%DD", + "%DE", + "%DF", + "%E0", + "%E1", + "%E2", + "%E3", + "%E4", + "%E5", + "%E6", + "%E7", + "%E8", + "%E9", + "%EA", + "%EB", + "%EC", + "%ED", + "%EE", + "%EF", + "%F0", + "%F1", + "%F2", + "%F3", + "%F4", + "%F5", + "%F6", + "%F7", + "%F8", + "%F9", + "%FA", + "%FB", + "%FC", + "%FD", + "%FE", + "%FF", + }; + + private static final int ALL_BITS_ENABLED = 0xFF; + private static final boolean[] utf8Mask = new boolean[] { + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + false, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + }; + + /** + * Helper function to generate url variables in format acceptable by the AEP web SDKs + * + * @param ts timestamp {@link String} denoting time when url variables request was made + * @param ecid Experience Cloud identifier {@link String} generated by the SDK + * @param orgId Experience Cloud Org identifier {@link String} set in the configuration + * @return {@link String} formatted with the visitor id payload + */ + static String generateURLVariablesPayload(final String ts, final String ecid, final String orgId) { + final StringBuilder urlFragment = new StringBuilder(); + + // construct the adobe_mc string + + // append timestamp + String theIdString = appendKVPToVisitorIdString(null, IdentityConstants.UrlKeys.TS, ts); + + // append ecid + theIdString = appendKVPToVisitorIdString(theIdString, IdentityConstants.UrlKeys.EXPERIENCE_CLOUD_ID, ecid); + + // add Experience Cloud Org ID + theIdString = appendKVPToVisitorIdString(theIdString, IdentityConstants.UrlKeys.EXPERIENCE_CLOUD_ORG_ID, orgId); + + // after the adobe_mc string is created, encode the idString before adding it to the url + urlFragment.append(IdentityConstants.UrlKeys.PAYLOAD); + urlFragment.append("="); + + String encodedIdString = urlEncode(theIdString); + if (encodedIdString != null) { + urlFragment.append(encodedIdString); + } + + return urlFragment.toString(); + } + + /** + * Takes in a key-value pair and appends it to the source string + *

+ * This method does not URL encode the provided {@code value} on the resulting string. + * If encoding is needed, make sure that the values are encoded before being passed into this function. + * + * @param originalString {@link String} to append the key and value to + * @param key key to append + * @param value value to append + * + * @return a new string with the key and value appended, or {@code originalString} + * if {@code key} or {@code value} are null or empty + */ + static String appendKVPToVisitorIdString(final String originalString, final String key, final String value) { + // quickly return original string if key or value are empty + if (Utils.isNullOrEmpty(key) || Utils.isNullOrEmpty(value)) { + return originalString; + } + + // get the value for the new variable + final String newUrlVariable = String.format("%s=%s", key, value); + + // if the original string is not empty, we need to append a pipe before we return + if (Utils.isNullOrEmpty(originalString)) { + return newUrlVariable; + } else { + return String.format("%s|%s", originalString, newUrlVariable); + } + } + + /** + * Encodes an URL given as {@code String}. + * + * @param unencodedString nullable {@link String} value to be encoded + * @return the encoded {@code String} + */ + static String urlEncode(final String unencodedString) { + // bail fast + if (unencodedString == null) { + return null; + } + + try { + final byte[] stringBytes = unencodedString.getBytes("UTF-8"); + final int len = stringBytes.length; + int curIndex = 0; + + // iterate looking for any characters that don't match our "safe" mask + while (curIndex < len && utf8Mask[stringBytes[curIndex] & ALL_BITS_ENABLED]) { + curIndex++; + } + + // if our iterator got all the way to the end of the string, no unsafe characters existed + // and it's safe to return the original value that was passed in + if (curIndex == len) { + return unencodedString; + } + + // if we get here we know there's at least one character we need to encode + final StringBuilder encodedString = new StringBuilder(stringBytes.length << 1); + + // if i > than 1 then we have some characters we can just "paste" in + if (curIndex > 0) { + encodedString.append(new String(stringBytes, 0, curIndex, "UTF-8")); + } + + // rip through the rest of the string character by character + for (; curIndex < len; curIndex++) { + encodedString.append(encodedChars[stringBytes[curIndex] & ALL_BITS_ENABLED]); + } + + // return the completed string + return encodedString.toString(); + } catch (UnsupportedEncodingException e) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + String.format("Failed to url encode string %s (%s)", unencodedString, e) + ); + return null; + } + } +} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Utils.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Utils.java index 1f108a5c..cf386d3b 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Utils.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/Utils.java @@ -198,4 +198,15 @@ static List> deepCopy(final List> listOf return deepCopy; } + + private static final long MILLISECONDS_PER_SECOND = 1000L; + + /** + * Gets current unix timestamp in seconds. + * + * @return {code long} current timestamp + */ + static long getUnixTimeInSeconds() { + return System.currentTimeMillis() / MILLISECONDS_PER_SECOND; + } } From 64752c0f5653fcbe59ef3df017d19138d311e1f9 Mon Sep 17 00:00:00 2001 From: Arjun Bhadra Date: Tue, 19 Apr 2022 21:59:16 -0700 Subject: [PATCH 02/17] [MOB-15817] Added unit tests for getUrlVariable implementation logic --- .../edge/identity/IdentityExtensionTests.java | 164 +++++++++++++++ .../mobile/edge/identity/IdentityTests.java | 191 ++++++++++++++++++ .../mobile/edge/identity/URLUtilsTests.java | 31 +++ 3 files changed, 386 insertions(+) create mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/URLUtilsTests.java diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityExtensionTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityExtensionTests.java index 42dad796..1e1f8d9a 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityExtensionTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityExtensionTests.java @@ -15,6 +15,7 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.buildUpdateIdentityRequest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -471,6 +472,152 @@ public void test_handleHubSharedState_doesNotShareStateIfLegacyECIDDoesNotChange .setXDMSharedEventState(sharedStateCaptor.capture(), eq(event), any(ExtensionErrorCallback.class)); } + // ======================================================================================== + // handleUrlVariablesRequest + // ======================================================================================== + + @Test + public void test_handleUrlVariablesRequest_nullEvent_shouldNotThrow() { + // test + extension.handleUrlVariablesRequest(null); + } + + @Test + public void test_handleUrlVariablesRequest_whenConfigAndEcidNotPresent_returnsNull() { + // setup + Event event = new Event.Builder( + "Test event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .build(); + final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor requestEventCaptor = ArgumentCaptor.forClass(Event.class); + + // test + extension.handleUrlVariablesRequest(event); + + // verify + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchResponseEvent( + responseEventCaptor.capture(), + requestEventCaptor.capture(), + any(ExtensionErrorCallback.class) + ); + + Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); + final Map data = urlVariablesResponseEvent.getEventData(); + String urlvariables = (String) data.get("urlvariables"); + + assertNull(urlvariables); + } + + @Test + public void test_handleUrlVariablesRequest_whenOrgIdAndECIDPresent_returnsValidUrlVariablesString() { + // setup + ECID testECID = new ECID(); + extension.state.getIdentityProperties().setECID(testECID); + setConfigurationSharedState("test-org-id@AdobeOrg"); + + Event event = new Event.Builder( + "Test event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .build(); + final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor requestEventCaptor = ArgumentCaptor.forClass(Event.class); + + // test + extension.handleUrlVariablesRequest(event); + + // verify + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchResponseEvent( + responseEventCaptor.capture(), + requestEventCaptor.capture(), + any(ExtensionErrorCallback.class) + ); + + Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); + final Map data = urlVariablesResponseEvent.getEventData(); + String urlvariables = (String) data.get("urlvariables"); + + String expectedUrlVariableTSString = "adobe_mc=TS%3"; + String expectedUrlVariableIdentifiersString = + "%7C" + "MCMID" + "%3D" + testECID.toString() + "%7C" + "MCORGID" + "%3D" + "test-org-id%40AdobeOrg"; + + assertNotNull(urlvariables); + assertTrue(urlvariables.contains("adobe_mc=")); + assertTrue(urlvariables.contains(expectedUrlVariableTSString)); + assertTrue(urlvariables.contains(expectedUrlVariableIdentifiersString)); + } + + @Test + public void test_handleUrlVariablesRequest_whenOrgIdMissing_returnsValidNull() { + // setup + ECID testECID = new ECID(); + extension.state.getIdentityProperties().setECID(testECID); + + Event event = new Event.Builder( + "Test event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .build(); + final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor requestEventCaptor = ArgumentCaptor.forClass(Event.class); + + // test + extension.handleUrlVariablesRequest(event); + + // verify + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchResponseEvent( + responseEventCaptor.capture(), + requestEventCaptor.capture(), + any(ExtensionErrorCallback.class) + ); + + Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); + final Map data = urlVariablesResponseEvent.getEventData(); + String urlvariables = (String) data.get("urlvariables"); + + assertNull(urlvariables); + } + + @Test + public void test_handleUrlVariablesRequest_whenECIDMissing_returnsValidNull() { + // setup + setConfigurationSharedState("test-org-id@AdobeOrg"); + + Event event = new Event.Builder( + "Test event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .build(); + final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor requestEventCaptor = ArgumentCaptor.forClass(Event.class); + + // test + extension.handleUrlVariablesRequest(event); + + // verify + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchResponseEvent( + responseEventCaptor.capture(), + requestEventCaptor.capture(), + any(ExtensionErrorCallback.class) + ); + + Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); + final Map data = urlVariablesResponseEvent.getEventData(); + String urlvariables = (String) data.get("urlvariables"); + + assertNull(urlvariables); + } + // ======================================================================================== // handleUpdateIdentities // ======================================================================================== @@ -648,4 +795,21 @@ private void setIdentityDirectSharedState(final String ecid) { } ); } + + private void setConfigurationSharedState(final String orgId) { + when( + mockExtensionApi.getSharedEventState( + eq("com.adobe.module.configuration"), + any(Event.class), + any(ExtensionErrorCallback.class) + ) + ) + .thenReturn( + new HashMap() { + { + put("experienceCloud.org", orgId); + } + } + ); + } } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTests.java index d27f9e5c..93bc53b3 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTests.java @@ -16,6 +16,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import android.util.Log; import com.adobe.marketing.mobile.AdobeCallback; import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; @@ -269,6 +270,186 @@ public void call(Object o) {} assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); } + // ======================================================================================== + // getUrlVariables API + // ======================================================================================== + @Test + public void testGetUrlVariables() { + // setup + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final ArgumentCaptor extensionErrorCallbackCaptor = ArgumentCaptor.forClass( + ExtensionErrorCallback.class + ); + final List callbackReturnValues = new ArrayList<>(); + + // test + Identity.getUrlVariables( + new AdobeCallback() { + @Override + public void call(String s) { + callbackReturnValues.add(s); + } + } + ); + + // verify + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + adobeCallbackCaptor.capture(), + extensionErrorCallbackCaptor.capture() + ); + + // verify the dispatched event details + Event dispatchedEvent = eventCaptor.getValue(); + assertEquals(IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, dispatchedEvent.getName()); + assertEquals(IdentityConstants.EventType.EDGE_IDENTITY.toLowerCase(), dispatchedEvent.getType()); + assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY.toLowerCase(), dispatchedEvent.getSource()); + assertTrue(dispatchedEvent.getEventData().containsKey("urlvariables")); + + // verify callback responses + Map urlVariablesResponse = new HashMap<>(); + urlVariablesResponse.put("urlvariables", "test-url-variable-string"); + + adobeCallbackCaptor.getValue().call(buildUrlVariablesResponseEvent(urlVariablesResponse)); + assertEquals("test-url-variable-string", callbackReturnValues.get(0)); + // TODO - enable when ExtensionError creation is available + // should not crash on calling the callback + //extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); + } + + @Test + public void testGetUrlVariables_nullCallback() { + // test + Identity.getExperienceCloudId(null); + + // verify + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); + MobileCore.dispatchEventWithResponseCallback( + any(Event.class), + any(AdobeCallback.class), + any(ExtensionErrorCallback.class) + ); + } + + @Test + public void testGetUrlVariables_nullResponseEvent() { + // setup + final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final Map errorCapture = new HashMap<>(); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + } + + @Override + public void call(Object o) {} + }; + + // test + Identity.getUrlVariables(callbackWithError); + + // verify if the event is dispatched + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchEventWithResponseCallback( + any(Event.class), + adobeCallbackCaptor.capture(), + any(ExtensionErrorCallback.class) + ); + + // set response event to null + adobeCallbackCaptor.getValue().call(null); + + // verify + assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + } + + @Test + public void testGetUrlVariables_invalidEventData() { + // setup + final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final Map errorCapture = new HashMap<>(); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + } + + @Override + public void call(Object o) {} + }; + + // test + Identity.getUrlVariables(callbackWithError); + + // verify if the event is dispatched + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchEventWithResponseCallback( + any(Event.class), + adobeCallbackCaptor.capture(), + any(ExtensionErrorCallback.class) + ); + + // set response event data to not have urlvariables key + Map eventData = new HashMap<>(); + eventData.put("someKey", "someValue"); + adobeCallbackCaptor.getValue().call(buildUrlVariablesResponseEvent(eventData)); + + // verify + assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + } + + @Test + public void testGetUrlVariables_NullUrlVariablesStringInResponseData() { + // setup + final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final Map errorCapture = new HashMap<>(); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + } + + @Override + public void call(Object o) { + Log.d("test", "test"); + } + }; + + // test + Identity.getUrlVariables(callbackWithError); + + // verify if the event is dispatched + PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); + MobileCore.dispatchEventWithResponseCallback( + any(Event.class), + adobeCallbackCaptor.capture(), + any(ExtensionErrorCallback.class) + ); + + // set response event to have urlvariables map to null value + Map nullUrlVariablesData = new HashMap<>(); + nullUrlVariablesData.put("urlvariables", null); + adobeCallbackCaptor.getValue().call(buildUrlVariablesResponseEvent(nullUrlVariablesData)); + + // verify + assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + } + // ======================================================================================== // updateIdentities API // ======================================================================================== @@ -581,4 +762,14 @@ private Event buildIdentityResponseEvent(final Map eventData) { .setEventData(eventData) .build(); } + + private Event buildUrlVariablesResponseEvent(final Map eventData) { + return new Event.Builder( + IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.RESPONSE_IDENTITY + ) + .setEventData(eventData) + .build(); + } } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/URLUtilsTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/URLUtilsTests.java new file mode 100644 index 00000000..f735614f --- /dev/null +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/URLUtilsTests.java @@ -0,0 +1,31 @@ +/* + Copyright 2022 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.edge.identity; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class URLUtilsTests { + + @Test + public void test_generateURLVariablesPayload_emptyValuesPassed_returnsStringWithURLPrefixOnly() { + String actual = URLUtils.generateURLVariablesPayload("", "", ""); + assertEquals("adobe_mc=", actual); + } + + @Test + public void test_generateURLVariablesPayload_validStringValuesPassed_returnsStringWith_TS_ECID_ORGID() { + String actual = URLUtils.generateURLVariablesPayload("TEST_TS", "TEST_ECID", "TEST_ORGID"); + assertEquals("adobe_mc=TS%3DTEST_TS%7CMCMID%3DTEST_ECID%7CMCORGID%3DTEST_ORGID", actual); + } +} From 4323496e7ed664597e139838470eea3f1299d7fc Mon Sep 17 00:00:00 2001 From: Arjun Bhadra Date: Tue, 19 Apr 2022 21:59:43 -0700 Subject: [PATCH 03/17] [MOB-15817] Updated sample app with getUrlVariable public API --- .../identity/app/EdgeIdentityApplication.kt | 2 +- .../identity/app/model/SharedViewModel.kt | 7 ++++++ .../identity/app/ui/GetIdentityFragment.kt | 23 +++++++++++++++++ .../main/res/layout/fragment_get_identity.xml | 25 +++++++++++++++++++ code/app/src/main/res/values/strings.xml | 1 + 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/EdgeIdentityApplication.kt b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/EdgeIdentityApplication.kt index 5ecd1e90..f402797e 100644 --- a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/EdgeIdentityApplication.kt +++ b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/EdgeIdentityApplication.kt @@ -20,7 +20,7 @@ import com.adobe.marketing.mobile.edge.identity.Identity class EdgeIdentityApplication : Application() { // Add your Launch Environment ID to configure the SDK from your Launch property - private var LAUNCH_ENVIRONMENT_ID: String = "" + private var LAUNCH_ENVIRONMENT_ID: String = "94f571f308d5/b2ed191ae7bf/launch-abc96d26de57" override fun onCreate() { super.onCreate() diff --git a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/model/SharedViewModel.kt b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/model/SharedViewModel.kt index 77cac6d4..223c89ba 100644 --- a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/model/SharedViewModel.kt +++ b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/model/SharedViewModel.kt @@ -32,6 +32,9 @@ class SharedViewModel : ViewModel() { private val _ecidLegacyText = MutableLiveData("") val ecidLegacyText: LiveData = _ecidLegacyText + private val _urlVariablesText = MutableLiveData("") + val urlVariablesText: LiveData = _urlVariablesText + private val _identitiesText = MutableLiveData("") val identitiesText: LiveData = _identitiesText @@ -43,6 +46,10 @@ class SharedViewModel : ViewModel() { _ecidLegacyText.value = value } + fun setUrlVariablesValue(value: String) { + _urlVariablesText.value = value + } + fun setIdentitiesValue(value: String) { _identitiesText.value = value } diff --git a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/GetIdentityFragment.kt b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/GetIdentityFragment.kt index 7198f7f0..afd9753d 100644 --- a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/GetIdentityFragment.kt +++ b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/GetIdentityFragment.kt @@ -54,6 +54,14 @@ class GetIdentityFragment : Fragment() { } ) + val urlVariablesTextView = root.findViewById(R.id.text_url_variables) + sharedViewModel.urlVariablesText.observe( + viewLifecycleOwner, + Observer { + urlVariablesTextView.text = it + } + ) + val identitiesTextView = root.findViewById(R.id.text_identities) sharedViewModel.identitiesText.observe( viewLifecycleOwner, @@ -85,6 +93,21 @@ class GetIdentityFragment : Fragment() { sharedViewModel.setEcidLegacyValue(if (resultSecondary != null) "legacy: $resultSecondary" else "") } + root.findViewById