Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read ECID from IdentityDirect on Boot when registered #40

Merged
merged 12 commits into from
Apr 8, 2021
2 changes: 1 addition & 1 deletion code/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

<resources>
<string name="app_name">App</string>
<string name="app_name">Edge Identity App</string>
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>
<string name="nav_header_title">Edge Identity Test App</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,34 @@ public void testECID_loadsIdentityDirectECID() throws Exception {
}

@Test
public void testECID_whenBothExtensionRegistered() throws Exception {
public void testECID_whenBothExtensionRegistered_install() throws Exception {
// setup
registerBothIdentityExtensions();
registerBothIdentityExtensions(); // no ECID exists before this step

String directECID = getIdentityDirectECIDSync();
String edgeECID = getExperienceCloudIdSync();

// verify ECID
verifyPrimaryECID(directECID);
verifySecondaryECID(null);
assertEquals(directECID, edgeECID);
}

@Test
public void testECID_whenBothExtensionRegistered_migrationPath() throws Exception {
// setup
String existingECID = "legacyECID";
setIdentityDirectPersistedECID(existingECID);
registerBothIdentityExtensions();

String directECID = getIdentityDirectECIDSync();
String edgeECID = getExperienceCloudIdSync();

// verify ECID
verifyPrimaryECID(edgeECID);
verifySecondaryECID(directECID);
verifyPrimaryECID(directECID);
verifySecondaryECID(null);
assertEquals(directECID, edgeECID);
assertEquals(existingECID, edgeECID);
}

@Test
Expand Down Expand Up @@ -165,23 +183,20 @@ public void testECID_AreDifferentAfterResetIdentitiesAndPrivacyChange() throws E
@Test
public void testECID_DirectEcidIsRemovedOnPrivacyOptOut() throws Exception {
// setup
setIdentityDirectPersistedECID("legacyECID");
setEdgeIdentityPersistence(CreateIdentityMap("ECID", "edgeECID").asXDMMap());
registerBothIdentityExtensions();

String edgeECID = getExperienceCloudIdSync();
String directECID = getIdentityDirectECIDSync();

// verify ECID
verifyPrimaryECID(edgeECID);
verifySecondaryECID(directECID);

verifyPrimaryECID("edgeECID");
verifySecondaryECID("legacyECID");

// Set privacy opted-out
MobileCore.setPrivacyStatus(MobilePrivacyStatus.OPT_OUT);
TestHelper.waitForThreads(2000);
edgeECID = getExperienceCloudIdSync();

// verify that the secondary ECID is removed
verifyPrimaryECID(edgeECID);
verifyPrimaryECID("edgeECID");
verifySecondaryECID(null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.adobe.marketing.mobile.TestHelper.getXDMSharedStateFor;
import static com.adobe.marketing.mobile.TestHelper.resetTestExpectations;
Expand All @@ -45,7 +46,7 @@ public void call(Object o) {
}
});

latch.await();
latch.await(1000, TimeUnit.MILLISECONDS);
TestHelper.waitForThreads(2000);
resetTestExpectations();
}
Expand All @@ -58,7 +59,7 @@ static void registerIdentityDirectExtension() throws Exception {
{
put("global.privacy", "optedin");
put("experienceCloud.org", "testOrg@AdobeOrg");
put("experienceCloud.server", "notasever");
put("experienceCloud.server", "notaserver");
}
};
MobileCore.updateConfiguration(config);
Expand All @@ -72,7 +73,7 @@ public void call(Object o) {
}
});

latch.await();
latch.await(1000, TimeUnit.MILLISECONDS);
TestHelper.waitForThreads(2000);
resetTestExpectations();
}
Expand All @@ -85,12 +86,11 @@ static void registerBothIdentityExtensions() throws Exception {
{
put("global.privacy", "optedin");
put("experienceCloud.org", "testOrg@AdobeOrg");
put("experienceCloud.server", "notasever");
put("experienceCloud.server", "notaserver");
}
};
MobileCore.updateConfiguration(config);


com.adobe.marketing.mobile.edge.identity.Identity.registerExtension();
com.adobe.marketing.mobile.Identity.registerExtension();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,22 @@ final class EventNames {
private EventNames() { }
}

final class EventDataKeys {
static final String VISITOR_ID_ECID = "mid";
final class SharedState {
static final String STATE_OWNER = "stateowner";
private EventDataKeys() { }
}

final class SharedStateKeys {
static final String IDENTITY_DIRECT = "com.adobe.module.identity";
private SharedStateKeys() { }
final class Hub {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some cleanup on these constants

static final String NAME = "com.adobe.module.eventhub";
static final String EXTENSIONS = "extensions";
private Hub() {}
}

final class IdentityDirect {
static final String NAME = "com.adobe.module.identity";
static final String ECID = "mid";
private IdentityDirect() {}
}

private SharedState() { }
}

final class Namespaces {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@


class IdentityExtension extends Extension {
private final IdentityState state = new IdentityState(new IdentityProperties());
// package private for testing
IdentityState state = new IdentityState(new IdentityProperties());

/**
* Constructor.
Expand Down Expand Up @@ -80,7 +81,6 @@ public void error(final ExtensionError extensionError) {
ListenerHubSharedState.class, listenerErrorCallback);
extensionApi.registerEventListener(IdentityConstants.EventType.GENERIC_IDENTITY,
IdentityConstants.EventSource.REQUEST_RESET, ListenerIdentityRequestReset.class, listenerErrorCallback);
state.bootUp();
}

/**
Expand Down Expand Up @@ -112,6 +112,26 @@ protected String getVersion() {
* @param event the boot {@link Event}
*/
void handleEventHubBoot(final Event event) {
SharedStateCallback callback = new SharedStateCallback() {
@Override
public Map<String, Object> getSharedState(String stateOwner, Event event) {
emdobrin marked this conversation as resolved.
Show resolved Hide resolved
if (getApi() == null) {
emdobrin marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

return getApi().getSharedEventState(stateOwner, event, new ExtensionErrorCallback<ExtensionError>() {
@Override
public void error(ExtensionError extensionError) {
MobileCore.log(LoggingMode.WARNING, LOG_TAG,
"SharedStateCallback - Unable to fetch shared state, failed with error: " + extensionError.getErrorName());
}
});
}
};

if (!state.bootupIfReady(callback)) {
return;
}

// share the initial XDMSharedState on bootUp
final Map currentIdentities = state.getIdentityProperties().toXDMData(false);
Expand Down Expand Up @@ -181,21 +201,21 @@ void handleHubSharedState(final Event event) {
return;
}

String stateOwner;
try {
final String stateOwner = (String) event.getEventData().get(IdentityConstants.EventDataKeys.STATE_OWNER);

if (!IdentityConstants.SharedStateKeys.IDENTITY_DIRECT.equals(stateOwner)) {
return;
}
stateOwner = (String) event.getEventData().get(IdentityConstants.SharedState.STATE_OWNER);
} catch (ClassCastException e) {
MobileCore.log(LoggingMode.DEBUG, LOG_TAG,
"IdentityExtension - Could not process direct Identity shared state change event, failed to parse event state owner as String: "
"IdentityExtension - Could not process shared state change event, failed to parse event state owner as String: "
+ e.getLocalizedMessage());
return;
}

if (!IdentityConstants.SharedState.IdentityDirect.NAME.equals(stateOwner)) {
return;
}

final Map<String, Object> identityState = getSharedState(IdentityConstants.SharedStateKeys.IDENTITY_DIRECT, event);
final Map<String, Object> identityState = getSharedState(IdentityConstants.SharedState.IdentityDirect.NAME, event);

if (identityState == null) {
MobileCore.log(LoggingMode.DEBUG, LOG_TAG,
Expand All @@ -204,7 +224,7 @@ void handleHubSharedState(final Event event) {
}

try {
final String legacyEcidString = (String) identityState.get(IdentityConstants.EventDataKeys.VISITOR_ID_ECID);
final String legacyEcidString = (String) identityState.get(IdentityConstants.SharedState.IdentityDirect.ECID);
final ECID legacyEcid = legacyEcidString == null ? null : new ECID(legacyEcidString);

if (state.updateLegacyExperienceCloudId(legacyEcid)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
import com.adobe.marketing.mobile.LoggingMode;
import com.adobe.marketing.mobile.MobileCore;

import java.util.HashMap;
import java.util.Map;

import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG;

/**
* Manages the business logic of this Identity extension
*/
class IdentityState {
private IdentityProperties identityProperties;
private boolean hasBooted;

/**
* Creates a new {@link IdentityState} with the given {@link IdentityProperties}
Expand All @@ -43,8 +47,13 @@ IdentityProperties getIdentityProperties() {
* Attempts to load the already persisted identities from persistence into {@link #identityProperties}
* If no ECID is loaded from persistence (ideally meaning first launch), then we attempt to read ECID for the direct Identity Extension.
* If there is no ECID loaded from the persistence of direct Identity Extension, then and new ECID is generated and persisted finishing the bootUp sequence.
* @return True if it should share state after bootup, false otherwise
*/
void bootUp() {
boolean bootupIfReady(final SharedStateCallback callback) {
if (hasBooted) {
return false;
}

// Load properties from local storage
identityProperties = IdentityStorageService.loadPropertiesFromPersistence();

Expand All @@ -56,20 +65,25 @@ void bootUp() {
if (identityProperties.getECID() == null) {
final ECID directIdentityEcid = IdentityStorageService.loadEcidFromDirectIdentityPersistence();

if (directIdentityEcid == null) {
identityProperties.setECID(new ECID());
if (directIdentityEcid != null) {
identityProperties.setECID(directIdentityEcid);
MobileCore.log(LoggingMode.DEBUG, LOG_TAG,
"IdentityState - Generating new ECID on bootup '" + identityProperties.getECID().toString() + "'");
"IdentityState - On bootup Loading ECID from direct Identity extension '" + directIdentityEcid + "'");
} else if (isIdentityDirectRegistered(callback)) {
MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "IdentityState - On bootup direct Identity extension is registered, waiting for its state change.");
return false; // If no ECID to migrate but Identity direct is registered, wait for Identity direct shared state
} else {
identityProperties.setECID(directIdentityEcid);
identityProperties.setECID(new ECID());
MobileCore.log(LoggingMode.DEBUG, LOG_TAG,
"IdentityState - On bootup Loading ECID from direct Identity extension '" + directIdentityEcid + "'");
"IdentityState - Generating new ECID on bootup '" + identityProperties.getECID().toString() + "'");
}

IdentityStorageService.savePropertiesToPersistence(identityProperties);
}

hasBooted = true;
MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "IdentityState - Edge Identity has successfully booted up");
return true;
}

/**
Expand Down Expand Up @@ -117,6 +131,11 @@ boolean updateLegacyExperienceCloudId(final ECID legacyEcid) {
final ECID ecid = identityProperties.getECID();
final ECID ecidSecondary = identityProperties.getECIDSecondary();

if (ecid == null) {
handleInstallWithIdentityDirectECID(legacyEcid);
return true;
}

if ((legacyEcid != null) && (legacyEcid.equals(ecid) || legacyEcid.equals(ecidSecondary))) {
return false;
}
Expand All @@ -133,4 +152,51 @@ boolean updateLegacyExperienceCloudId(final ECID legacyEcid) {
return true;
}

/**
* This method is called when the primary Edge ECID is null and the Identity Direct shared state has been updated (install scenario when Identity Direct is registered).
* Sets the {@code legacyEcid} as primary ECID when not null, otherwise generates a new ECID, then updates the persistence.
*
* @param legacyEcid the current ECID from the direct Identity extension
*/
private void handleInstallWithIdentityDirectECID(final ECID legacyEcid) {
if (legacyEcid != null) {
identityProperties.setECID(legacyEcid); // set legacy ECID as main ECID
MobileCore.log(LoggingMode.DEBUG, LOG_TAG,
"IdentityState - Identity direct ECID '" + legacyEcid + "' was migrated to Edge Identity, updating the IdentityMap");

} else { // opt-out scenario or an unexpected state for Identity direct, generate new ECID
identityProperties.setECID(new ECID());
MobileCore.log(LoggingMode.DEBUG, LOG_TAG,
"IdentityState - Identity direct ECID is null, generating new ECID '" + identityProperties.getECID() + "', updating the IdentityMap");
}

IdentityStorageService.savePropertiesToPersistence(identityProperties);
}

/**
* Check if the Identity direct extension is registered by checking the EventHub's shared state list of registered extensions.
* @param callback the {@link SharedStateCallback} to be used for fetching the EventHub Shared state
* @return true if the Identity direct extension is registered with the EventHub
*/
private boolean isIdentityDirectRegistered(final SharedStateCallback callback) {
Map<String, Object> registeredExtensionsWithHub = callback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null);
Map<String, Object> identityDirectInfo = null;

if (registeredExtensionsWithHub != null) {
try {
final Map<String, Object> extensions = (HashMap<String, Object>) registeredExtensionsWithHub.get(
IdentityConstants.SharedState.Hub.EXTENSIONS);

if (extensions != null) {
identityDirectInfo = (HashMap<String, Object>) extensions.get(IdentityConstants.SharedState.IdentityDirect.NAME);
}
} catch (ClassCastException e) {
MobileCore.log(LoggingMode.DEBUG, LOG_TAG,
"IdentityState - Unable to fetch com.adobe.module.identity info from Hub State due to invalid format, expected Map");
}
}

return !Utils.isNullOrEmpty(identityDirectInfo);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright 2021 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.Event;

import java.util.Map;

/**
* Callback for fetching Shared States from the outside of the extension class.
*/
interface SharedStateCallback {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this way of exposing the sharedState API from extension class.

How about renaming this to SharedStateAPI ?
If so the usage of this in IdentityState Class would look more intuitive

	boolean bootupIfReady(final SharedStateAPI api) {

	       final Map<String, Object> identityDirectSharedState = api.getSharedState(
						IdentityConstants.SharedState.IdentityDirect.NAME, null);
                  ....
          }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice suggestion, code looks nice, but I feel that this way you loose the context that it is a callback resolved in the parent extension and it can be easily confused with ExtensionsApi getApi.


/**
* Fetches the Shared State for the provided {@code event} from the specified {@code stateOwner}.
*
* @param stateOwner Shared state owner name
* @param event current event for which to fetch the shared state; if null is passed, the latest shared state will be returned
* @return current shared state if found, null if shared state is pending or an error occurred
*/
Map<String, Object> getSharedState(final String stateOwner, final Event event);
}
Loading