From 10c43eac7280893ef1d6502e3ea17d87612408b8 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat <96199823+prudrabhat@users.noreply.github.com> Date: Wed, 16 Nov 2022 08:44:02 -0800 Subject: [PATCH 01/50] Upgrade AGP, Gradle, language dependencies (#75) * Upgrade AGP, Gradle, language dependencies - Upgrades - Gradle distribution 7.5 - Gradle 7.3.1 - Java 1.8 - Kotlin 1.6.2 - jacoco 0.8.8 - Expand .gitignore - Restructure plugin declarations - Fix functional test configuration (dex, androidx.test:core)) * Fix javadoc task * Fix app namespace warning * Address review comments - remove *Version suffix for sdk options in gradle - switch to gradle 7.3.0 - Use `required` instead of `enabled` for xml, csv, html reports - Remove aar maven upload script * Fix namespace warning and rename variable * Enable multidex only for debug builds Functional tests will fail without this flag as this module exceeds the DEX limit. It is sufficient to enable this for debug as it is the type used for functional tests. Consuming apps are responsible for making changes to their gradle to overcome the DEX limit as necessary. * Remove unneeded dependencies and variables --- .gitignore | 19 +++- code/app/build.gradle | 27 ++--- code/app/src/main/AndroidManifest.xml | 3 +- code/build.gradle | 65 +++++------- code/edgeidentity/build.gradle | 98 +++++++------------ .../edgeidentity/src/main/AndroidManifest.xml | 4 +- code/gradle.properties | 1 + code/gradle/wrapper/gradle-wrapper.properties | 4 +- code/settings.gradle | 17 ++++ 9 files changed, 115 insertions(+), 123 deletions(-) diff --git a/.gitignore b/.gitignore index f86dd9f4..74484c94 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,21 @@ ci/ **/.idea/ # Secrets -**/*/values/secrets.xml \ No newline at end of file +**/*/values/secrets.xml + +# Local configuration file (sdk path, etc) +local.properties + +# Android Studio Navigation editor temp files +.navigation/ + +# Coverage tools +jacoco.exec + +# Application files +*.apk +*.aar +*.aab + +# Dex files for ART/Dalvik VM +*.dex \ No newline at end of file diff --git a/code/app/build.gradle b/code/app/build.gradle index c3abf6ab..47354225 100644 --- a/code/app/build.gradle +++ b/code/app/build.gradle @@ -1,7 +1,8 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -apply plugin: "com.diffplug.spotless" +plugins { + id 'com.android.application' + id 'com.diffplug.spotless' + id 'org.jetbrains.kotlin.android' +} spotless { kotlin { @@ -12,14 +13,16 @@ spotless { } android { - compileSdkVersion 31 + namespace 'com.adobe.marketing.edge.identity.app' + + compileSdk rootProject.ext.compileSdkVersion defaultConfig { applicationId "com.adobe.marketing.edge.identity.app" - minSdkVersion 19 - targetSdkVersion 31 - versionCode 1 - versionName "1.0" + minSdk rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdkVersion + versionCode rootProject.ext.versionCode + versionName project.property('moduleVersion') as String ?: "1.0.0 (Default)" multiDexEnabled = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -35,14 +38,14 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { - jvmTarget = '1.8' + jvmTarget = rootProject.ext.kotlinJvmTarget } } dependencies { - implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:${rootProject.ext.kotlinVersion}" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' diff --git a/code/app/src/main/AndroidManifest.xml b/code/app/src/main/AndroidManifest.xml index 18386222..a11edc9a 100644 --- a/code/app/src/main/AndroidManifest.xml +++ b/code/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - + +afterEvaluate { tasks.withType(Javadoc) { - source = [android.sourceSets.main.java.sourceFiles, android.sourceSets.phone.java.sourceFiles] - ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + classpath += files(android.libraryVariants.collect { variant -> + variant.javaCompileProvider.get().classpath.files + }) - doFirst{classpath = files(variant.javaCompile.classpath.files) + files(ext.androidJar)} - - exclude "**/BuildConfig.java" - exclude "**/R.java" options { + source = "8" links "https://developer.android.com/reference" } } @@ -113,39 +118,6 @@ publish.dependsOn('assemblePhone') publishing { publications { - aar(MavenPublication) { - groupId = 'com.adobe.marketing.mobile' - artifactId = rootProject.moduleName - version = rootProject.moduleVersion - artifact("$buildDir/outputs/aar/${rootProject.moduleAARName}") - artifact javadocPublish - pom.withXml { - asNode().appendNode('name', mavenRepoName) - asNode().appendNode('description', mavenRepoDescription) - asNode().appendNode('url', 'https://aep-sdks.gitbook.io/docs/') - def scmNode = asNode().appendNode('scm') - scmNode.appendNode('url', 'https://github.com/Adobe-Marketing-Cloud/acp-sdks/') - - - def developersNode = asNode().appendNode('developers') - def developerNode = developersNode.appendNode('developer') - developerNode.appendNode('id', 'adobe') - developerNode.appendNode('name', 'adobe') - - def licensesNode = asNode().appendNode('licenses') - def licenseNode = licensesNode.appendNode('license') - licenseNode.appendNode('name', 'Adobe Proprietary') - - def dependenciesNode = asNode().appendNode('dependencies') - - //Iterate over the compile dependencies (we don't want the test ones), adding a node for each - - def coreDependencyNode = dependenciesNode.appendNode('dependency') - coreDependencyNode.appendNode('groupId', 'com.adobe.marketing.mobile') - coreDependencyNode.appendNode('artifactId', 'core') - coreDependencyNode.appendNode('version', mavenCoreVersion) - } - } release(MavenPublication) { groupId = 'com.adobe.marketing.mobile' artifactId = rootProject.moduleName @@ -156,7 +128,7 @@ publishing { pom { name = mavenRepoName description = mavenRepoDescription - url = 'https://aep-sdks.gitbook.io' + url = 'https://developer.adobe.com/client-sdks' licenses { license { name = 'Adobe Proprietary' @@ -170,9 +142,9 @@ publishing { } } scm { - connection = 'scm:git:github.com//Adobe-Marketing-Cloud/acp-sdks.git' - developerConnection = 'scm:git:ssh://github.com//Adobe-Marketing-Cloud/acp-sdks.git' - url = 'https://github.com/Adobe-Marketing-Cloud/acp-sdks' + connection = 'scm:git:github.com//adobe/aepsdk-edgeidentity-android.git' + developerConnection = 'scm:git:ssh://github.com//adobe/aepsdk-edgeidentity-android.git' + url = 'https://github.com/adobe/aepsdk-edgeidentity-android' } withXml { def dependenciesNode = asNode().appendNode('dependencies') @@ -212,23 +184,23 @@ signing { } task platformUnitTestJacocoReport(type: JacocoReport, dependsOn: "testPhoneDebugUnitTest") { - def excludeRegex = ['**/ADB*.class', '**/BuildConfig.class'] + def excludeRegex = ['**/BuildConfig.class'] def debugTree = fileTree(dir: "${project.buildDir}/intermediates/javac/phoneDebug/classes/com/adobe/marketing/mobile", excludes: excludeRegex) additionalClassDirs.setFrom files([debugTree]) additionalSourceDirs.setFrom files(android.sourceSets.main.java.sourceFiles) sourceDirectories.setFrom files(android.sourceSets.phone.java.sourceFiles) - executionData "$buildDir/jacoco/testPhoneDebugUnitTest.exec" + executionData "$buildDir/outputs/unit_test_code_coverage/phoneDebugUnitTest/testPhoneDebugUnitTest.exec" reports { - xml.enabled true - csv.enabled false - html.enabled true + xml.required = true + csv.required = false + html.required = true } } task platformFunctionalTestJacocoReport(type: JacocoReport, dependsOn: "createPhoneDebugCoverageReport") { - def excludeRegex = ['**/ADB*.class', '**/BuildConfig.class'] + def excludeRegex = ['**/BuildConfig.class'] def debugTree = fileTree(dir: "${project.buildDir}/intermediates/javac/phoneDebug/classes/com/adobe/marketing/mobile", excludes: excludeRegex) additionalClassDirs.setFrom files([debugTree]) @@ -239,15 +211,13 @@ task platformFunctionalTestJacocoReport(type: JacocoReport, dependsOn: "createPh ]) reports { - xml.enabled true - csv.enabled false - html.enabled true + xml.required = true + csv.required = false + html.required = true } } - dependencies { - implementation 'androidx.appcompat:appcompat:1.2.0' // Adobe Mobile SDK Core implementation "com.adobe.marketing.mobile:core:${rootProject.mavenCoreVersion}" diff --git a/code/edgeidentity/src/main/AndroidManifest.xml b/code/edgeidentity/src/main/AndroidManifest.xml index 653c8ae3..61363294 100644 --- a/code/edgeidentity/src/main/AndroidManifest.xml +++ b/code/edgeidentity/src/main/AndroidManifest.xml @@ -1,3 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/code/gradle.properties b/code/gradle.properties index 746b503c..8f211c79 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -4,6 +4,7 @@ org.gradle.jvmargs=-Xmx2048m android.useAndroidX=true android.enableJetifier=true +android.disableAutomaticComponentCreation=true moduleProjectName=edgeidentity moduleName=edgeidentity diff --git a/code/gradle/wrapper/gradle-wrapper.properties b/code/gradle/wrapper/gradle-wrapper.properties index df944bc5..5e4cdd73 100644 --- a/code/gradle/wrapper/gradle-wrapper.properties +++ b/code/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Mar 18 11:50:52 PDT 2022 +#Mon Nov 07 08:48:57 PST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/code/settings.gradle b/code/settings.gradle index fa1b7708..bb5f0304 100644 --- a/code/settings.gradle +++ b/code/settings.gradle @@ -1,3 +1,20 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } + } +} + include ':app' include ':edgeidentity' rootProject.name = "edgeidentity-sdk" \ No newline at end of file From 427901459afd61052aa0c9109f0c32b8377e803d Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat <96199823+prudrabhat@users.noreply.github.com> Date: Tue, 29 Nov 2022 10:27:44 -0800 Subject: [PATCH 02/50] Adopt changes in Core 2.0 (#76) * Core 2.0 adoption - .gradle changes - Add jitpack to repositories - Consume Core 2.0 via com.github.adobe.aepsdk-core-android:core:v2.0.0-SNAPSHOT from jitpack - Prevent transitive dependencies from other extensions - Change moduleVersion, mavenCoreVersion to 2.0.0. - Change IdentityConstants.EXTENSION_VERSION to 2.0.0 - Expose IdentityExtension.class via Idenity.EXTENSION - Switch to MobileCore.dispatchEventWithResponseCallback(event, timeout, callbackWithError) and ExtensionApi.dispatch(event) - Switch to using new shared state api - Remove multiple instance creation of SharedStateCallback. - Streamline shared state calls via SharedStateCallback within and outside the Identity extension - Implement IdentityExtension.onRegistered() - Change event listener registration to be done in onRegistered() instead of the IdentityExtension's constructor - Remove Listener*.java classes in favor of inline methods - Remove the inhouse executor service and mutex as the events are now incident via a single thread maintained by the extension container housing this extension. - Implement IdentityExtension.readyForEvent() - Add logic to wait for EventHub state and IdentityDirect registration check if ECID from persisted properties is unavailable. - Remove the logic to cache events internally. Take advantage of the Hub shared state fetching to boot up and disambiguate between Identity direct and EdgeIdentity. - Switch to use public utility classes and methods from Core where applicable - Use StringUtils.isEmpty() - Use JSONUtil.toMap() and JSONUtil.toList() - Use DataReader.opt*() to read and parse event data. Remove class cast exception guards and resolve unchecked cast warnings where possible. - General changes done on code path toched - private and finals where applicable - Use cascading constructors to eliminate constructor divergence risk and ease test injection Out of scope for this PR - Switching to Log from MobileCore.Log - Switching IdentityStorageService to NamedCollection - Test changes - PowerMock elimination * Core 2.0 adoption - .gradle changes - Add jitpack to repositories - Consume Core 2.0 via com.github.adobe.aepsdk-core-android:core:v2.0.0-SNAPSHOT from jitpack - Prevent transitive dependencies from other extensions - Change moduleVersion, mavenCoreVersion to 2.0.0. - Change IdentityConstants.EXTENSION_VERSION to 2.0.0 - Expose IdentityExtension.class via Idenity.EXTENSION - Switch to MobileCore.dispatchEventWithResponseCallback(event, timeout, callbackWithError) and ExtensionApi.dispatch(event) - Switch to using new shared state api - Remove multiple instance creation of SharedStateCallback. - Streamline shared state calls via SharedStateCallback within and outside the Identity extension - Implement IdentityExtension.onRegistered() - Change event listener registration to be done in onRegistered() instead of the IdentityExtension's constructor - Remove Listener*.java classes in favor of inline methods - Remove the inhouse executor service and mutex as the events are now incident via a single thread maintained by the extension container housing this extension. - Implement IdentityExtension.readyForEvent() - Add logic to wait for EventHub state and IdentityDirect registration check if ECID from persisted properties is unavailable. - Remove the logic to cache events internally. Take advantage of the Hub shared state fetching to boot up and disambiguate between Identity direct and EdgeIdentity. - Switch to use public utility classes and methods from Core where applicable - Use StringUtils.isEmpty() - Use JSONUtil.toMap() and JSONUtil.toList() - Use DataReader.opt*() to read and parse event data. Remove class cast exception guards and resolve unchecked cast warnings where possible. - General changes done on code path toched - private and finals where applicable - Use cascading constructors to eliminate constructor divergence risk and ease test injection Out of scope for this PR - Switching to Log from MobileCore.Log - Switching IdentityStorageService to NamedCollection - Test changes - PowerMock elimination * Address review comments - Switch to using handlers for each event listener - Use TimeUtils where applicable - Eliminate two calls to get event hub shared state - Add comments and TODOs for verification and logging * Switch to SNAPSHOT core artifact * Change callback timeout to 500ms to match API contract --- code/app/build.gradle | 22 +- code/edgeidentity/build.gradle | 6 +- .../edge/identity/AuthenticatedState.java | 4 +- .../marketing/mobile/edge/identity/ECID.java | 3 +- .../mobile/edge/identity/EventUtils.java | 174 +----- .../mobile/edge/identity/Identity.java | 234 ++++---- .../edge/identity/IdentityConstants.java | 30 +- .../edge/identity/IdentityExtension.java | 540 ++++++------------ .../mobile/edge/identity/IdentityItem.java | 25 +- .../mobile/edge/identity/IdentityMap.java | 60 +- .../edge/identity/IdentityProperties.java | 7 +- .../mobile/edge/identity/IdentityState.java | 115 ++-- .../edge/identity/IdentityStorageService.java | 3 +- .../ListenerEdgeIdentityRemoveIdentity.java | 81 --- .../ListenerEdgeIdentityRequestIdentity.java | 81 --- .../ListenerEdgeIdentityUpdateIdentity.java | 81 --- .../edge/identity/ListenerEventHubBoot.java | 72 --- .../edge/identity/ListenerHubSharedState.java | 81 --- .../ListenerIdentityRequestContent.java | 81 --- .../ListenerIdentityRequestReset.java | 81 --- .../edge/identity/SharedStateCallback.java | 9 +- .../mobile/edge/identity/URLUtils.java | 7 +- .../marketing/mobile/edge/identity/Utils.java | 128 +---- code/gradle.properties | 4 +- code/settings.gradle | 1 + 25 files changed, 440 insertions(+), 1490 deletions(-) delete mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentity.java delete mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentity.java delete mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentity.java delete mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBoot.java delete mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedState.java delete mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContent.java delete mode 100644 code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestReset.java diff --git a/code/app/build.gradle b/code/app/build.gradle index 47354225..6fe46a51 100644 --- a/code/app/build.gradle +++ b/code/app/build.gradle @@ -58,13 +58,23 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.3.3' implementation project(':edgeidentity') - implementation 'com.adobe.marketing.mobile:core:1+' - implementation 'com.adobe.marketing.mobile:identity:1+' - implementation 'com.adobe.marketing.mobile:edgeconsent:1.+' - implementation('com.adobe.marketing.mobile:edge:1.3.2-SNAPSHOT') { - exclude group: 'com.adobe.marketing.mobile', module:'edgeidentity' + + // TODO: Uncomment next line when core 2.0 artifacts are published to maven + // implementation "com.adobe.marketing.mobile:core:${rootProject.mavenCoreVersion}" + implementation 'com.github.adobe.aepsdk-core-android:core:dev-v2.0.0-SNAPSHOT' + + implementation ('com.adobe.marketing.mobile:identity:1+') { + transitive = false + } + implementation('com.adobe.marketing.mobile:edgeconsent:1.+') { + transitive = false + } + implementation('com.adobe.marketing.mobile:edge:1.+') { + transitive = false + } + implementation ('com.adobe.marketing.mobile:assurance:1+') { + transitive = false } - implementation 'com.adobe.marketing.mobile:assurance:1+' /* Ad ID implementation (pt. 1/5) implementation("com.google.android.gms:play-services-ads-lite:20.6.0") diff --git a/code/edgeidentity/build.gradle b/code/edgeidentity/build.gradle index 0c7805e7..aef0c300 100644 --- a/code/edgeidentity/build.gradle +++ b/code/edgeidentity/build.gradle @@ -220,7 +220,11 @@ task platformFunctionalTestJacocoReport(type: JacocoReport, dependsOn: "createPh dependencies { // Adobe Mobile SDK Core - implementation "com.adobe.marketing.mobile:core:${rootProject.mavenCoreVersion}" + + // TODO: Uncomment next line when core 2.0 artifacts are published to maven + // implementation "com.adobe.marketing.mobile:core:${rootProject.mavenCoreVersion}" + implementation 'com.github.adobe.aepsdk-core-android:core:dev-v2.0.0-SNAPSHOT' + implementation 'androidx.annotation:annotation:1.3.0' testImplementation "androidx.test.ext:junit:${rootProject.ext.junitVersion}" testImplementation "org.mockito:mockito-core:${rootProject.ext.mockitoCoreVersion}" diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/AuthenticatedState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/AuthenticatedState.java index cedfe712..47dc29fb 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/AuthenticatedState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/AuthenticatedState.java @@ -30,9 +30,9 @@ public enum AuthenticatedState { */ LOGGED_OUT("loggedOut"); - private String name; + private final String name; - private AuthenticatedState(final String name) { + AuthenticatedState(final String name) { this.name = name; } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java index 3bdf5c74..401a5376 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java @@ -13,6 +13,7 @@ import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.StringUtils; import java.util.Locale; import java.util.Objects; import java.util.UUID; @@ -41,7 +42,7 @@ final class ECID { * @param ecidString a valid (38-digit UUID) ECID string representation, if null or empty a new ECID will be generated */ ECID(final String ecidString) { - if (Utils.isNullOrEmpty(ecidString)) { + if (StringUtils.isNullOrEmpty(ecidString)) { MobileCore.log( LoggingMode.DEBUG, IdentityConstants.LOG_TAG, 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 44dd1e9c..bc756f64 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 @@ -11,11 +11,9 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; - import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.StringUtils; import java.util.Map; /** @@ -34,62 +32,6 @@ static boolean isAdIdEvent(final Event event) { return data.containsKey(IdentityConstants.EventDataKeys.ADVERTISING_IDENTIFIER); } - /** - * Checks if the provided {@code event} is of type {@link IdentityConstants.EventType#EDGE_IDENTITY} and source {@link IdentityConstants.EventSource#REMOVE_IDENTITY} - * - * @param event the event to verify - * @return true if both type and source match - */ - static boolean isRemoveIdentityEvent(final Event event) { - return ( - event != null && - IdentityConstants.EventType.EDGE_IDENTITY.equalsIgnoreCase(event.getType()) && - IdentityConstants.EventSource.REMOVE_IDENTITY.equalsIgnoreCase(event.getSource()) - ); - } - - /** - * Checks if the provided {@code event} is of type {@link IdentityConstants.EventType#EDGE_IDENTITY} and source {@link IdentityConstants.EventSource#UPDATE_IDENTITY} - * - * @param event the event to verify - * @return true if both type and source match - */ - static boolean isUpdateIdentityEvent(final Event event) { - return ( - event != null && - IdentityConstants.EventType.EDGE_IDENTITY.equalsIgnoreCase(event.getType()) && - IdentityConstants.EventSource.UPDATE_IDENTITY.equalsIgnoreCase(event.getSource()) - ); - } - - /** - * Checks if the provided {@code event} is of type {@link IdentityConstants.EventType#EDGE_IDENTITY} and source {@link IdentityConstants.EventSource#REQUEST_IDENTITY} - * - * @param event the event to verify - * @return true if both type and source match - */ - static boolean isRequestIdentityEvent(final Event event) { - return ( - event != null && - IdentityConstants.EventType.EDGE_IDENTITY.equalsIgnoreCase(event.getType()) && - IdentityConstants.EventSource.REQUEST_IDENTITY.equalsIgnoreCase(event.getSource()) - ); - } - - /** - * Checks if the provided {@code event} is of type {@link IdentityConstants.EventType#GENERIC_IDENTITY} and source {@link IdentityConstants.EventSource#REQUEST_CONTENT} - * - * @param event the event to verify - * @return true if both type and source match - */ - static boolean isRequestContentEvent(final Event event) { - return ( - event != null && - IdentityConstants.EventType.GENERIC_IDENTITY.equalsIgnoreCase(event.getType()) && - IdentityConstants.EventSource.REQUEST_CONTENT.equalsIgnoreCase(event.getSource()) - ); - } - /** * Reads the url variables flag from the event data, returns false if not present * Note: This API needs to be used with isRequestIdentityEvent API to determine the correct event type and event source @@ -97,37 +39,9 @@ static boolean isRequestContentEvent(final Event event) { * @return true if urlVariables key is present in the event data and has a value of true */ static boolean isGetUrlVariablesRequestEvent(final Event event) { - if (event == null || event.getEventData() == null) { - return false; - } - boolean getUrlVariablesFlag = false; - - try { - Object urlVariablesFlagObject = event.getEventData().get(IdentityConstants.EventDataKeys.URL_VARIABLES); - getUrlVariablesFlag = urlVariablesFlagObject != null && (boolean) urlVariablesFlagObject; - } catch (ClassCastException e) { - MobileCore.log( - LoggingMode.WARNING, - LOG_TAG, - "EventUtils - Failed to read urlvariables value, expected boolean: " + e.getLocalizedMessage() - ); - return false; - } - - return getUrlVariablesFlag; - } - - /** - * Checks if the provided {@code event} is of type {@link IdentityConstants.EventType#GENERIC_IDENTITY} and source {@link IdentityConstants.EventSource#REQUEST_RESET} - * - * @param event the event to verify - * @return true if both type and source match - */ - static boolean isRequestResetEvent(final Event event) { return ( event != null && - IdentityConstants.EventType.GENERIC_IDENTITY.equalsIgnoreCase(event.getType()) && - IdentityConstants.EventSource.REQUEST_RESET.equalsIgnoreCase(event.getSource()) + DataReader.optBoolean(event.getEventData(), IdentityConstants.EventDataKeys.URL_VARIABLES, false) ); } @@ -139,18 +53,15 @@ static boolean isRequestResetEvent(final Event event) { * @return {@code boolean} indicating if it is the shared state update for the provided {@code stateOwnerName} */ static boolean isSharedStateUpdateFor(final String stateOwnerName, final Event event) { - if (Utils.isNullOrEmpty(stateOwnerName) || event == null) { - return false; - } - - String stateOwner; - - try { - stateOwner = (String) event.getEventData().get(IdentityConstants.EventDataKeys.STATE_OWNER); - } catch (ClassCastException e) { + if (StringUtils.isNullOrEmpty(stateOwnerName) || event == null) { return false; } + final String stateOwner = DataReader.optString( + event.getEventData(), + IdentityConstants.EventDataKeys.STATE_OWNER, + "" + ); return stateOwnerName.equals(stateOwner); } @@ -169,23 +80,9 @@ static boolean isSharedStateUpdateFor(final String stateOwnerName, final Event e */ static String getAdId(final Event event) { final Map data = event.getEventData(); - String adID; - - try { - adID = (String) data.get(IdentityConstants.EventDataKeys.ADVERTISING_IDENTIFIER); - } catch (ClassCastException e) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "EventUtils - Failed to extract ad ID from event, expected String: " + e.getLocalizedMessage() - ); - return ""; - } + final String adID = DataReader.optString(data, IdentityConstants.EventDataKeys.ADVERTISING_IDENTIFIER, null); - if (adID == null || IdentityConstants.Default.ZERO_ADVERTISING_ID.equals(adID)) { - return ""; - } - return adID; + return (adID == null || IdentityConstants.Default.ZERO_ADVERTISING_ID.equals(adID) ? "" : adID); } /** @@ -195,23 +92,12 @@ static String getAdId(final Event event) { * @return the ECID or null if not found or unable to parse the payload */ static ECID getECID(final Map identityDirectSharedState) { - ECID legacyEcid = null; - - try { - final String legacyEcidString = (String) identityDirectSharedState.get( - IdentityConstants.SharedState.IdentityDirect.ECID - ); - legacyEcid = legacyEcidString == null ? null : new ECID(legacyEcidString); - } catch (ClassCastException e) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "EventUtils - Failed to extract ECID from Identity direct shared state, expected String: " + - e.getLocalizedMessage() - ); - } - - return legacyEcid; + final String legacyEcidString = DataReader.optString( + identityDirectSharedState, + IdentityConstants.SharedState.IdentityDirect.ECID, + null + ); + return (legacyEcidString == null ? null : new ECID(legacyEcidString)); } /** @@ -221,26 +107,10 @@ static ECID getECID(final Map identityDirectSharedState) { * @return the Experience Cloud Org Id or null if not found or unable to parse the payload */ 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; + return DataReader.optString( + configurationSharedState, + IdentityConstants.SharedState.Configuration.EXPERIENCE_CLOUD_ORGID, + null + ); } } 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 5c22bbdb..63967f02 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 @@ -17,10 +17,13 @@ import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionError; import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.StringUtils; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,6 +33,9 @@ */ public class Identity { + public static final Class EXTENSION = IdentityExtension.class; + private static final long CALLBACK_TIMEOUT_MILLIS = 500L; + private Identity() {} /** @@ -43,7 +49,10 @@ public static String extensionVersion() { /** * Registers the extension with the Mobile SDK. This method should be called only once in your application class. + * + * @deprecated Use {@link MobileCore#registerExtensions(List, AdobeCallback)} with {@link Identity#EXTENSION} instead. */ + @Deprecated public static void registerExtension() { MobileCore.registerExtension( IdentityExtension.class, @@ -85,57 +94,53 @@ public static void getExperienceCloudId(final AdobeCallback callback) { ) .build(); - final ExtensionErrorCallback errorCallback = new ExtensionErrorCallback() { + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + @Override + public void call(Event responseEvent) { + if (responseEvent == null || responseEvent.getEventData() == null) { + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + + final IdentityMap identityMap = IdentityMap.fromXDMMap(responseEvent.getEventData()); + + if (identityMap == null) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + "Identity - Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" + ); + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + + final List ecidItems = identityMap.getIdentityItemsForNamespace( + IdentityConstants.Namespaces.ECID + ); + + if (ecidItems == null || ecidItems.isEmpty() || ecidItems.get(0).getId() == null) { + callback.call(""); + } else { + callback.call(ecidItems.get(0).getId()); + } + } + @Override - public void error(final ExtensionError extensionError) { - returnError(callback, extensionError); + public void fail(AdobeError adobeError) { + returnError(callback, adobeError); MobileCore.log( LoggingMode.DEBUG, LOG_TAG, String.format( "Identity - Failed to dispatch %s event: Error : %s.", IdentityConstants.EventNames.IDENTITY_REQUEST_IDENTITY_ECID, - extensionError.getErrorName() + adobeError.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 IdentityMap identityMap = IdentityMap.fromXDMMap(responseEvent.getEventData()); - - if (identityMap == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Identity - Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" - ); - returnError(callback, AdobeError.UNEXPECTED_ERROR); - return; - } - - final List ecidItems = identityMap.getIdentityItemsForNamespace( - IdentityConstants.Namespaces.ECID - ); - - if (ecidItems == null || ecidItems.isEmpty() || ecidItems.get(0).getId() == null) { - callback.call(""); - } else { - callback.call(ecidItems.get(0).getId()); - } - } - }, - errorCallback - ); + MobileCore.dispatchEventWithResponseCallback(event, CALLBACK_TIMEOUT_MILLIS, callbackWithError); } /** @@ -179,48 +184,44 @@ public static void getUrlVariables(final AdobeCallback callback) { ) .build(); - final ExtensionErrorCallback errorCallback = new ExtensionErrorCallback() { + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override - public void error(final ExtensionError extensionError) { - returnError(callback, extensionError); + public void call(final Event responseEvent) { + if (responseEvent == null || responseEvent.getEventData() == null) { + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + + final Map data = responseEvent.getEventData(); + final String urlVariableString = DataReader.optString( + data, + IdentityConstants.EventDataKeys.URL_VARIABLES, + null + ); + + if (urlVariableString == null) { + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + callback.call(urlVariableString); + } + + @Override + public void fail(final AdobeError adobeError) { + returnError(callback, adobeError); MobileCore.log( LoggingMode.DEBUG, LOG_TAG, String.format( "Identity - Failed to dispatch %s event: Error : %s.", IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - extensionError.getErrorName() + adobeError.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 - ); + MobileCore.dispatchEventWithResponseCallback(event, CALLBACK_TIMEOUT_MILLIS, callbackWithError); } /** @@ -241,21 +242,6 @@ public static void updateIdentities(final IdentityMap identityMap) { return; } - final ExtensionErrorCallback errorCallback = new ExtensionErrorCallback() { - @Override - public void error(final ExtensionError extensionError) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - String.format( - "Identity - Update Identities API. Failed to dispatch %s event: Error : %s.", - IdentityConstants.EventNames.UPDATE_IDENTITIES, - extensionError.getErrorName() - ) - ); - } - }; - final Event updateIdentitiesEvent = new Event.Builder( IdentityConstants.EventNames.UPDATE_IDENTITIES, IdentityConstants.EventType.EDGE_IDENTITY, @@ -263,7 +249,8 @@ public void error(final ExtensionError extensionError) { ) .setEventData(identityMap.asXDMMap(false)) .build(); - MobileCore.dispatchEvent(updateIdentitiesEvent, errorCallback); + + MobileCore.dispatchEvent(updateIdentitiesEvent); } /** @@ -274,7 +261,7 @@ public void error(final ExtensionError extensionError) { * @param namespace The namespace of the identity to remove. */ public static void removeIdentity(final IdentityItem item, final String namespace) { - if (Utils.isNullOrEmpty(namespace)) { + if (StringUtils.isNullOrEmpty(namespace)) { MobileCore.log( LoggingMode.DEBUG, LOG_TAG, @@ -291,21 +278,6 @@ public static void removeIdentity(final IdentityItem item, final String namespac IdentityMap identityMap = new IdentityMap(); identityMap.addItem(item, namespace); - final ExtensionErrorCallback errorCallback = new ExtensionErrorCallback() { - @Override - public void error(final ExtensionError extensionError) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - String.format( - "Identity - removeIdentity API. Failed to dispatch %s event: Error : %s.", - IdentityConstants.EventNames.REMOVE_IDENTITIES, - extensionError.getErrorName() - ) - ); - } - }; - final Event removeIdentitiesEvent = new Event.Builder( IdentityConstants.EventNames.REMOVE_IDENTITIES, IdentityConstants.EventType.EDGE_IDENTITY, @@ -313,7 +285,7 @@ public void error(final ExtensionError extensionError) { ) .setEventData(identityMap.asXDMMap(false)) .build(); - MobileCore.dispatchEvent(removeIdentitiesEvent, errorCallback); + MobileCore.dispatchEvent(removeIdentitiesEvent); } /** @@ -340,49 +312,45 @@ public static void getIdentities(final AdobeCallback callback) { ) .build(); - final ExtensionErrorCallback errorCallback = new ExtensionErrorCallback() { + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override - public void error(final ExtensionError extensionError) { - returnError(callback, extensionError); + public void call(final Event responseEvent) { + if (responseEvent == null || responseEvent.getEventData() == null) { + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + + final IdentityMap identityMap = IdentityMap.fromXDMMap(responseEvent.getEventData()); + + if (identityMap == null) { + MobileCore.log( + LoggingMode.DEBUG, + LOG_TAG, + "Identity - Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" + ); + returnError(callback, AdobeError.UNEXPECTED_ERROR); + return; + } + + callback.call(identityMap); + } + + @Override + public void fail(final AdobeError adobeError) { + returnError(callback, adobeError); MobileCore.log( LoggingMode.DEBUG, LOG_TAG, String.format( "Identity - Failed to dispatch %s event: Error : %s.", IdentityConstants.EventNames.REQUEST_IDENTITIES, - extensionError.getErrorName() + adobeError.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 IdentityMap identityMap = IdentityMap.fromXDMMap(responseEvent.getEventData()); - - if (identityMap == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Identity - Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" - ); - returnError(callback, AdobeError.UNEXPECTED_ERROR); - return; - } - - callback.call(identityMap); - } - }, - errorCallback - ); + MobileCore.dispatchEventWithResponseCallback(event, CALLBACK_TIMEOUT_MILLIS, callbackWithError); } /** 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 fbfcf14d..bb1edfb1 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 @@ -15,16 +15,16 @@ final class IdentityConstants { static final String LOG_TAG = "EdgeIdentity"; static final String EXTENSION_NAME = "com.adobe.edge.identity"; - static final String EXTENSION_VERSION = "1.1.0"; + static final String EXTENSION_VERSION = "2.0.0"; - final class Default { + static final class Default { static final String ZERO_ADVERTISING_ID = "00000000-0000-0000-0000-000000000000"; private Default() {} } - final class EventSource { + static final class EventSource { static final String BOOTED = "com.adobe.eventSource.booted"; static final String REMOVE_IDENTITY = "com.adobe.eventSource.removeIdentity"; @@ -40,7 +40,7 @@ final class EventSource { private EventSource() {} } - final class EventType { + static final class EventType { static final String EDGE_CONSENT = "com.adobe.eventType.edgeConsent"; static final String EDGE_IDENTITY = "com.adobe.eventType.edgeIdentity"; @@ -51,7 +51,7 @@ final class EventType { private EventType() {} } - final class EventNames { + static final class EventNames { static final String CONSENT_UPDATE_REQUEST_AD_ID = "Consent Update Request for Ad ID"; static final String IDENTITY_REQUEST_IDENTITY_ECID = "Edge Identity Request ECID"; @@ -66,7 +66,7 @@ final class EventNames { private EventNames() {} } - final class EventDataKeys { + static final class EventDataKeys { static final String ADVERTISING_IDENTIFIER = "advertisingidentifier"; static final String STATE_OWNER = "stateowner"; @@ -75,9 +75,9 @@ final class EventDataKeys { private EventDataKeys() {} } - final class SharedState { + static final class SharedState { - final class Hub { + static final class Hub { static final String NAME = "com.adobe.module.eventhub"; static final String EXTENSIONS = "extensions"; @@ -85,7 +85,7 @@ final class Hub { private Hub() {} } - final class Configuration { + static final class Configuration { static final String NAME = "com.adobe.module.configuration"; static final String EXPERIENCE_CLOUD_ORGID = "experienceCloud.org"; @@ -93,7 +93,7 @@ final class Configuration { private Configuration() {} } - final class IdentityDirect { + static final class IdentityDirect { static final String NAME = "com.adobe.module.identity"; static final String ECID = "mid"; @@ -104,7 +104,7 @@ private IdentityDirect() {} private SharedState() {} } - final class Namespaces { + static final class Namespaces { static final String ECID = "ECID"; static final String IDFA = "IDFA"; @@ -113,14 +113,14 @@ final class Namespaces { private Namespaces() {} } - final class XDMKeys { + static final class XDMKeys { static final String IDENTITY_MAP = "identityMap"; static final String ID = "id"; static final String AUTHENTICATED_STATE = "authenticatedState"; static final String PRIMARY = "primary"; - final class Consent { + static final class Consent { static final String AD_ID = "adID"; static final String CONSENTS = "consents"; @@ -135,7 +135,7 @@ private Consent() {} private XDMKeys() {} } - final class DataStoreKey { + static final class DataStoreKey { static final String DATASTORE_NAME = EXTENSION_NAME; static final String IDENTITY_PROPERTIES = "identity.properties"; @@ -145,7 +145,7 @@ final class DataStoreKey { private DataStoreKey() {} } - final class UrlKeys { + static final class UrlKeys { static final String TS = "TS"; static final String EXPERIENCE_CLOUD_ORG_ID = "MCORGID"; 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 2a4b2ec0..385dc9c7 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 @@ -13,192 +13,160 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionError; -import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.SharedStateResolution; +import com.adobe.marketing.mobile.SharedStateResult; +import com.adobe.marketing.mobile.SharedStateStatus; +import com.adobe.marketing.mobile.util.StringUtils; +import com.adobe.marketing.mobile.util.TimeUtils; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; class IdentityExtension extends Extension { - private ExecutorService executorService; - private final Object executorMutex = new Object(); - private final ConcurrentLinkedQueue cachedEvents; // cached events in memory until required shared states are resolved + /** + * A {@code SharedStateCallback} to retrieve the last set state of an extension and to + * create an XDM state at the event provided. + */ + private final SharedStateCallback sharedStateHandle = new SharedStateCallback() { + @Override + public SharedStateResult getSharedState(final String stateOwner, final Event event) { + return getApi().getSharedState(stateOwner, event, false, SharedStateResolution.LAST_SET); + } + + @Override + public void createXDMSharedState(final Map state, final Event event) { + getApi().createXDMSharedState(state, event); + } + }; - // package private for testing - IdentityState state = new IdentityState(new IdentityProperties()); + private final IdentityState state; /** * Constructor. - * - *

- * Called during the Identity extension's registration. - * The following listeners are registered during this extension's registration. - *

    - *
  • Listener {@link ListenerEventHubBoot} to listen for event with eventType {@link IdentityConstants.EventType#HUB} - * and EventSource {@link IdentityConstants.EventSource#BOOTED}
  • - *
  • Listener {@link ListenerIdentityRequestContent} to listen for event with eventType {@link IdentityConstants.EventType#GENERIC_IDENTITY} - * and EventSource {@link IdentityConstants.EventSource#REQUEST_CONTENT}
  • - *
  • Listener {@link ListenerEdgeIdentityRequestIdentity} to listen for event with eventType {@link IdentityConstants.EventType#EDGE_IDENTITY} - * and EventSource {@link IdentityConstants.EventSource#REQUEST_IDENTITY}
  • - *
  • Listener {@link ListenerEdgeIdentityUpdateIdentity} to listen for event with eventType {@link IdentityConstants.EventType#EDGE_IDENTITY} - * and EventSource {@link IdentityConstants.EventSource#UPDATE_IDENTITY}
  • - *
  • Listener {@link ListenerEdgeIdentityRemoveIdentity} to listen for event with eventType {@link IdentityConstants.EventType#EDGE_IDENTITY} - * and EventSource {@link IdentityConstants.EventSource#REMOVE_IDENTITY}
  • - *
  • Listener {@link ListenerIdentityRequestReset} to listen for event with eventType {@link IdentityConstants.EventType#EDGE_IDENTITY} - * and EventSource {@link IdentityConstants.EventSource#REQUEST_CONTENT}
  • - *
  • Listener {@link ListenerIdentityRequestReset} to listen for event with eventType {@link IdentityConstants.EventType#GENERIC_IDENTITY} - * and EventSource {@link IdentityConstants.EventSource#REQUEST_RESET}
  • - *
  • Listener {@link ListenerHubSharedState} to listen for event with eventType {@link IdentityConstants.EventType#HUB} - * and EventSource {@link IdentityConstants.EventSource#SHARED_STATE}
  • - *
- *

- * Thread : Background thread created by MobileCore + * Invoked on the background thread owned by an extension container that manages this extension. * * @param extensionApi {@link ExtensionApi} instance */ protected IdentityExtension(ExtensionApi extensionApi) { + this(extensionApi, new IdentityState()); + } + + @VisibleForTesting + IdentityExtension(final ExtensionApi extensionApi, final IdentityState state) { super(extensionApi); - cachedEvents = new ConcurrentLinkedQueue<>(); - - ExtensionErrorCallback listenerErrorCallback = new ExtensionErrorCallback() { - @Override - public void error(final ExtensionError extensionError) { - MobileCore.log( - LoggingMode.ERROR, - LOG_TAG, - String.format("Failed to register listener, error: %s", extensionError.getErrorName()) - ); - } - }; - - extensionApi.registerEventListener( - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.BOOTED, - ListenerEventHubBoot.class, - listenerErrorCallback - ); - extensionApi.registerEventListener( - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_CONTENT, - ListenerIdentityRequestContent.class, - listenerErrorCallback - ); - extensionApi.registerEventListener( - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY, - ListenerEdgeIdentityRequestIdentity.class, - listenerErrorCallback - ); - extensionApi.registerEventListener( - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY, - ListenerEdgeIdentityUpdateIdentity.class, - listenerErrorCallback - ); - extensionApi.registerEventListener( - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY, - ListenerEdgeIdentityRemoveIdentity.class, - listenerErrorCallback - ); - extensionApi.registerEventListener( - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE, - ListenerHubSharedState.class, - listenerErrorCallback - ); - extensionApi.registerEventListener( - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_RESET, - ListenerIdentityRequestReset.class, - listenerErrorCallback - ); + this.state = state; } - /** - * Required override. Each extension must have a unique name within the application. - * - * @return unique name of this extension - */ + @NonNull @Override protected String getName() { return IdentityConstants.EXTENSION_NAME; } - /** - * Optional override. - * - * @return the version of this extension - */ @Override protected String getVersion() { return IdentityConstants.EXTENSION_VERSION; } /** - * Adds an event to the event queue and starts processing the queue. + * {@inheritDoc} * - * @param event the received event to be added in the events queue; should not be null + *

+ * The following listeners are registered during this extension's registration. + *

    + *
  • EventType {@link IdentityConstants.EventType#GENERIC_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_CONTENT}
  • + *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_IDENTITY}
  • + *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#UPDATE_IDENTITY}
  • + *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REMOVE_IDENTITY}
  • + *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_CONTENT}
  • + *
  • EventType {@link IdentityConstants.EventType#GENERIC_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_RESET}
  • + *
  • EventType {@link IdentityConstants.EventType#HUB} and EventSource {@link IdentityConstants.EventSource#SHARED_STATE}
  • + *
+ *

*/ - void processAddEvent(final Event event) { - if (event == null) { - return; - } + @Override + protected void onRegistered() { + // GENERIC_IDENTITY event listeners + getApi() + .registerEventListener( + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_CONTENT, + this::handleRequestContent + ); - cachedEvents.add(event); - processCachedEvents(); - } + getApi() + .registerEventListener( + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_RESET, + this::handleRequestReset + ); - /** - * Processes the cached events in the order they were received. - */ - void processCachedEvents() { - if (!state.hasBooted()) { - return; - } + // EDGE_IDENTITY event listeners + getApi() + .registerEventListener( + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY, + this::handleRequestIdentity + ); - while (!cachedEvents.isEmpty()) { - final Event event = cachedEvents.peek(); + getApi() + .registerEventListener( + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.UPDATE_IDENTITY, + this::handleUpdateIdentities + ); - if (EventUtils.isRequestIdentityEvent(event)) { - if (EventUtils.isGetUrlVariablesRequestEvent(event)) { - handleUrlVariablesRequest(event); - } else { - handleIdentityRequest(event); - } - } else if (EventUtils.isRequestContentEvent(event)) { - handleRequestContent(event); - } else if (EventUtils.isUpdateIdentityEvent(event)) { - handleUpdateIdentities(event); - } else if (EventUtils.isRemoveIdentityEvent(event)) { - handleRemoveIdentity(event); - } else if (EventUtils.isRequestResetEvent(event)) { - handleRequestReset(event); - } else if (EventUtils.isSharedStateUpdateFor(IdentityConstants.SharedState.IdentityDirect.NAME, event)) { - handleIdentityDirectECIDUpdate(event); - } - - cachedEvents.poll(); + getApi() + .registerEventListener( + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REMOVE_IDENTITY, + this::handleRemoveIdentity + ); + + // HUB shared state event listener + getApi() + .registerEventListener( + IdentityConstants.EventType.HUB, + IdentityConstants.EventSource.SHARED_STATE, + this::handleIdentityDirectECIDUpdate + ); + } + + @Override + public boolean readyForEvent(@NonNull Event event) { + if (!state.bootupIfReady(sharedStateHandle)) return false; + + // Get url variables request depends on Configuration shared state + // Wait for configuration state to be set before processing such an event. + if (EventUtils.isGetUrlVariablesRequestEvent(event)) { + final SharedStateResult configurationStateResult = sharedStateHandle.getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event + ); + + return (configurationStateResult != null && configurationStateResult.getStatus() == SharedStateStatus.SET); } + + return true; } /** - * Calls {@link IdentityState#bootupIfReady(SharedStateCallback)} with a valid callback - * - * @return True if the bootup is complete + * Handles events requesting for identifiers. Dispatches a response event containing requested identifiers. + * @param event the identity request event */ - boolean bootupIfReady() { - final SharedStateCallback callback = createSharedStateCallback(); - - return state.bootupIfReady(callback); + void handleRequestIdentity(@NonNull final Event event) { + if (EventUtils.isGetUrlVariablesRequestEvent(event)) { + handleUrlVariablesRequest(event); + } else { + handleGetIdentifiersRequest(event); + } } /** @@ -206,39 +174,44 @@ boolean bootupIfReady() { * * @param event the identity request {@link Event} */ - void handleUrlVariablesRequest(final Event event) { - String urlVariablesString = null; - - final Map configurationState = getSharedState( + private void handleUrlVariablesRequest(@NonNull final Event event) { + final SharedStateResult configSharedStateResult = sharedStateHandle.getSharedState( IdentityConstants.SharedState.Configuration.NAME, event ); + final Map configurationState = configSharedStateResult != null + ? configSharedStateResult.getValue() + : null; + final String orgId = EventUtils.getOrgId(configurationState); - if (Utils.isNullOrEmpty(orgId)) { + if (StringUtils.isNullOrEmpty(orgId)) { handleUrlVariableResponse( event, - urlVariablesString, + null, "IdentityExtension - Cannot process getUrlVariables request Identity event, Experience Cloud Org ID not found in configuration." ); return; } - ECID ecid = state.getIdentityProperties().getECID(); + final ECID ecid = state.getIdentityProperties().getECID(); final String ecidString = ecid != null ? ecid.toString() : null; - if (Utils.isNullOrEmpty(ecidString)) { + if (StringUtils.isNullOrEmpty(ecidString)) { handleUrlVariableResponse( event, - urlVariablesString, + null, "IdentityExtension - Cannot process getUrlVariables request Identity event, ECID not found." ); return; } - urlVariablesString = - URLUtils.generateURLVariablesPayload(String.valueOf(Utils.getUnixTimeInSeconds()), ecidString, orgId); + final String urlVariablesString = URLUtils.generateURLVariablesPayload( + String.valueOf(TimeUtils.getUnixTimeInSeconds()), + ecidString, + orgId + ); handleUrlVariableResponse(event, urlVariablesString); } @@ -249,7 +222,7 @@ void handleUrlVariablesRequest(final Event event) { * @param event the identity request {@link Event} * @param urlVariables {@link String} representing the urlVariables encoded string */ - private void handleUrlVariableResponse(final Event event, final String urlVariables) { + void handleUrlVariableResponse(@NonNull final Event event, final String urlVariables) { handleUrlVariableResponse(event, urlVariables, null); } @@ -260,7 +233,7 @@ private void handleUrlVariableResponse(final Event event, final String urlVariab * @param urlVariables {@link String} representing the urlVariables encoded string * @param errorMsg {@link String} representing error encountered while generating the urlVariables string */ - private void handleUrlVariableResponse(final Event event, final String urlVariables, final String errorMsg) { + void handleUrlVariableResponse(@NonNull final Event event, final String urlVariables, final String errorMsg) { Event responseEvent = new Event.Builder( IdentityConstants.EventNames.IDENTITY_RESPONSE_URL_VARIABLES, IdentityConstants.EventType.EDGE_IDENTITY, @@ -273,27 +246,14 @@ private void handleUrlVariableResponse(final Event event, final String urlVariab } } ) + .inResponseToEvent(event) .build(); - ExtensionErrorCallback errorCallback = 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() - ); - } - }; - - if (Utils.isNullOrEmpty(urlVariables) && !Utils.isNullOrEmpty(errorMsg)) { + if (StringUtils.isNullOrEmpty(urlVariables) && !StringUtils.isNullOrEmpty(errorMsg)) { MobileCore.log(LoggingMode.WARNING, LOG_TAG, errorMsg); } - MobileCore.dispatchResponseEvent(responseEvent, event, errorCallback); + getApi().dispatch(responseEvent); } /** @@ -301,8 +261,11 @@ public void error(ExtensionError extensionError) { * * @param event the edge update identity {@link Event} */ - void handleUpdateIdentities(final Event event) { - final Map eventData = event.getEventData(); // do not need to null check on eventData, as they are done on listeners + void handleUpdateIdentities(@NonNull final Event event) { + final Map eventData = event.getEventData(); + + if (eventData == null) return; // TODO: Add log message when logging changes are made + final IdentityMap map = IdentityMap.fromXDMMap(eventData); if (map == null) { @@ -323,8 +286,11 @@ void handleUpdateIdentities(final Event event) { * * @param event the edge remove identity request {@link Event} */ - void handleRemoveIdentity(final Event event) { - final Map eventData = event.getEventData(); // do not need to null check on eventData, as they are done on listeners + void handleRemoveIdentity(@NonNull final Event event) { + final Map eventData = event.getEventData(); + + if (eventData == null) return; // TODO: Add log message when logging changes are made + final IdentityMap map = IdentityMap.fromXDMMap(eventData); if (map == null) { @@ -340,64 +306,23 @@ void handleRemoveIdentity(final Event event) { shareIdentityXDMSharedState(event); } - /** - * Handles events of type {@code com.adobe.eventType.hub} and source {@code com.adobe.eventSource.sharedState}. - * If the state change event is for the direct Identity extension, get the direct Identity shared state and attempt - * to update the legacy ECID with the direct Identity extension ECID. - * - * @param event an event of type {@code com.adobe.eventType.hub} and source {@code com.adobe.eventSource.sharedState}; - * the event and its data should not be null, checked in listener - */ - void handleHubSharedState(final Event event) { - if ( - !EventUtils.isSharedStateUpdateFor(IdentityConstants.SharedState.Hub.NAME, event) && - !EventUtils.isSharedStateUpdateFor(IdentityConstants.SharedState.IdentityDirect.NAME, event) - ) { - return; - } - - if (state.hasBooted()) { - // regular Identity Direct shared state update, queue event for handleIdentityDirectECIDUpdate - processAddEvent(event); - } else { - if (bootupIfReady()) { - processCachedEvents(); - } - } - } - /** * Handles events requesting for identifiers. Dispatches response event containing the identifiers. Called by listener registered with event hub. * * @param event the identity request {@link Event} */ - void handleIdentityRequest(final Event event) { - Map xdmData = state.getIdentityProperties().toXDMData(false); - Event responseEvent = new Event.Builder( + private void handleGetIdentifiersRequest(@NonNull final Event event) { + final Map xdmData = state.getIdentityProperties().toXDMData(false); + final Event responseEvent = new Event.Builder( IdentityConstants.EventNames.IDENTITY_RESPONSE_CONTENT_ONE_TIME, IdentityConstants.EventType.EDGE_IDENTITY, IdentityConstants.EventSource.RESPONSE_IDENTITY ) .setEventData(xdmData) + .inResponseToEvent(event) .build(); - 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() - ); - } - } - ); + getApi().dispatch(responseEvent); } /** @@ -405,7 +330,7 @@ public void error(ExtensionError extensionError) { * * @param event the identity request reset {@link Event} */ - void handleRequestReset(final Event event) { + void handleRequestReset(@NonNull final Event event) { state.resetIdentifiers(); shareIdentityXDMSharedState(event); @@ -415,24 +340,10 @@ void handleRequestReset(final Event event) { IdentityConstants.EventType.EDGE_IDENTITY, IdentityConstants.EventSource.RESET_COMPLETE ) + .inResponseToEvent(event) .build(); - MobileCore.dispatchEvent( - responseEvent, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityExtension - Failed to dispatch Edge Identity reset response event for event " + - event.getUniqueIdentifier() + - " with error " + - extensionError.getErrorName() - ); - } - } - ); + getApi().dispatch(responseEvent); } /** @@ -440,12 +351,20 @@ public void error(ExtensionError extensionError) { * * @param event the shared state update {@link Event} */ - void handleIdentityDirectECIDUpdate(final Event event) { - final Map identityState = getSharedState( + void handleIdentityDirectECIDUpdate(@NonNull final Event event) { + if (!EventUtils.isSharedStateUpdateFor(IdentityConstants.SharedState.IdentityDirect.NAME, event)) { + return; + } + + final SharedStateResult identitySharedStateResult = sharedStateHandle.getSharedState( IdentityConstants.SharedState.IdentityDirect.NAME, event ); + final Map identityState = (identitySharedStateResult != null) + ? identitySharedStateResult.getValue() + : null; + if (identityState == null) { return; } @@ -462,61 +381,12 @@ void handleIdentityDirectECIDUpdate(final Event event) { * * @param event the {@link Event} containing advertising identifier data */ - void handleRequestContent(final Event event) { + void handleRequestContent(@NonNull final Event event) { if (!EventUtils.isAdIdEvent(event)) { return; } // Doesn't need event dispatcher because MobileCore can be called directly - state.updateAdvertisingIdentifier(event, createSharedStateCallback()); - } - - /** - * Called by listeners to retrieve an {@code ExecutorService}. - * The {@code ExecutorService} is used to process events on a separate thread than the - * {@code EventHub} thread on which they were received. Processing events on a separate - * thread prevents blocking of the {@code EventHub}. - * - * @return this extension's instance of a single thread executor - */ - ExecutorService getExecutor() { - synchronized (executorMutex) { - if (executorService == null) { - executorService = Executors.newSingleThreadExecutor(); - } - - return executorService; - } - } - - /** - * Retrieves the shared state for the given state owner - * - * @param stateOwner the state owner for the requested shared state - * @param event the {@link Event} for which is shared state is to be retrieved - */ - private Map getSharedState(final String stateOwner, final Event event) { - final ExtensionApi extensionApi = getApi(); - - if (extensionApi == null) { - return null; - } - - final ExtensionErrorCallback getSharedStateCallback = new ExtensionErrorCallback() { - @Override - public void error(final ExtensionError extensionError) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - String.format( - "IdentityExtension - Failed getting %s shared state. Error : %s.", - stateOwner, - extensionError.getErrorName() - ) - ); - } - }; - - return extensionApi.getSharedEventState(stateOwner, event, getSharedStateCallback); + state.updateAdvertisingIdentifier(event, sharedStateHandle); } /** @@ -525,90 +395,6 @@ public void error(final ExtensionError extensionError) { * @param event the {@link Event} that triggered the XDM shared state change */ private void shareIdentityXDMSharedState(final Event event) { - final ExtensionApi extensionApi = super.getApi(); - - if (extensionApi == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityExtension - ExtensionApi is null, unable to share XDM shared state for reset identities" - ); - return; - } - - // set the shared state - final ExtensionErrorCallback errorCallback = new ExtensionErrorCallback() { - @Override - public void error(final ExtensionError extensionError) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - String.format( - "IdentityExtension - Failed create XDM shared state. Error : %s.", - extensionError.getErrorName() - ) - ); - } - }; - - extensionApi.setXDMSharedEventState(state.getIdentityProperties().toXDMData(false), event, errorCallback); - } - - /** - * Creates standard shared state callback with functionality from {@link ExtensionApi} - * @return a new instance of {@link SharedStateCallback} - */ - private SharedStateCallback createSharedStateCallback() { - return new SharedStateCallback() { - @Override - public Map getSharedState(final String stateOwner, final Event event) { - ExtensionApi api = getApi(); - - if (api == null) { - return null; - } - - return api.getSharedEventState( - stateOwner, - event, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.WARNING, - LOG_TAG, - "SharedStateCallback - Unable to fetch shared state, failed with error: " + - extensionError.getErrorName() - ); - } - } - ); - } - - @Override - public boolean setXDMSharedEventState(final Map state, final Event event) { - ExtensionApi api = getApi(); - - if (api == null) { - return false; - } - - return api.setXDMSharedEventState( - state, - event, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.WARNING, - LOG_TAG, - "SharedStateCallback - Unable to set XDM shared state, failed with error: " + - extensionError.getErrorName() - ); - } - } - ); - } - }; + sharedStateHandle.createXDMSharedState(state.getIdentityProperties().toXDMData(false), event); } } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java index f40959d5..0a400b0b 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java @@ -15,6 +15,8 @@ import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.DataReaderException; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -166,23 +168,20 @@ static IdentityItem fromData(final Map data) { } try { - final String id = (String) data.get(IdentityConstants.XDMKeys.ID); - AuthenticatedState authenticatedState = AuthenticatedState.fromString( - (String) data.get(IdentityConstants.XDMKeys.AUTHENTICATED_STATE) + final String id = DataReader.getString(data, IdentityConstants.XDMKeys.ID); + + final AuthenticatedState authenticatedState = AuthenticatedState.fromString( + DataReader.optString( + data, + IdentityConstants.XDMKeys.AUTHENTICATED_STATE, + AuthenticatedState.AMBIGUOUS.getName() + ) ); - if (authenticatedState == null) { - authenticatedState = AuthenticatedState.AMBIGUOUS; - } - - boolean primary = false; - - if (data.get(IdentityConstants.XDMKeys.PRIMARY) != null) { - primary = (boolean) data.get(IdentityConstants.XDMKeys.PRIMARY); - } + final boolean primary = DataReader.optBoolean(data, IdentityConstants.XDMKeys.PRIMARY, false); return new IdentityItem(id, authenticatedState, primary); - } catch (ClassCastException e) { + } catch (final DataReaderException e) { MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "IdentityItem - Failed to create IdentityItem from data."); return null; } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java index 41fa4c64..c82f3469 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java @@ -15,6 +15,8 @@ import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.StringUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -43,7 +45,7 @@ public class IdentityMap { public List getIdentityItemsForNamespace(final String namespace) { final List copyItems = new ArrayList<>(); - if (Utils.isNullOrEmpty(namespace)) { + if (StringUtils.isNullOrEmpty(namespace)) { return copyItems; } @@ -96,7 +98,7 @@ public void removeItem(final IdentityItem item, final String namespace) { return; } - if (Utils.isNullOrEmpty(namespace)) { + if (StringUtils.isNullOrEmpty(namespace)) { MobileCore.log( LoggingMode.DEBUG, LOG_TAG, @@ -167,7 +169,7 @@ void addItem(final IdentityItem item, final String namespace, final boolean isFi return; } - if (Utils.isNullOrEmpty(namespace)) { + if (StringUtils.isNullOrEmpty(namespace)) { MobileCore.log( LoggingMode.DEBUG, LOG_TAG, @@ -281,7 +283,7 @@ Map asXDMMap(final boolean allowEmpty) { } /** - * Creates an {@link IdentityMap} from the given xdm formatted {@link Map} + * Creates an {@link IdentityMap} from the given xdm formatted immutable {@link Map} * Returns null if the provided map is null/empty. * Return null if the provided map is not in Identity Map's XDM format. * @@ -292,16 +294,12 @@ static IdentityMap fromXDMMap(final Map map) { return null; } - Map identityMapDict = null; - try { - identityMapDict = (HashMap) map.get(IdentityConstants.XDMKeys.IDENTITY_MAP); - } catch (ClassCastException e) { - MobileCore.log( - LoggingMode.ERROR, - LOG_TAG, - String.format("Failed to create IdentityMap from data. Exception thrown: %s", e.getLocalizedMessage()) - ); - } + final Map identityMapDict = DataReader.optTypedMap( + Object.class, + map, + IdentityConstants.XDMKeys.IDENTITY_MAP, + null + ); if (identityMapDict == null) { return null; @@ -309,31 +307,21 @@ static IdentityMap fromXDMMap(final Map map) { final IdentityMap identityMap = new IdentityMap(); for (final String namespace : identityMapDict.keySet()) { - try { - final ArrayList> idArr = (ArrayList>) identityMapDict.get( - namespace - ); - if (idArr == null) { - continue; - } + final List> immutableIdList = DataReader.optTypedListOfMap( + Object.class, + identityMapDict, + namespace, + null + ); + + if (immutableIdList == null) continue; - for (Object idMap : idArr) { - final IdentityItem item = IdentityItem.fromData((Map) idMap); + for (final Map idMap : immutableIdList) { + final IdentityItem item = IdentityItem.fromData(idMap); - if (item != null) { - identityMap.addItemToMap(item, namespace, false); - } + if (item != null) { + identityMap.addItemToMap(item, namespace, false); } - } catch (ClassCastException e) { - MobileCore.log( - LoggingMode.ERROR, - LOG_TAG, - String.format( - "Failed to parse data for namespace (%s). Exception thrown: %s", - namespace, - e.getLocalizedMessage() - ) - ); } } 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 3547f3c5..110b313c 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 @@ -15,6 +15,7 @@ import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -82,7 +83,7 @@ void setAdId(final String newAdId) { identityMap.removeItem(previousAdIdItem, IdentityConstants.Namespaces.GAID); } - if (Utils.isNullOrEmpty(newAdId)) { + if (StringUtils.isNullOrEmpty(newAdId)) { return; } @@ -130,7 +131,7 @@ ECID getECID() { ecidItems != null && !ecidItems.isEmpty() && ecidItems.get(0) != null && - !Utils.isNullOrEmpty(ecidItems.get(0).getId()) + !StringUtils.isNullOrEmpty(ecidItems.get(0).getId()) ) { return new ECID(ecidItems.get(0).getId()); } @@ -183,7 +184,7 @@ ECID getECIDSecondary() { ecidItems != null && ecidItems.size() > 1 && ecidItems.get(1) != null && - !Utils.isNullOrEmpty(ecidItems.get(1).getId()) + !StringUtils.isNullOrEmpty(ecidItems.get(1).getId()) ) { return new ECID(ecidItems.get(1).getId()); } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index f250b046..40a6f291 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -13,11 +13,13 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; +import androidx.annotation.VisibleForTesting; import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.ExtensionError; -import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.SharedStateResult; +import com.adobe.marketing.mobile.SharedStateStatus; +import com.adobe.marketing.mobile.util.DataReader; import java.util.HashMap; import java.util.Map; @@ -29,18 +31,27 @@ class IdentityState { private IdentityProperties identityProperties; private boolean hasBooted; + /** + * Loads the persisted identities (if any) into {@link #identityProperties} + */ + IdentityState() { + this(IdentityStorageService.loadPropertiesFromPersistence()); + } + /** * Creates a new {@link IdentityState} with the given {@link IdentityProperties} * * @param identityProperties identity properties */ + @VisibleForTesting IdentityState(final IdentityProperties identityProperties) { - this.identityProperties = identityProperties; + this.identityProperties = (identityProperties != null) ? identityProperties : new IdentityProperties(); } /** * @return the current bootup status */ + @VisibleForTesting boolean hasBooted() { return hasBooted; } @@ -54,7 +65,6 @@ IdentityProperties getIdentityProperties() { /** * Completes init for this Identity extension. - * Attempts to load the already persisted identities from persistence into {@link #identityProperties} * If no ECID is loaded from persistence (ideally meaning first launch), attempts to migrate existing ECID * from the direct Identity Extension, either from its persisted store or from its shared state if the * direct Identity extension is registered. If no ECID is found for migration, then a new ECID is generated. @@ -69,15 +79,19 @@ boolean bootupIfReady(final SharedStateCallback callback) { return true; } - // Load properties from local storage - identityProperties = IdentityStorageService.loadPropertiesFromPersistence(); - - if (identityProperties == null) { - identityProperties = new IdentityProperties(); - } - // Reuse the ECID from Identity Direct (if registered) or generate new ECID on first launch if (identityProperties.getECID() == null) { + // Wait for all extensions to be registered as forthcoming logic depends on Identity Direct state. + // This is inferred via EventHub's shared state and is based on the assumption that EventHub + // sets its state only when all the extensions are registered initially. + final SharedStateResult eventHubStateResult = callback.getSharedState( + IdentityConstants.SharedState.Hub.NAME, + null + ); + if (eventHubStateResult == null || eventHubStateResult.getStatus() != SharedStateStatus.SET) { + return false; + } + // Attempt to get ECID from direct Identity persistence to migrate an existing ECID final ECID directIdentityEcid = IdentityStorageService.loadEcidFromDirectIdentityPersistence(); @@ -92,25 +106,25 @@ boolean bootupIfReady(final SharedStateCallback callback) { ); } // If direct Identity has no persisted ECID, check if direct Identity is registered with the SDK - else if (isIdentityDirectRegistered(callback)) { - final Map identityDirectSharedState = callback.getSharedState( + else if (isIdentityDirectRegistered(eventHubStateResult.getValue())) { + // If the direct Identity extension is registered, attempt to get its shared state + final SharedStateResult sharedStateResult = callback.getSharedState( IdentityConstants.SharedState.IdentityDirect.NAME, null ); - // If the direct Identity extension is registered, attempt to get its shared state - if (identityDirectSharedState != null) { // identity direct shared state is set - handleECIDFromIdentityDirect(EventUtils.getECID(identityDirectSharedState)); - } // If there is no direct Identity shared state, abort boot-up and try again when direct Identity shares its state - else { + if (sharedStateResult == null || sharedStateResult.getStatus() != SharedStateStatus.SET) { 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 + return false; } + + final Map identityDirectSharedState = sharedStateResult.getValue(); + handleECIDFromIdentityDirect(EventUtils.getECID(identityDirectSharedState)); } // Generate a new ECID as the direct Identity extension is not registered with the SDK and there was no direct Identity persisted ECID else { @@ -127,7 +141,7 @@ else if (isIdentityDirectRegistered(callback)) { hasBooted = true; MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "IdentityState - Edge Identity has successfully booted up"); - callback.setXDMSharedEventState(identityProperties.toXDMData(false), null); + callback.createXDMSharedState(identityProperties.toXDMData(false), null); return hasBooted; } @@ -199,7 +213,7 @@ void updateAdvertisingIdentifier(final Event event, final SharedStateCallback ca // Save to persistence IdentityStorageService.savePropertiesToPersistence(identityProperties); - callback.setXDMSharedEventState(identityProperties.toXDMData(false), event); + callback.createXDMSharedState(identityProperties.toXDMData(false), event); } /** @@ -264,35 +278,27 @@ private void handleECIDFromIdentityDirect(final ECID legacyEcid) { /** * 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; should not be null - * @return true if the Identity direct extension is registered with the EventHub + * @param eventHubSharedState a {@code Map registeredExtensionsWithHub = callback.getSharedState( - IdentityConstants.SharedState.Hub.NAME, + private boolean isIdentityDirectRegistered(final Map eventHubSharedState) { + if (eventHubSharedState == null) { + return false; + } + + final Map extensions = DataReader.optTypedMap( + Object.class, + eventHubSharedState, + IdentityConstants.SharedState.Hub.EXTENSIONS, null ); - Map identityDirectInfo = null; - - if (registeredExtensionsWithHub != null) { - try { - final Map extensions = (HashMap) registeredExtensionsWithHub.get( - IdentityConstants.SharedState.Hub.EXTENSIONS - ); - - if (extensions != null) { - identityDirectInfo = - (HashMap) 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" - ); - } - } + final Map identityDirectInfo = DataReader.optTypedMap( + Object.class, + extensions, + IdentityConstants.SharedState.Hub.EXTENSIONS, + null + ); return !Utils.isNullOrEmpty(identityDirectInfo); } @@ -334,21 +340,6 @@ private void dispatchAdIdConsentRequestEvent(final String consentVal) { .setEventData(consentData) .build(); - MobileCore.dispatchEvent( - consentEvent, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Failed to dispatch consent event " + - consentEvent.toString() + - ": " + - extensionError.getErrorName() - ); - } - } - ); + MobileCore.dispatchEvent(consentEvent); } } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java index a3b6d80b..fb6d2f51 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java @@ -18,6 +18,7 @@ import android.content.SharedPreferences; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.JSONUtils; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; @@ -59,7 +60,7 @@ static IdentityProperties loadPropertiesFromPersistence() { try { final JSONObject jsonObject = new JSONObject(jsonString); - final Map propertyMap = Utils.toMap(jsonObject); + final Map propertyMap = JSONUtils.toMap(jsonObject); return new IdentityProperties(propertyMap); } catch (JSONException exception) { MobileCore.log( diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentity.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentity.java deleted file mode 100644 index 1d734b63..00000000 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentity.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionListener; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; - -class ListenerEdgeIdentityRemoveIdentity extends ExtensionListener { - - /** - * Constructor. - * - * @param extensionApi an instance of {@link ExtensionApi} - * @param type the {@link String} eventType this listener is registered to handle - * @param source the {@link String} eventSource this listener is registered to handle - */ - ListenerEdgeIdentityRemoveIdentity(final ExtensionApi extensionApi, final String type, final String source) { - super(extensionApi, type, source); - } - - /** - * Method that gets called when event with event type {@link IdentityConstants.EventType#EDGE_IDENTITY} - * and with event source {@link IdentityConstants.EventSource#REMOVE_IDENTITY} is dispatched through eventHub. - * - * @param event the remove identity {@link Event} to be processed - */ - @Override - public void hear(final Event event) { - if (event == null || event.getEventData() == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerEdgeIdentityRemoveIdentity - Event or Event data is null. Ignoring the event." - ); - return; - } - - final IdentityExtension parentExtension = getIdentityExtension(); - - if (parentExtension == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerEdgeIdentityRemoveIdentity - The parent extension, associated with this listener is null, Ignoring the event." - ); - return; - } - - parentExtension - .getExecutor() - .execute( - new Runnable() { - @Override - public void run() { - parentExtension.processAddEvent(event); - } - } - ); - } - - /** - * Returns the parent extension associated with the listener. - * - * @return a {@link IdentityExtension} object registered with the eventHub - */ - IdentityExtension getIdentityExtension() { - return (IdentityExtension) getParentExtension(); - } -} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentity.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentity.java deleted file mode 100644 index cf71f5a6..00000000 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentity.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionListener; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; - -class ListenerEdgeIdentityRequestIdentity extends ExtensionListener { - - /** - * Constructor. - * - * @param extensionApi an instance of {@link ExtensionApi} - * @param type the {@link String} eventType this listener is registered to handle - * @param source the {@link String} eventSource this listener is registered to handle - */ - ListenerEdgeIdentityRequestIdentity(final ExtensionApi extensionApi, final String type, final String source) { - super(extensionApi, type, source); - } - - /** - * Method that gets called when event with event type {@link IdentityConstants.EventType#GENERIC_IDENTITY} - * and with event source {@link IdentityConstants.EventSource#REQUEST_CONTENT} is dispatched through eventHub. - * - * @param event the request identity {@link Event} to be processed - */ - @Override - public void hear(final Event event) { - if (event == null || event.getEventData() == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerEdgeIdentityRequestIdentity - Event or Event data is null. Ignoring the event." - ); - return; - } - - final IdentityExtension parentExtension = getIdentityExtension(); - - if (parentExtension == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerEdgeIdentityRequestIdentity - The parent extension, associated with this listener is null. Ignoring the event." - ); - return; - } - - parentExtension - .getExecutor() - .execute( - new Runnable() { - @Override - public void run() { - parentExtension.processAddEvent(event); - } - } - ); - } - - /** - * Returns the parent extension associated with the listener. - * - * @return a {@link IdentityExtension} object registered with the eventHub - */ - IdentityExtension getIdentityExtension() { - return (IdentityExtension) getParentExtension(); - } -} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentity.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentity.java deleted file mode 100644 index 09bad374..00000000 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentity.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionListener; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; - -class ListenerEdgeIdentityUpdateIdentity extends ExtensionListener { - - /** - * Constructor. - * - * @param extensionApi an instance of {@link ExtensionApi} - * @param type the {@link String} eventType this listener is registered to handle - * @param source the {@link String} eventSource this listener is registered to handle - */ - ListenerEdgeIdentityUpdateIdentity(final ExtensionApi extensionApi, final String type, final String source) { - super(extensionApi, type, source); - } - - /** - * Method that gets called when event with event type {@link IdentityConstants.EventType#EDGE_IDENTITY} - * and with event source {@link IdentityConstants.EventSource#UPDATE_IDENTITY} is dispatched through eventHub. - * - * @param event the udpate identity {@link Event} to be processed - */ - @Override - public void hear(final Event event) { - if (event == null || event.getEventData() == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerEdgeIdentityUpdateIdentity - Event or Event data is null. Ignoring the event." - ); - return; - } - - final IdentityExtension parentExtension = getIdentityExtension(); - - if (parentExtension == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerEdgeIdentityUpdateIdentity - The parent extension, associated with this listener is null, ignoring event." - ); - return; - } - - parentExtension - .getExecutor() - .execute( - new Runnable() { - @Override - public void run() { - parentExtension.processAddEvent(event); - } - } - ); - } - - /** - * Returns the parent extension associated with the listener. - * - * @return a {@link IdentityExtension} object registered with the eventHub - */ - IdentityExtension getIdentityExtension() { - return (IdentityExtension) getParentExtension(); - } -} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBoot.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBoot.java deleted file mode 100644 index d9872173..00000000 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBoot.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - 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 com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionListener; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; - -class ListenerEventHubBoot extends ExtensionListener { - - /** - * Constructor. - * - * @param extensionApi an instance of {@link ExtensionApi} - * @param type the {@link String} eventType this listener is registered to handle - * @param source the {@link String} eventSource this listener is registered to handle - */ - ListenerEventHubBoot(final ExtensionApi extensionApi, final String type, final String source) { - super(extensionApi, type, source); - } - - /** - * Method that gets called when event with event type {@link IdentityConstants.EventType#HUB} - * and with event source {@link IdentityConstants.EventSource#BOOTED} is dispatched through eventHub. - * - * @param event the boot {@link Event} - */ - @Override - public void hear(final Event event) { - final IdentityExtension parentExtension = getIdentityExtension(); - - if (parentExtension == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerEventHubBoot - The parent extension associated with this listener is null, ignoring this event." - ); - return; - } - - parentExtension - .getExecutor() - .execute( - new Runnable() { - @Override - public void run() { - parentExtension.bootupIfReady(); - } - } - ); - } - - /** - * Returns the parent extension associated with the listener. - * - * @return a {@link IdentityExtension} object registered with the eventHub - */ - IdentityExtension getIdentityExtension() { - return (IdentityExtension) getParentExtension(); - } -} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedState.java deleted file mode 100644 index 1f5125fd..00000000 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedState.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionListener; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; - -class ListenerHubSharedState extends ExtensionListener { - - /** - * Constructor. - * - * @param extensionApi an instance of {@link ExtensionApi} - * @param type the {@link String} eventType this listener is registered to handle - * @param source the {@link String} eventSource this listener is registered to handle - */ - ListenerHubSharedState(final ExtensionApi extensionApi, final String type, final String source) { - super(extensionApi, type, source); - } - - /** - * Method that gets called when event with event type {@link IdentityConstants.EventType#HUB} - * and with event source {@link IdentityConstants.EventSource#SHARED_STATE} is dispatched through eventHub. - * - * @param event the hub shared state change {@link Event} to be processed - */ - @Override - public void hear(final Event event) { - if (event == null || event.getEventData() == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerHubSharedState - Event / EventData is null. Ignoring the event." - ); - return; - } - - final IdentityExtension parentExtension = getIdentityExtension(); - - if (parentExtension == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerHubSharedState - The parent extension, associated with this listener is null, ignoring the event." - ); - return; - } - - parentExtension - .getExecutor() - .execute( - new Runnable() { - @Override - public void run() { - parentExtension.handleHubSharedState(event); - } - } - ); - } - - /** - * Returns the parent extension associated with the listener. - * - * @return a {@link IdentityExtension} object registered with the eventHub - */ - IdentityExtension getIdentityExtension() { - return (IdentityExtension) getParentExtension(); - } -} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContent.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContent.java deleted file mode 100644 index c2222c80..00000000 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContent.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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.Event; -import com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionListener; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; - -class ListenerIdentityRequestContent extends ExtensionListener { - - /** - * Constructor. - * - * @param extensionApi an instance of {@link ExtensionApi} - * @param type the {@link String} eventType this listener is registered to handle - * @param source the {@link String} eventSource this listener is registered to handle - */ - ListenerIdentityRequestContent(final ExtensionApi extensionApi, final String type, final String source) { - super(extensionApi, type, source); - } - - /** - * Method that gets called when event with event type {@link IdentityConstants.EventType#GENERIC_IDENTITY} - * and with event source {@link IdentityConstants.EventSource#REQUEST_CONTENT} is dispatched through eventHub. - * - * @param event the advertising identifier request content {@link Event} to be processed - */ - @Override - public void hear(final Event event) { - if (event == null || event.getEventData() == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerIdentityRequestContent - Event or Event data is null. Ignoring the event." - ); - return; - } - - final IdentityExtension parentExtension = getIdentityExtension(); - - if (parentExtension == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerIdentityRequestContent - The parent extension, associated with this listener is null, ignoring event." - ); - return; - } - - parentExtension - .getExecutor() - .execute( - new Runnable() { - @Override - public void run() { - parentExtension.processAddEvent(event); - } - } - ); - } - - /** - * Returns the parent extension associated with the listener. - * - * @return a {@link IdentityExtension} object registered with the eventHub - */ - IdentityExtension getIdentityExtension() { - return (IdentityExtension) getParentExtension(); - } -} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestReset.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestReset.java deleted file mode 100644 index fd7fc25c..00000000 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestReset.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionListener; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; - -class ListenerIdentityRequestReset extends ExtensionListener { - - /** - * Constructor. - * - * @param extensionApi an instance of {@link ExtensionApi} - * @param type the {@link String} eventType this listener is registered to handle - * @param source the {@link String} eventSource this listener is registered to handle - */ - ListenerIdentityRequestReset(final ExtensionApi extensionApi, final String type, final String source) { - super(extensionApi, type, source); - } - - /** - * Method that gets called when event with event type {@link IdentityConstants.EventType#GENERIC_IDENTITY} - * and with event source {@link IdentityConstants.EventSource#REQUEST_RESET} is dispatched through eventHub. - * - * @param event the identity reset request {@link Event} to be processed - */ - @Override - public void hear(final Event event) { - if (event == null || event.getEventData() == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerIdentityRequestReset - Event or Event data is null. Ignoring the event." - ); - return; - } - - final IdentityExtension parentExtension = getIdentityExtension(); - - if (parentExtension == null) { - MobileCore.log( - LoggingMode.DEBUG, - IdentityConstants.LOG_TAG, - "ListenerIdentityRequestReset - The parent extension, associated with this listener is null, ignoring event." - ); - return; - } - - parentExtension - .getExecutor() - .execute( - new Runnable() { - @Override - public void run() { - parentExtension.processAddEvent(event); - } - } - ); - } - - /** - * Returns the parent extension associated with the listener. - * - * @return a {@link IdentityExtension} object registered with the eventHub - */ - IdentityExtension getIdentityExtension() { - return (IdentityExtension) getParentExtension(); - } -} diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/SharedStateCallback.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/SharedStateCallback.java index 9cda5606..8580e407 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/SharedStateCallback.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/SharedStateCallback.java @@ -12,10 +12,11 @@ package com.adobe.marketing.mobile.edge.identity; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.SharedStateResult; import java.util.Map; /** - * Callback for fetching Shared States from the outside of the extension class. + * Callback for streamlining Shared State operations (within and outside the extension class) */ interface SharedStateCallback { /** @@ -23,9 +24,9 @@ interface SharedStateCallback { * * @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 + * @return a {@code SharedStateResult} at the event; null if an error occurred */ - Map getSharedState(final String stateOwner, final Event event); + SharedStateResult getSharedState(final String stateOwner, final Event event); /** * Creates an XDM Shared State for the provided {@code event} with the specified {@code state}. @@ -33,5 +34,5 @@ interface SharedStateCallback { * @param state data to be set as XDM Shared State * @param event current event for which to set the shared state; if null is passed, the next shared state version will be set */ - boolean setXDMSharedEventState(final Map state, final Event event); + void createXDMSharedState(final Map state, final Event event); } 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 index 2a62a87b..8b5e81e9 100644 --- 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 @@ -13,6 +13,7 @@ import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.StringUtils; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -48,7 +49,7 @@ static String generateURLVariablesPayload(final String ts, final String ecid, fi urlFragment.append(IdentityConstants.UrlKeys.PAYLOAD); urlFragment.append("="); - if (Utils.isNullOrEmpty(theIdString)) { + if (StringUtils.isNullOrEmpty(theIdString)) { // No need to encode urlFragment.append("null"); } else { @@ -75,7 +76,7 @@ static String generateURLVariablesPayload(final String ts, final String ecid, fi */ 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)) { + if (StringUtils.isNullOrEmpty(key) || StringUtils.isNullOrEmpty(value)) { return originalString; } @@ -83,7 +84,7 @@ static String appendKVPToVisitorIdString(final String originalString, final Stri 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)) { + if (StringUtils.isNullOrEmpty(originalString)) { return newUrlVariable; } else { return String.format("%s|%s", originalString, newUrlVariable); 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 4e628f4e..2ad211ea 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 @@ -15,25 +15,17 @@ import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.JSONUtils; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; class Utils { - private static final long MILLISECONDS_PER_SECOND = 1000L; - private Utils() {} - static boolean isNullOrEmpty(final String str) { - return str == null || str.isEmpty(); - } - static boolean isNullOrEmpty(final Map map) { return map == null || map.isEmpty(); } @@ -59,107 +51,6 @@ static void putIfNotNull(final Map map, final String key, final } } - /* JSON - Map conversion helpers */ - // TODO: add tests / replace with third party library for json conversion; test more around jsonObject/jsonArray with null nodes - // TODO: check what should be the expected behavior with the konductor team (e.g. don't add the null nodes or add them with null values) - - /** - * Converts provided {@link JSONObject} into {@link Map} for any number of levels, which can be used as event data - * This method is recursive. - * The elements for which the conversion fails will be skipped. - * - * @param jsonObject to be converted - * @return {@link Map} containing the elements from the provided json, null if {@code jsonObject} is null - */ - static Map toMap(final JSONObject jsonObject) { - if (jsonObject == null) { - return null; - } - - Map jsonAsMap = new HashMap<>(); - Iterator keysIterator = jsonObject.keys(); - - while (keysIterator.hasNext()) { - String nextKey = keysIterator.next(); - Object value = null; - Object returnValue; - - try { - value = jsonObject.get(nextKey); - } catch (JSONException e) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Utils(toMap) - Unable to convert jsonObject to Map for key " + nextKey + ", skipping." - ); - } - - if (value == null) { - continue; - } - - if (value instanceof JSONObject) { - returnValue = toMap((JSONObject) value); - } else if (value instanceof JSONArray) { - returnValue = toList((JSONArray) value); - } else { - returnValue = value; - } - - jsonAsMap.put(nextKey, returnValue); - } - - return jsonAsMap; - } - - /** - * Converts provided {@link JSONArray} into {@link List} for any number of levels which can be used as event data - * This method is recursive. - * The elements for which the conversion fails will be skipped. - * - * @param jsonArray to be converted - * @return {@link List} containing the elements from the provided json, null if {@code jsonArray} is null - */ - static List toList(final JSONArray jsonArray) { - if (jsonArray == null) { - return null; - } - - List jsonArrayAsList = new ArrayList<>(); - int size = jsonArray.length(); - - for (int i = 0; i < size; i++) { - Object value = null; - Object returnValue = null; - - try { - value = jsonArray.get(i); - } catch (JSONException e) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Utils(toList) - Unable to convert jsonObject to List for index " + i + ", skipping." - ); - } - - if (value == null) { - continue; - } - - if (value instanceof JSONObject) { - returnValue = toMap((JSONObject) value); - } else if (value instanceof JSONArray) { - returnValue = toList((JSONArray) value); - } else { - returnValue = value; - } - - jsonArrayAsList.add(returnValue); - } - - return jsonArrayAsList; - } - /** * Creates a deep copy of the provided {@link Map}. * @@ -172,8 +63,12 @@ static Map deepCopy(final Map map) { } try { - return Utils.toMap(new JSONObject(map)); - } catch (NullPointerException e) { + // Core's JSONUtils retains null in resulting Map but, EdgeIdentity 1.0 implementaion + // filtered out the null value keys. One issue this may cause is sending empty objects to + // Edge Network. + // TODO: Add/verify tests to check side effects of retaining nulls in the resulting Map + return JSONUtils.toMap(new JSONObject(map)); + } catch (final JSONException | NullPointerException e) { MobileCore.log( LoggingMode.DEBUG, LOG_TAG, @@ -204,13 +99,4 @@ static List> deepCopy(final List> listOf return deepCopy; } - - /** - * Gets current unix timestamp in seconds. - * - * @return {code long} current timestamp - */ - static long getUnixTimeInSeconds() { - return System.currentTimeMillis() / MILLISECONDS_PER_SECOND; - } } diff --git a/code/gradle.properties b/code/gradle.properties index 8f211c79..363b6f82 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -9,12 +9,12 @@ android.disableAutomaticComponentCreation=true moduleProjectName=edgeidentity moduleName=edgeidentity moduleAARName=edgeidentity-phone-release.aar -moduleVersion=1.1.0 +moduleVersion=2.0.0 mavenRepoName=AdobeMobileEdgeIdentitySdk mavenRepoDescription=Adobe Experience Platform Edge Identity extension for the Adobe Experience Platform Mobile SDK mavenUploadDryRunFlag=false # production versions for production build -mavenCoreVersion=1.8.0 +mavenCoreVersion=2.0.0 diff --git a/code/settings.gradle b/code/settings.gradle index bb5f0304..9782bf67 100644 --- a/code/settings.gradle +++ b/code/settings.gradle @@ -12,6 +12,7 @@ dependencyResolutionManagement { google() mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } + maven { url 'https://jitpack.io' } } } From 74bd68313c66c36f92c0c316db4d995635da7102 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Fri, 18 Nov 2022 09:52:56 -0800 Subject: [PATCH 03/50] Switch logging to Log.* in non test classes --- .../marketing/mobile/MonitorExtension.java | 4 +- .../marketing/mobile/edge/identity/ECID.java | 9 ++- .../mobile/edge/identity/Identity.java | 73 ++++++++----------- .../edge/identity/IdentityExtension.java | 21 ++---- .../mobile/edge/identity/IdentityItem.java | 7 +- .../mobile/edge/identity/IdentityMap.java | 29 ++------ .../edge/identity/IdentityProperties.java | 32 ++++---- .../mobile/edge/identity/IdentityState.java | 46 ++++++------ .../edge/identity/IdentityStorageService.java | 58 ++++++--------- .../mobile/edge/identity/URLUtils.java | 9 ++- .../marketing/mobile/edge/identity/Utils.java | 13 ++-- 11 files changed, 126 insertions(+), 175 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java index 4b7478ee..580178ba 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java @@ -140,7 +140,7 @@ public void wildcardProcessor(final Event event) { EventSpec eventSpec = new EventSpec(event.getSource(), event.getType()); - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Received and processing event " + eventSpec); + Log.debug(LOG_TAG, TAG, "Received and processing event " + eventSpec); if (!receivedEvents.containsKey(eventSpec)) { receivedEvents.put(eventSpec, new ArrayList()); @@ -158,7 +158,7 @@ public void wildcardProcessor(final Event event) { * @param event */ private void processUnregisterRequest(final Event event) { - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Unregistering the Monitor Extension."); + Log.debug(LOG_TAG, TAG, "Unregistering the Monitor Extension."); getApi().unregisterExtension(); } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java index 401a5376..7dfa4aa7 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java @@ -11,8 +11,7 @@ package com.adobe.marketing.mobile.edge.identity; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.StringUtils; import java.util.Locale; import java.util.Objects; @@ -23,6 +22,8 @@ */ final class ECID { + private static final String LOG_SOURCE = "ECID"; + private final String ecidString; /** @@ -43,9 +44,9 @@ final class ECID { */ ECID(final String ecidString) { if (StringUtils.isNullOrEmpty(ecidString)) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( IdentityConstants.LOG_TAG, + LOG_SOURCE, "Creating an ECID with null or empty ecidString is not allowed, generating a new ECID." ); this.ecidString = new ECID().toString(); 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 63967f02..d22a4d3b 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 @@ -20,8 +20,8 @@ import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionError; import com.adobe.marketing.mobile.ExtensionErrorCallback; -import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; import com.adobe.marketing.mobile.util.StringUtils; import java.util.HashMap; @@ -36,6 +36,8 @@ public class Identity { public static final Class EXTENSION = IdentityExtension.class; private static final long CALLBACK_TIMEOUT_MILLIS = 500L; + private static final String LOG_SOURCE = "Identity"; + private Identity() {} /** @@ -59,11 +61,10 @@ public static void registerExtension() { new ExtensionErrorCallback() { @Override public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.ERROR, + Log.error( LOG_TAG, - "Identity - There was an error registering the Edge Identity extension: " + - extensionError.getErrorName() + LOG_SOURCE, + "There was an error registering the Edge Identity extension: " + extensionError.getErrorName() ); } } @@ -79,11 +80,7 @@ public void error(ExtensionError extensionError) { */ public static void getExperienceCloudId(final AdobeCallback callback) { if (callback == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Identity - Unexpected null callback, provide a callback to retrieve current ECID." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Unexpected null callback, provide a callback to retrieve current ECID."); return; } @@ -105,10 +102,10 @@ public void call(Event responseEvent) { final IdentityMap identityMap = IdentityMap.fromXDMMap(responseEvent.getEventData()); if (identityMap == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "Identity - Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" + LOG_SOURCE, + "Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" ); returnError(callback, AdobeError.UNEXPECTED_ERROR); return; @@ -128,11 +125,11 @@ public void call(Event responseEvent) { @Override public void fail(AdobeError adobeError) { returnError(callback, adobeError); - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, + LOG_SOURCE, String.format( - "Identity - Failed to dispatch %s event: Error : %s.", + "Failed to dispatch %s event: Error : %s.", IdentityConstants.EventNames.IDENTITY_REQUEST_IDENTITY_ECID, adobeError.getErrorName() ) @@ -162,10 +159,10 @@ public void fail(AdobeError adobeError) { */ public static void getUrlVariables(final AdobeCallback callback) { if (callback == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "Identity - Unexpected null callback, provide a callback to retrieve current visitor identifiers (URLVariables) query string." + LOG_SOURCE, + "Unexpected null callback, provide a callback to retrieve current visitor identifiers (URLVariables) query string." ); return; } @@ -209,11 +206,11 @@ public void call(final Event responseEvent) { @Override public void fail(final AdobeError adobeError) { returnError(callback, adobeError); - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, + LOG_SOURCE, String.format( - "Identity - Failed to dispatch %s event: Error : %s.", + "Failed to dispatch %s event: Error : %s.", IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, adobeError.getErrorName() ) @@ -234,11 +231,7 @@ public void fail(final AdobeError adobeError) { */ public static void updateIdentities(final IdentityMap identityMap) { if (identityMap == null || identityMap.isEmpty()) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Identity - Unable to updateIdentities, IdentityMap is null or empty" - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Unable to updateIdentities, IdentityMap is null or empty"); return; } @@ -262,16 +255,12 @@ public static void updateIdentities(final IdentityMap identityMap) { */ public static void removeIdentity(final IdentityItem item, final String namespace) { if (StringUtils.isNullOrEmpty(namespace)) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Identity - Unable to removeIdentity, namespace is null or empty" - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Unable to removeIdentity, namespace is null or empty"); return; } if (item == null) { - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Identity - Unable to removeIdentity, IdentityItem is null"); + Log.debug(LOG_TAG, LOG_SOURCE, "Unable to removeIdentity, IdentityItem is null"); return; } @@ -297,10 +286,10 @@ public static void removeIdentity(final IdentityItem item, final String namespac */ public static void getIdentities(final AdobeCallback callback) { if (callback == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "Identity - Unexpected null callback, provide a callback to retrieve current IdentityMap." + LOG_SOURCE, + "Unexpected null callback, provide a callback to retrieve current IdentityMap." ); return; } @@ -323,10 +312,10 @@ public void call(final Event responseEvent) { final IdentityMap identityMap = IdentityMap.fromXDMMap(responseEvent.getEventData()); if (identityMap == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "Identity - Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" + LOG_SOURCE, + "Failed to read IdentityMap from response event, invoking error callback with AdobeError.UNEXPECTED_ERROR" ); returnError(callback, AdobeError.UNEXPECTED_ERROR); return; @@ -338,11 +327,11 @@ public void call(final Event responseEvent) { @Override public void fail(final AdobeError adobeError) { returnError(callback, adobeError); - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, + LOG_SOURCE, String.format( - "Identity - Failed to dispatch %s event: Error : %s.", + "Failed to dispatch %s event: Error : %s.", IdentityConstants.EventNames.REQUEST_IDENTITIES, adobeError.getErrorName() ) 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 385dc9c7..ce63520b 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 @@ -18,11 +18,10 @@ import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.SharedStateResolution; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.StringUtils; import com.adobe.marketing.mobile.util.TimeUtils; import java.util.HashMap; @@ -190,7 +189,7 @@ private void handleUrlVariablesRequest(@NonNull final Event event) { handleUrlVariableResponse( event, null, - "IdentityExtension - Cannot process getUrlVariables request Identity event, Experience Cloud Org ID not found in configuration." + "Cannot process getUrlVariables request Identity event, Experience Cloud Org ID not found in configuration." ); return; } @@ -202,7 +201,7 @@ private void handleUrlVariablesRequest(@NonNull final Event event) { handleUrlVariableResponse( event, null, - "IdentityExtension - Cannot process getUrlVariables request Identity event, ECID not found." + "Cannot process getUrlVariables request Identity event, ECID not found." ); return; } @@ -250,7 +249,7 @@ void handleUrlVariableResponse(@NonNull final Event event, final String urlVaria .build(); if (StringUtils.isNullOrEmpty(urlVariables) && !StringUtils.isNullOrEmpty(errorMsg)) { - MobileCore.log(LoggingMode.WARNING, LOG_TAG, errorMsg); + Log.warning(LOG_TAG, LOG_TAG, errorMsg); } getApi().dispatch(responseEvent); @@ -269,11 +268,7 @@ void handleUpdateIdentities(@NonNull final Event event) { final IdentityMap map = IdentityMap.fromXDMMap(eventData); if (map == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityExtension - Failed to update identifiers as no identifiers were found in the event data." - ); + Log.debug(LOG_TAG, LOG_TAG, "Failed to update identifiers as no identifiers were found in the event data."); return; } @@ -294,11 +289,7 @@ void handleRemoveIdentity(@NonNull final Event event) { final IdentityMap map = IdentityMap.fromXDMMap(eventData); if (map == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityExtension - Failed to remove identifiers as no identifiers were found in the event data." - ); + Log.debug(LOG_TAG, LOG_TAG, "Failed to remove identifiers as no identifiers were found in the event data."); return; } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java index 0a400b0b..70efedb0 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityItem.java @@ -13,8 +13,7 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; import com.adobe.marketing.mobile.util.DataReaderException; import java.util.HashMap; @@ -29,6 +28,8 @@ */ public final class IdentityItem { + private static final String LOG_SOURCE = "IdentityItem"; + private final String id; private final AuthenticatedState authenticatedState; private final boolean primary; @@ -182,7 +183,7 @@ static IdentityItem fromData(final Map data) { return new IdentityItem(id, authenticatedState, primary); } catch (final DataReaderException e) { - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "IdentityItem - Failed to create IdentityItem from data."); + Log.debug(LOG_TAG, LOG_SOURCE, "Failed to create IdentityItem from data."); return null; } } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java index c82f3469..4db3ee9b 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java @@ -13,8 +13,7 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; import com.adobe.marketing.mobile.util.StringUtils; import java.util.ArrayList; @@ -33,6 +32,8 @@ @SuppressWarnings("unused") public class IdentityMap { + private static final String LOG_SOURCE = "IdentityMap"; + private final Map> identityItems = new HashMap<>(); /** @@ -90,20 +91,12 @@ public void addItem(final IdentityItem item, final String namespace) { */ public void removeItem(final IdentityItem item, final String namespace) { if (item == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityMap remove item ignored as must contain a non-null IdentityItem." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Remove item ignored as must contain a non-null IdentityItem."); return; } if (StringUtils.isNullOrEmpty(namespace)) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityMap remove item ignored as must contain a non-null/non-empty namespace." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Remove item ignored as must contain a non-null/non-empty namespace."); return; } @@ -161,20 +154,12 @@ public String toString() { */ void addItem(final IdentityItem item, final String namespace, final boolean isFirstItem) { if (item == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityMap - add item ignored as must contain a non-null IdentityItem." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Add item ignored as must contain a non-null IdentityItem."); return; } if (StringUtils.isNullOrEmpty(namespace)) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityMap - add item ignored as must contain a non-null/non-empty namespace." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Add item ignored as must contain a non-null/non-empty namespace."); return; } 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 110b313c..36c79dec 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 @@ -13,10 +13,9 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.StringUtils; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -25,13 +24,12 @@ */ class IdentityProperties { - private static final List reservedNamespaces = new ArrayList() { - { - add(IdentityConstants.Namespaces.ECID); - add(IdentityConstants.Namespaces.GAID); - add(IdentityConstants.Namespaces.IDFA); - } - }; + private static final String LOG_SOURCE = "IdentityProperties"; + private static final List reservedNamespaces = Arrays.asList( + IdentityConstants.Namespaces.ECID, + IdentityConstants.Namespaces.GAID, + IdentityConstants.Namespaces.IDFA + ); private final IdentityMap identityMap; @@ -155,7 +153,7 @@ void setECIDSecondary(final ECID newSecondaryEcid) { // do not set secondary ECID if primary ECID is not set if (getECID() == null) { - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Cannot set secondary ECID value as no primary ECID exists."); + Log.debug(LOG_TAG, LOG_SOURCE, "Cannot set secondary ECID value as no primary ECID exists."); return; } @@ -248,20 +246,20 @@ private void removeIdentitiesWithReservedNamespaces(final IdentityMap identityMa reservedNamespace.equalsIgnoreCase(IdentityConstants.Namespaces.GAID) || reservedNamespace.equalsIgnoreCase(IdentityConstants.Namespaces.IDFA) ) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, + LOG_SOURCE, String.format( - "IdentityProperties - Operation not allowed for namespace %s; use MobileCore.setAdvertisingIdentifier instead.", + "Operation not allowed for namespace %s; use MobileCore.setAdvertisingIdentifier instead.", reservedNamespace ) ); } else { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, + LOG_SOURCE, String.format( - "IdentityProperties - Updating/Removing identifiers in namespace %s is not allowed.", + "Updating/Removing identifiers in namespace %s is not allowed.", reservedNamespace ) ); diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index 40a6f291..82748ebb 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -15,10 +15,10 @@ import androidx.annotation.VisibleForTesting; import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; import java.util.HashMap; import java.util.Map; @@ -28,6 +28,8 @@ */ class IdentityState { + private static final String LOG_SOURCE = "IdentityState"; + private IdentityProperties identityProperties; private boolean hasBooted; @@ -97,12 +99,10 @@ boolean bootupIfReady(final SharedStateCallback callback) { if (directIdentityEcid != null) { identityProperties.setECID(directIdentityEcid); - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityState - On bootup Loading ECID from direct Identity extension '" + - directIdentityEcid + - "'" + LOG_SOURCE, + "On bootup Loading ECID from direct Identity extension '" + directIdentityEcid + "'" ); } // If direct Identity has no persisted ECID, check if direct Identity is registered with the SDK @@ -115,10 +115,10 @@ else if (isIdentityDirectRegistered(eventHubStateResult.getValue())) { // If there is no direct Identity shared state, abort boot-up and try again when direct Identity shares its state if (sharedStateResult == null || sharedStateResult.getStatus() != SharedStateStatus.SET) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityState - On bootup direct Identity extension is registered, waiting for its state change." + LOG_SOURCE, + "On bootup direct Identity extension is registered, waiting for its state change." ); return false; } @@ -129,10 +129,10 @@ else if (isIdentityDirectRegistered(eventHubStateResult.getValue())) { // Generate a new ECID as the direct Identity extension is not registered with the SDK and there was no direct Identity persisted ECID else { identityProperties.setECID(new ECID()); - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityState - Generating new ECID on bootup '" + identityProperties.getECID().toString() + "'" + LOG_SOURCE, + "Generating new ECID on bootup '" + identityProperties.getECID().toString() + "'" ); } @@ -140,7 +140,7 @@ else if (isIdentityDirectRegistered(eventHubStateResult.getValue())) { } hasBooted = true; - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "IdentityState - Edge Identity has successfully booted up"); + Log.debug(LOG_TAG, LOG_SOURCE, "Edge Identity has successfully booted up"); callback.createXDMSharedState(identityProperties.toXDMData(false), null); return hasBooted; @@ -238,10 +238,10 @@ boolean updateLegacyExperienceCloudId(final ECID legacyEcid) { identityProperties.setECIDSecondary(legacyEcid); IdentityStorageService.savePropertiesToPersistence(identityProperties); - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityState - Identity direct ECID updated to '" + legacyEcid + "', updating the IdentityMap" + LOG_SOURCE, + "Identity direct ECID updated to '" + legacyEcid + "', updating the IdentityMap" ); return true; } @@ -256,19 +256,17 @@ boolean updateLegacyExperienceCloudId(final ECID legacyEcid) { private void handleECIDFromIdentityDirect(final ECID legacyEcid) { if (legacyEcid != null) { identityProperties.setECID(legacyEcid); // migrate legacy ECID - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityState - Identity direct ECID '" + - legacyEcid + - "' was migrated to Edge Identity, updating the IdentityMap" + LOG_SOURCE, + "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.debug( LOG_TAG, - "IdentityState - Identity direct ECID is null, generating new ECID '" + + LOG_SOURCE, + "Identity direct ECID is null, generating new ECID '" + identityProperties.getECID() + "', updating the IdentityMap" ); diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java index fb6d2f51..73fcd770 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java @@ -16,8 +16,8 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; -import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.JSONUtils; import java.util.Map; import org.json.JSONException; @@ -28,6 +28,8 @@ */ class IdentityStorageService { + private static final String LOG_SOURCE = "IdentityStorageService"; + private IdentityStorageService() {} /** @@ -39,10 +41,10 @@ static IdentityProperties loadPropertiesFromPersistence() { final SharedPreferences sharedPreferences = getSharedPreference(IdentityConstants.DataStoreKey.DATASTORE_NAME); if (sharedPreferences == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityStorageService - Shared Preference value is null. Unable to load saved identity properties from persistence." + LOG_SOURCE, + "Shared Preference value is null. Unable to load saved identity properties from persistence." ); return null; } @@ -50,10 +52,10 @@ static IdentityProperties loadPropertiesFromPersistence() { final String jsonString = sharedPreferences.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null); if (jsonString == null) { - MobileCore.log( - LoggingMode.VERBOSE, + Log.debug( LOG_TAG, - "IdentityStorageService - No previous properties were stored in persistence. Current identity properties are null" + LOG_SOURCE, + "No previous properties were stored in persistence. Current identity properties are null" ); return null; } @@ -63,10 +65,10 @@ static IdentityProperties loadPropertiesFromPersistence() { final Map propertyMap = JSONUtils.toMap(jsonObject); return new IdentityProperties(propertyMap); } catch (JSONException exception) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityStorageService - Serialization error while reading properties jsonString from persistence. Unable to load saved identity properties from persistence." + LOG_SOURCE, + "Serialization error while reading properties jsonString from persistence. Unable to load saved identity properties from persistence." ); return null; } @@ -81,10 +83,10 @@ static void savePropertiesToPersistence(final IdentityProperties properties) { final SharedPreferences sharedPreferences = getSharedPreference(IdentityConstants.DataStoreKey.DATASTORE_NAME); if (sharedPreferences == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityStorageService - Shared Preference value is null. Unable to write identity properties to persistence." + LOG_SOURCE, + "Shared Preference value is null. Unable to write identity properties to persistence." ); return; } @@ -92,20 +94,16 @@ static void savePropertiesToPersistence(final IdentityProperties properties) { final SharedPreferences.Editor editor = sharedPreferences.edit(); if (editor == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityStorageService - Shared Preference Editor is null. Unable to write identity properties to persistence." + LOG_SOURCE, + "Shared Preference Editor is null. Unable to write identity properties to persistence." ); return; } if (properties == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityStorageService - Identity Properties are null, removing them from persistence." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Identity Properties are null, removing them from persistence."); editor.remove(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES); editor.apply(); return; @@ -128,10 +126,10 @@ static ECID loadEcidFromDirectIdentityPersistence() { ); if (sharedPreferences == null) { - MobileCore.log( - LoggingMode.DEBUG, + Log.debug( LOG_TAG, - "IdentityStorageService - Shared Preference value is null. Unable to load saved direct identity ECID from persistence." + LOG_SOURCE, + "Shared Preference value is null. Unable to load saved direct identity ECID from persistence." ); return null; } @@ -160,22 +158,14 @@ private static SharedPreferences getSharedPreference(final String datastoreName) final Application application = MobileCore.getApplication(); if (application == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityStorageService - Application value is null. Unable to read/write data from persistence." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Application value is null. Unable to read/write data from persistence."); return null; } final Context context = application.getApplicationContext(); if (context == null) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "IdentityStorageService - Context value is null. Unable to read/write data from persistence." - ); + Log.debug(LOG_TAG, LOG_SOURCE, "Context value is null. Unable to read/write data from persistence."); return null; } 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 index 8b5e81e9..2970d61c 100644 --- 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 @@ -11,8 +11,9 @@ package com.adobe.marketing.mobile.edge.identity; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; +import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; + +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.StringUtils; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @@ -20,7 +21,7 @@ class URLUtils { - static final String LOG_TAG = "URLUtils"; + private static final String LOG_SOURCE = "URLUtils"; /** * Helper function to generate url variables in format acceptable by the AEP web SDKs @@ -56,7 +57,7 @@ static String generateURLVariablesPayload(final String ts, final String ecid, fi urlFragment.append(URLEncoder.encode(theIdString, StandardCharsets.UTF_8.toString())); } } catch (UnsupportedEncodingException e) { - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, String.format("Failed to encode urlVariable string: %s", e)); + Log.debug(LOG_TAG, LOG_SOURCE, String.format("Failed to encode urlVariable string: %s", e)); } return urlFragment.toString(); } 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 2ad211ea..3e339df0 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 @@ -13,8 +13,7 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; -import com.adobe.marketing.mobile.LoggingMode; -import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.JSONUtils; import java.util.ArrayList; import java.util.List; @@ -24,6 +23,8 @@ class Utils { + private static final String LOG_SOURCE = "Utils"; + private Utils() {} static boolean isNullOrEmpty(final Map map) { @@ -68,12 +69,8 @@ static Map deepCopy(final Map map) { // Edge Network. // TODO: Add/verify tests to check side effects of retaining nulls in the resulting Map return JSONUtils.toMap(new JSONObject(map)); - } catch (final JSONException | NullPointerException e) { - MobileCore.log( - LoggingMode.DEBUG, - LOG_TAG, - "Utils(deepCopy) - Unable to deep copy map, json string invalid." - ); + } catch (final JSONException e) { + Log.debug(LOG_TAG, LOG_SOURCE, "Unable to deep copy map, json string invalid."); } return null; From db4eae3bb12a3e43d50873e64276e404eac827e7 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 21 Nov 2022 14:21:49 -0800 Subject: [PATCH 04/50] Use ServiceProvider.NamedCollection for local storage management - Make IdentityStorageService methods non-static and inject from constuctor - Rename IdentityStorageService to IdentityStorageManager --- .../edge/identity/IdentityExtension.java | 2 +- .../mobile/edge/identity/IdentityState.java | 30 +++--- ...rvice.java => IdentityStorageManager.java} | 97 ++++++------------- 3 files changed, 43 insertions(+), 86 deletions(-) rename code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/{IdentityStorageService.java => IdentityStorageManager.java} (52%) 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 ce63520b..19a0194a 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 @@ -54,7 +54,7 @@ public void createXDMSharedState(final Map state, final Event ev * @param extensionApi {@link ExtensionApi} instance */ protected IdentityExtension(ExtensionApi extensionApi) { - this(extensionApi, new IdentityState()); + this(extensionApi, new IdentityState(new IdentityStorageManager())); } @VisibleForTesting diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index 82748ebb..b834e572 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -30,24 +30,18 @@ class IdentityState { private static final String LOG_SOURCE = "IdentityState"; + private final IdentityStorageManager identityStorageManager; private IdentityProperties identityProperties; private boolean hasBooted; /** * Loads the persisted identities (if any) into {@link #identityProperties} */ - IdentityState() { - this(IdentityStorageService.loadPropertiesFromPersistence()); - } + IdentityState(final IdentityStorageManager identityStorageManager) { + this.identityStorageManager = identityStorageManager; - /** - * Creates a new {@link IdentityState} with the given {@link IdentityProperties} - * - * @param identityProperties identity properties - */ - @VisibleForTesting - IdentityState(final IdentityProperties identityProperties) { - this.identityProperties = (identityProperties != null) ? identityProperties : new IdentityProperties(); + final IdentityProperties persistedProperties = identityStorageManager.loadPropertiesFromPersistence(); + this.identityProperties = (persistedProperties != null) ? persistedProperties : new IdentityProperties(); } /** @@ -95,7 +89,7 @@ boolean bootupIfReady(final SharedStateCallback callback) { } // Attempt to get ECID from direct Identity persistence to migrate an existing ECID - final ECID directIdentityEcid = IdentityStorageService.loadEcidFromDirectIdentityPersistence(); + final ECID directIdentityEcid = identityStorageManager.loadEcidFromDirectIdentityPersistence(); if (directIdentityEcid != null) { identityProperties.setECID(directIdentityEcid); @@ -136,7 +130,7 @@ else if (isIdentityDirectRegistered(eventHubStateResult.getValue())) { ); } - IdentityStorageService.savePropertiesToPersistence(identityProperties); + identityStorageManager.savePropertiesToPersistence(identityProperties); } hasBooted = true; @@ -153,7 +147,7 @@ void resetIdentifiers() { identityProperties = new IdentityProperties(); identityProperties.setECID(new ECID()); identityProperties.setECIDSecondary(null); - IdentityStorageService.savePropertiesToPersistence(identityProperties); + identityStorageManager.savePropertiesToPersistence(identityProperties); } /** @@ -163,7 +157,7 @@ void resetIdentifiers() { */ void updateCustomerIdentifiers(final IdentityMap map) { identityProperties.updateCustomerIdentifiers(map); - IdentityStorageService.savePropertiesToPersistence(identityProperties); + identityStorageManager.savePropertiesToPersistence(identityProperties); } /** @@ -173,7 +167,7 @@ void updateCustomerIdentifiers(final IdentityMap map) { */ void removeCustomerIdentifiers(final IdentityMap map) { identityProperties.removeCustomerIdentifiers(map); - IdentityStorageService.savePropertiesToPersistence(identityProperties); + identityStorageManager.savePropertiesToPersistence(identityProperties); } /** @@ -212,7 +206,7 @@ void updateAdvertisingIdentifier(final Event event, final SharedStateCallback ca } // Save to persistence - IdentityStorageService.savePropertiesToPersistence(identityProperties); + identityStorageManager.savePropertiesToPersistence(identityProperties); callback.createXDMSharedState(identityProperties.toXDMData(false), event); } @@ -237,7 +231,7 @@ boolean updateLegacyExperienceCloudId(final ECID legacyEcid) { } identityProperties.setECIDSecondary(legacyEcid); - IdentityStorageService.savePropertiesToPersistence(identityProperties); + identityStorageManager.savePropertiesToPersistence(identityProperties); Log.debug( LOG_TAG, LOG_SOURCE, diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java similarity index 52% rename from code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java rename to code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java index 73fcd770..c61469db 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageService.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java @@ -13,11 +13,11 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; -import com.adobe.marketing.mobile.MobileCore; +import androidx.annotation.VisibleForTesting; +import com.adobe.marketing.mobile.services.DataStoring; import com.adobe.marketing.mobile.services.Log; +import com.adobe.marketing.mobile.services.NamedCollection; +import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.JSONUtils; import java.util.Map; import org.json.JSONException; @@ -26,30 +26,38 @@ /** * Manages persistence for this Identity extension */ -class IdentityStorageService { +class IdentityStorageManager { private static final String LOG_SOURCE = "IdentityStorageService"; + private final NamedCollection edgeIdentityStore; + private final NamedCollection directIdentityStore; - private IdentityStorageService() {} + IdentityStorageManager() { + this(ServiceProvider.getInstance().getDataStoreService()); + } + + @VisibleForTesting + IdentityStorageManager(final DataStoring dataStoreService) { + this.edgeIdentityStore = dataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.DATASTORE_NAME); + this.directIdentityStore = + dataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME); + } /** * Loads identity properties from local storage, returns null if not found. * * @return properties stored in local storage if present, otherwise null. */ - static IdentityProperties loadPropertiesFromPersistence() { - final SharedPreferences sharedPreferences = getSharedPreference(IdentityConstants.DataStoreKey.DATASTORE_NAME); - - if (sharedPreferences == null) { + IdentityProperties loadPropertiesFromPersistence() { + if (edgeIdentityStore == null) { Log.debug( LOG_TAG, LOG_SOURCE, - "Shared Preference value is null. Unable to load saved identity properties from persistence." + "EdgeIdentity named collection is null. Unable to load saved identity properties from persistence." ); return null; } - - final String jsonString = sharedPreferences.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null); + final String jsonString = edgeIdentityStore.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null); if (jsonString == null) { Log.debug( @@ -79,40 +87,25 @@ static IdentityProperties loadPropertiesFromPersistence() { * * @param properties properties to be stored */ - static void savePropertiesToPersistence(final IdentityProperties properties) { - final SharedPreferences sharedPreferences = getSharedPreference(IdentityConstants.DataStoreKey.DATASTORE_NAME); - - if (sharedPreferences == null) { + void savePropertiesToPersistence(final IdentityProperties properties) { + if (edgeIdentityStore == null) { Log.debug( LOG_TAG, LOG_SOURCE, - "Shared Preference value is null. Unable to write identity properties to persistence." - ); - return; - } - - final SharedPreferences.Editor editor = sharedPreferences.edit(); - - if (editor == null) { - Log.debug( - LOG_TAG, - LOG_SOURCE, - "Shared Preference Editor is null. Unable to write identity properties to persistence." + "EdgeIdentity named collection is null. Unable to write identity properties to persistence." ); return; } if (properties == null) { Log.debug(LOG_TAG, LOG_SOURCE, "Identity Properties are null, removing them from persistence."); - editor.remove(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES); - editor.apply(); + edgeIdentityStore.remove(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES); return; } final JSONObject jsonObject = new JSONObject(properties.toXDMData(false)); final String jsonString = jsonObject.toString(); - editor.putString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, jsonString); - editor.apply(); + edgeIdentityStore.setString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, jsonString); } /** @@ -120,21 +113,17 @@ static void savePropertiesToPersistence(final IdentityProperties properties) { * * @return {@link ECID} stored in direct Identity extension's persistence, or null if no ECID value is stored. */ - static ECID loadEcidFromDirectIdentityPersistence() { - final SharedPreferences sharedPreferences = getSharedPreference( - IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME - ); - - if (sharedPreferences == null) { + ECID loadEcidFromDirectIdentityPersistence() { + if (directIdentityStore == null) { Log.debug( LOG_TAG, LOG_SOURCE, - "Shared Preference value is null. Unable to load saved direct identity ECID from persistence." + "Identity direct named collection is null. Unable to load ECID from Identity Direct persistence." ); return null; } - final String ecidString = sharedPreferences.getString( + final String ecidString = directIdentityStore.getString( IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null ); @@ -145,30 +134,4 @@ static ECID loadEcidFromDirectIdentityPersistence() { return new ECID(ecidString); } - - /** - * Getter for the applications {@link SharedPreferences} - *

- * Returns null if the app or app context is not available - * - * @param datastoreName the name of the data store to get - * @return a {@code SharedPreferences} instance - */ - private static SharedPreferences getSharedPreference(final String datastoreName) { - final Application application = MobileCore.getApplication(); - - if (application == null) { - Log.debug(LOG_TAG, LOG_SOURCE, "Application value is null. Unable to read/write data from persistence."); - return null; - } - - final Context context = application.getApplicationContext(); - - if (context == null) { - Log.debug(LOG_TAG, LOG_SOURCE, "Context value is null. Unable to read/write data from persistence."); - return null; - } - - return context.getSharedPreferences(datastoreName, Context.MODE_PRIVATE); - } } From 4394ca9d4df0535f8fcaf8b241531bd8e51ae3fc Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 29 Nov 2022 11:18:54 -0800 Subject: [PATCH 05/50] Fix doc, undo non-test and overlapped changes --- .../java/com/adobe/marketing/mobile/MonitorExtension.java | 4 ++-- .../mobile/edge/identity/IdentityStorageManager.java | 2 +- .../java/com/adobe/marketing/mobile/edge/identity/Utils.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java index 580178ba..4b7478ee 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java @@ -140,7 +140,7 @@ public void wildcardProcessor(final Event event) { EventSpec eventSpec = new EventSpec(event.getSource(), event.getType()); - Log.debug(LOG_TAG, TAG, "Received and processing event " + eventSpec); + MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Received and processing event " + eventSpec); if (!receivedEvents.containsKey(eventSpec)) { receivedEvents.put(eventSpec, new ArrayList()); @@ -158,7 +158,7 @@ public void wildcardProcessor(final Event event) { * @param event */ private void processUnregisterRequest(final Event event) { - Log.debug(LOG_TAG, TAG, "Unregistering the Monitor Extension."); + MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Unregistering the Monitor Extension."); getApi().unregisterExtension(); } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java index c61469db..b8f95b3d 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java @@ -83,7 +83,7 @@ IdentityProperties loadPropertiesFromPersistence() { } /** - * Saves the properties to local storage + * Saves identity properties to local storage. * * @param properties properties to be stored */ 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 3e339df0..9d93020d 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 @@ -69,8 +69,8 @@ static Map deepCopy(final Map map) { // Edge Network. // TODO: Add/verify tests to check side effects of retaining nulls in the resulting Map return JSONUtils.toMap(new JSONObject(map)); - } catch (final JSONException e) { - Log.debug(LOG_TAG, LOG_SOURCE, "Unable to deep copy map, json string invalid."); + } catch (final JSONException | NullPointerException e) { + Log.debug(LOG_TAG, LOG_SOURCE, "Unable to deep copy map, json string is invalid."); } return null; From f2e680b3b3e868076e4a8403764facc7fe5e6047 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 29 Nov 2022 12:04:43 -0800 Subject: [PATCH 06/50] Simplify return statement --- .../mobile/edge/identity/IdentityStorageManager.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java index b8f95b3d..8d72aae1 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java @@ -19,6 +19,7 @@ import com.adobe.marketing.mobile.services.NamedCollection; import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.JSONUtils; +import com.adobe.marketing.mobile.util.StringUtils; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; @@ -128,10 +129,6 @@ ECID loadEcidFromDirectIdentityPersistence() { null ); - if (ecidString == null || ecidString.isEmpty()) { - return null; - } - - return new ECID(ecidString); + return StringUtils.isNullOrEmpty(ecidString) ? null : new ECID(ecidString); } } From ca3b3b0dc1b890c293202f54be835d424f5422f5 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Fri, 2 Dec 2022 05:53:12 -0800 Subject: [PATCH 07/50] Fix log levels and log tags. Inject DataStoreService into IdentityState --- .../marketing/mobile/edge/identity/ECID.java | 4 ++- .../edge/identity/IdentityExtension.java | 29 +++++++++++++++---- .../mobile/edge/identity/IdentityState.java | 6 ++++ .../edge/identity/IdentityStorageManager.java | 19 +++++------- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java index 7dfa4aa7..3c35b1f0 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/ECID.java @@ -11,6 +11,8 @@ package com.adobe.marketing.mobile.edge.identity; +import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; + import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.StringUtils; import java.util.Locale; @@ -45,7 +47,7 @@ final class ECID { ECID(final String ecidString) { if (StringUtils.isNullOrEmpty(ecidString)) { Log.debug( - IdentityConstants.LOG_TAG, + LOG_TAG, LOG_SOURCE, "Creating an ECID with null or empty ecidString is not allowed, generating a new ECID." ); 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 19a0194a..24e9b7c8 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 @@ -22,6 +22,7 @@ import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; import com.adobe.marketing.mobile.services.Log; +import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.StringUtils; import com.adobe.marketing.mobile.util.TimeUtils; import java.util.HashMap; @@ -29,6 +30,8 @@ class IdentityExtension extends Extension { + private static final String LOG_SOURCE = "IdentityExtension"; + /** * A {@code SharedStateCallback} to retrieve the last set state of an extension and to * create an XDM state at the event provided. @@ -54,7 +57,7 @@ public void createXDMSharedState(final Map state, final Event ev * @param extensionApi {@link ExtensionApi} instance */ protected IdentityExtension(ExtensionApi extensionApi) { - this(extensionApi, new IdentityState(new IdentityStorageManager())); + this(extensionApi, new IdentityState(ServiceProvider.getInstance().getDataStoreService())); } @VisibleForTesting @@ -249,7 +252,7 @@ void handleUrlVariableResponse(@NonNull final Event event, final String urlVaria .build(); if (StringUtils.isNullOrEmpty(urlVariables) && !StringUtils.isNullOrEmpty(errorMsg)) { - Log.warning(LOG_TAG, LOG_TAG, errorMsg); + Log.warning(LOG_TAG, LOG_SOURCE, errorMsg); } getApi().dispatch(responseEvent); @@ -263,12 +266,19 @@ void handleUrlVariableResponse(@NonNull final Event event, final String urlVaria void handleUpdateIdentities(@NonNull final Event event) { final Map eventData = event.getEventData(); - if (eventData == null) return; // TODO: Add log message when logging changes are made + if (eventData == null) { + Log.debug(LOG_TAG, LOG_SOURCE, "Cannot update identifiers, event data is null."); + return; + } final IdentityMap map = IdentityMap.fromXDMMap(eventData); if (map == null) { - Log.debug(LOG_TAG, LOG_TAG, "Failed to update identifiers as no identifiers were found in the event data."); + Log.debug( + LOG_TAG, + LOG_SOURCE, + "Failed to update identifiers as no identifiers were found in the event data." + ); return; } @@ -284,12 +294,19 @@ void handleUpdateIdentities(@NonNull final Event event) { void handleRemoveIdentity(@NonNull final Event event) { final Map eventData = event.getEventData(); - if (eventData == null) return; // TODO: Add log message when logging changes are made + if (eventData == null) { + Log.debug(LOG_TAG, LOG_SOURCE, "Cannot remove identifiers, event data is null."); + return; + } final IdentityMap map = IdentityMap.fromXDMMap(eventData); if (map == null) { - Log.debug(LOG_TAG, LOG_TAG, "Failed to remove identifiers as no identifiers were found in the event data."); + Log.debug( + LOG_TAG, + LOG_SOURCE, + "Failed to remove identifiers as no identifiers were found in the event data." + ); return; } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index b834e572..cc7c8f3e 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -18,6 +18,7 @@ import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; +import com.adobe.marketing.mobile.services.DataStoring; import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; import java.util.HashMap; @@ -34,9 +35,14 @@ class IdentityState { private IdentityProperties identityProperties; private boolean hasBooted; + IdentityState(final DataStoring dataStoreService) { + this(new IdentityStorageManager(dataStoreService)); + } + /** * Loads the persisted identities (if any) into {@link #identityProperties} */ + @VisibleForTesting IdentityState(final IdentityStorageManager identityStorageManager) { this.identityStorageManager = identityStorageManager; diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java index 8d72aae1..62092e10 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java @@ -1,5 +1,5 @@ /* - Copyright 2021 Adobe. All rights reserved. + 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 @@ -13,11 +13,9 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; -import androidx.annotation.VisibleForTesting; import com.adobe.marketing.mobile.services.DataStoring; import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.services.NamedCollection; -import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.JSONUtils; import com.adobe.marketing.mobile.util.StringUtils; import java.util.Map; @@ -29,15 +27,10 @@ */ class IdentityStorageManager { - private static final String LOG_SOURCE = "IdentityStorageService"; + private static final String LOG_SOURCE = "IdentityStorageManager"; private final NamedCollection edgeIdentityStore; private final NamedCollection directIdentityStore; - IdentityStorageManager() { - this(ServiceProvider.getInstance().getDataStoreService()); - } - - @VisibleForTesting IdentityStorageManager(final DataStoring dataStoreService) { this.edgeIdentityStore = dataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.DATASTORE_NAME); this.directIdentityStore = @@ -47,11 +40,13 @@ class IdentityStorageManager { /** * Loads identity properties from local storage, returns null if not found. * - * @return properties stored in local storage if present, otherwise null. + * @return {@code IdentityProperties} stored in local storage if present; + * null - if the content cannot be loaded from persistence or, if the content cannot be + * serialized to a {@code JSONObject} */ IdentityProperties loadPropertiesFromPersistence() { if (edgeIdentityStore == null) { - Log.debug( + Log.warning( LOG_TAG, LOG_SOURCE, "EdgeIdentity named collection is null. Unable to load saved identity properties from persistence." @@ -90,7 +85,7 @@ IdentityProperties loadPropertiesFromPersistence() { */ void savePropertiesToPersistence(final IdentityProperties properties) { if (edgeIdentityStore == null) { - Log.debug( + Log.warning( LOG_TAG, LOG_SOURCE, "EdgeIdentity named collection is null. Unable to write identity properties to persistence." From 5a5a6b20c21e36e1cfa39cddf85d9ddec0b2e241 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Fri, 2 Dec 2022 10:45:18 -0800 Subject: [PATCH 08/50] Downgrade logs and fix incorrect constant for IdentityDirect name --- .../marketing/mobile/edge/identity/IdentityExtension.java | 6 +++--- .../marketing/mobile/edge/identity/IdentityState.java | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) 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 24e9b7c8..ac595451 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 @@ -57,7 +57,7 @@ public void createXDMSharedState(final Map state, final Event ev * @param extensionApi {@link ExtensionApi} instance */ protected IdentityExtension(ExtensionApi extensionApi) { - this(extensionApi, new IdentityState(ServiceProvider.getInstance().getDataStoreService())); + this(extensionApi, new IdentityState()); } @VisibleForTesting @@ -267,7 +267,7 @@ void handleUpdateIdentities(@NonNull final Event event) { final Map eventData = event.getEventData(); if (eventData == null) { - Log.debug(LOG_TAG, LOG_SOURCE, "Cannot update identifiers, event data is null."); + Log.trace(LOG_TAG, LOG_SOURCE, "Cannot update identifiers, event data is null."); return; } @@ -295,7 +295,7 @@ void handleRemoveIdentity(@NonNull final Event event) { final Map eventData = event.getEventData(); if (eventData == null) { - Log.debug(LOG_TAG, LOG_SOURCE, "Cannot remove identifiers, event data is null."); + Log.trace(LOG_TAG, LOG_SOURCE, "Cannot remove identifiers, event data is null."); return; } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index cc7c8f3e..2abf6c67 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -20,6 +20,7 @@ import com.adobe.marketing.mobile.SharedStateStatus; import com.adobe.marketing.mobile.services.DataStoring; import com.adobe.marketing.mobile.services.Log; +import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.DataReader; import java.util.HashMap; import java.util.Map; @@ -35,8 +36,8 @@ class IdentityState { private IdentityProperties identityProperties; private boolean hasBooted; - IdentityState(final DataStoring dataStoreService) { - this(new IdentityStorageManager(dataStoreService)); + IdentityState() { + this(new IdentityStorageManager(ServiceProvider.getInstance().getDataStoreService())); } /** @@ -294,7 +295,7 @@ private boolean isIdentityDirectRegistered(final Map eventHubSha final Map identityDirectInfo = DataReader.optTypedMap( Object.class, extensions, - IdentityConstants.SharedState.Hub.EXTENSIONS, + IdentityConstants.SharedState.IdentityDirect.NAME, null ); From af416a7897119f02c82335a6e9b4bfee93466edb Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Thu, 1 Dec 2022 17:17:04 -0800 Subject: [PATCH 09/50] Remove Powermock and fix unit and functional tests --- code/build.gradle | 2 +- code/edgeidentity/build.gradle | 13 +- .../marketing/mobile/MobileCoreHelper.java | 26 + .../edge/identity/IdentityAdIdTest.java | 85 +- .../identity/IdentityAndroidTestUtil.java | 303 +++++ .../edge/identity/IdentityBootUpTest.java | 21 +- .../identity/IdentityECIDHandlingTest.java | 19 +- .../identity/IdentityFunctionalTestUtil.java | 73 +- .../edge/identity/IdentityPublicAPITest.java | 64 +- .../identity/IdentityResetHandlingTest.java | 12 +- .../identity/util}/ADBCountDownLatch.java | 13 +- .../identity/util/IdentityTestConstants.java | 42 + .../identity/util}/MockNetworkService.java | 11 +- .../identity/util}/MonitorExtension.java | 124 +- .../identity/util}/TestConstants.java | 2 +- .../{ => edge/identity/util}/TestHelper.java | 101 +- .../identity/util}/TestPersistenceHelper.java | 3 +- .../edge/identity/IdentityExtension.java | 13 +- .../mobile/edge/identity/IdentityState.java | 11 +- .../edge/identity/IdentityTestUtil.java | 197 +-- .../mobile/edge/identity/ECIDTests.java | 17 +- .../mobile/edge/identity/EventUtilsTests.java | 92 ++ .../edge/identity/IdentityExtensionTests.java | 1057 +++++++++-------- .../edge/identity/IdentityMapTests.java | 9 +- .../identity/IdentityPropertiesTests.java | 154 ++- .../edge/identity/IdentityStateTests.java | 723 +++++------ .../identity/IdentityStorageManagerTests.java | 208 ++++ .../identity/IdentityStorageServiceTests.java | 206 ---- .../mobile/edge/identity/IdentityTests.java | 934 +++++++++------ ...stenerEdgeIdentityRemoveIdentityTests.java | 105 -- ...tenerEdgeIdentityRequestIdentityTests.java | 132 -- ...stenerEdgeIdentityUpdateIdentityTests.java | 105 -- .../identity/ListenerEventHubBootTest.java | 84 -- .../identity/ListenerHubSharedStateTests.java | 105 -- .../ListenerIdentityRequestContentTests.java | 105 -- .../ListenerIdentityRequestResetTests.java | 105 -- .../edge/identity/MockIdentityState.java | 50 - .../org.mockito.plugins.MockMaker | 1 + 38 files changed, 2586 insertions(+), 2741 deletions(-) create mode 100644 code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java create mode 100644 code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java rename code/edgeidentity/src/{sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity => androidTest/java/com/adobe/marketing/mobile/edge/identity/util}/ADBCountDownLatch.java (85%) create mode 100644 code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java rename code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/{ => edge/identity/util}/MockNetworkService.java (80%) rename code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/{ => edge/identity/util}/MonitorExtension.java (69%) rename code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/{ => edge/identity/util}/TestConstants.java (96%) rename code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/{ => edge/identity/util}/TestHelper.java (91%) rename code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/{ => edge/identity/util}/TestPersistenceHelper.java (97%) create mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManagerTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageServiceTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentityTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentityTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentityTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBootTest.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedStateTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContentTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestResetTests.java delete mode 100644 code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/MockIdentityState.java create mode 100644 code/edgeidentity/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/code/build.gradle b/code/build.gradle index 865739af..56c90130 100644 --- a/code/build.gradle +++ b/code/build.gradle @@ -24,7 +24,7 @@ ext { // test dependencies junitVersion = "1.1.3" - mockitoCoreVersion = "2.28.2" + mockitoCoreVersion = "4.5.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // spotless diff --git a/code/edgeidentity/build.gradle b/code/edgeidentity/build.gradle index aef0c300..4e8529fc 100644 --- a/code/edgeidentity/build.gradle +++ b/code/edgeidentity/build.gradle @@ -229,18 +229,17 @@ dependencies { testImplementation "androidx.test.ext:junit:${rootProject.ext.junitVersion}" testImplementation "org.mockito:mockito-core:${rootProject.ext.mockitoCoreVersion}" testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' - testImplementation 'org.powermock:powermock-api-mockito2:2.0.0' - testImplementation 'org.powermock:powermock-module-junit4:2.0.0' testImplementation 'org.json:json:20180813' androidTestImplementation "androidx.test.ext:junit:${rootProject.ext.junitVersion}" androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - // Using older version of PowerMock as it supports minimum Android API < 26 - // the root issue appears to be a dependency with Objenesis in Mockito-Core - // where Objenesis 2 requires minimum Android API 26 - androidTestImplementation 'org.powermock:powermock-module-junit4:1+' androidTestImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' - androidTestImplementation "com.adobe.marketing.mobile:identity:1.+" + + // TODO: Uncomment next line when Identity 2.0 artifacts are published to maven + // implementation "com.adobe.marketing.mobile:identity:2.+" + androidTestImplementation ("com.github.adobe.aepsdk-core-android:identity:dev-v2.0.0-SNAPSHOT") { + transitive = false + } } tasks.withType(Test) { diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java new file mode 100644 index 00000000..c34a117c --- /dev/null +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java @@ -0,0 +1,26 @@ +/* + 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; + +/** + * Helper class that exists as a to access test helper methods provided in core + * within the package com.adobe.marketing.mobile + */ +public class MobileCoreHelper { + + /** + * Wrapper around {@link MobileCore#resetSDK()} + */ + public static void resetSDK() { + MobileCore.resetSDK(); + } +} diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java index 92fc643c..f7e080ee 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java @@ -11,23 +11,22 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.TestHelper.getDispatchedEventsWith; -import static com.adobe.marketing.mobile.TestHelper.getXDMSharedStateFor; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createXDMIdentityMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.setEdgeIdentityPersistence; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.createXDMIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getDispatchedEventsWith; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; import static org.junit.Assert.assertEquals; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.ExtensionError; -import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.MobileCore; -import com.adobe.marketing.mobile.TestHelper; -import com.adobe.marketing.mobile.TestPersistenceHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; +import com.adobe.marketing.mobile.util.JSONUtils; +import com.adobe.marketing.mobile.util.StringUtils; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -51,10 +50,11 @@ public void testGenericIdentityRequest_whenValidAdId_thenNewValidAdId() throws E String newAdId = "8d9ca5ff-7e74-44ac-bbcd-7aee7baf4f6c"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityTestUtil.TestItem("ECID", "primaryECID"), - new IdentityTestUtil.TestItem("GAID", initialAdId) + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) ) ); + registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); @@ -73,7 +73,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenNewValidAdId() throws E IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, newAdId); } @@ -83,8 +83,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenNonAdId() throws Except String initialAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityTestUtil.TestItem("ECID", "primaryECID"), - new IdentityTestUtil.TestItem("GAID", initialAdId) + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -105,7 +105,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenNonAdId() throws Except IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, initialAdId); } @@ -116,8 +116,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenSameValidAdId() throws String newAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityTestUtil.TestItem("ECID", "primaryECID"), - new IdentityTestUtil.TestItem("GAID", initialAdId) + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -137,7 +137,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenSameValidAdId() throws IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, newAdId); } @@ -148,8 +148,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenEmptyAdId() throws Exce String newAdId = ""; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityTestUtil.TestItem("ECID", "primaryECID"), - new IdentityTestUtil.TestItem("GAID", initialAdId) + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -169,7 +169,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenEmptyAdId() throws Exce IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, null); } @@ -180,8 +180,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E String newAdId = "00000000-0000-0000-0000-000000000000"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityTestUtil.TestItem("ECID", "primaryECID"), - new IdentityTestUtil.TestItem("GAID", initialAdId) + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -201,7 +201,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, null); } @@ -209,7 +209,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exception { // Test String newAdId = "8d9ca5ff-7e74-44ac-bbcd-7aee7baf4f6c"; - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); @@ -228,14 +228,14 @@ public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exce IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, newAdId); } @Test public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception { // Test - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); dispatchGenericIdentityNonAdIdEvent(); @@ -254,7 +254,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, null); } @@ -262,7 +262,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Exception { // Test String newAdId = ""; - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); @@ -280,7 +280,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Excepti IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, null); } @@ -288,7 +288,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Excepti public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws Exception { // Test String newAdId = "00000000-0000-0000-0000-000000000000"; - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); @@ -306,7 +306,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); verifyFlatIdentityMap(persistedMap, null); // Reset wildcard listener @@ -327,12 +327,12 @@ public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap2 = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson2))); + Map persistedMap2 = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson2))); verifyFlatIdentityMap(persistedMap2, null); } /** - * Verifies that the expected events from the {@link MobileCore#setAdvertisingIdentifier(String)} or {@link MobileCore#dispatchEvent(Event, ExtensionErrorCallback)} + * Verifies that the expected events from the {@link MobileCore#setAdvertisingIdentifier(String)} or {@link MobileCore#dispatchEvent(Event)} * APIs are properly dispatched. Verifies: * 1. Event type and source * 2. Event data/properties as required for proper ad ID functionality @@ -352,14 +352,14 @@ private void verifyDispatchedEvents(boolean isGenericIdentityEventAdIdEvent, Str // Verify Generic Identity event assertEquals(1, dispatchedGenericIdentityEvents.size()); Event genericIdentityEvent = dispatchedGenericIdentityEvents.get(0); - assertEquals(isGenericIdentityEventAdIdEvent ? true : false, EventUtils.isAdIdEvent(genericIdentityEvent)); + assertEquals(isGenericIdentityEventAdIdEvent, EventUtils.isAdIdEvent(genericIdentityEvent)); // Verify Edge Consent event List dispatchedConsentEvents = getDispatchedEventsWith( IdentityConstants.EventType.EDGE_CONSENT, IdentityConstants.EventSource.UPDATE_CONSENT ); - assertEquals(Utils.isNullOrEmpty(expectedConsentValue) ? 0 : 1, dispatchedConsentEvents.size()); - if (!Utils.isNullOrEmpty(expectedConsentValue)) { + assertEquals(StringUtils.isNullOrEmpty(expectedConsentValue) ? 0 : 1, dispatchedConsentEvents.size()); + if (!StringUtils.isNullOrEmpty(expectedConsentValue)) { Map consentDataMap = flattenMap(dispatchedConsentEvents.get(0).getEventData()); assertEquals("GAID", consentDataMap.get("consents.adID.idType")); assertEquals(expectedConsentValue, consentDataMap.get("consents.adID.val")); @@ -389,7 +389,6 @@ private void verifyFlatIdentityMap( assertEquals("false", flatIdentityMap.get("identityMap.ECID[0].primary")); assertEquals(expectedECID, flatIdentityMap.get("identityMap.ECID[0].id")); assertEquals("ambiguous", flatIdentityMap.get("identityMap.ECID[0].authenticatedState")); - return; } /** @@ -413,14 +412,6 @@ private void dispatchGenericIdentityNonAdIdEvent() { } ) .build(); - MobileCore.dispatchEvent( - genericIdentityNonAdIdEvent, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - Log.e("IdentityAdIdTest", "Failed to dispatch event." + extensionError.toString()); - } - } - ); + MobileCore.dispatchEvent(genericIdentityNonAdIdEvent); } } diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java new file mode 100644 index 00000000..9ef1bd82 --- /dev/null +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java @@ -0,0 +1,303 @@ +/* + 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 static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; + +import com.adobe.marketing.mobile.AdobeCallback; +import com.adobe.marketing.mobile.AdobeCallbackWithError; +import com.adobe.marketing.mobile.AdobeError; +import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.edge.identity.util.ADBCountDownLatch; +import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; +import com.adobe.marketing.mobile.services.Log; +import com.adobe.marketing.mobile.util.JSONUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Util class used by both Functional and Unit tests + */ +public class IdentityAndroidTestUtil { + + private static final String LOG_SOURCE = "IdentityAndroidTestUtil"; + + /** + * Helper method to create IdentityXDM Map using {@link TestItem}s + */ + static Map createXDMIdentityMap(TestItem... items) { + final Map>> allItems = new HashMap<>(); + + for (TestItem item : items) { + final Map itemMap = new HashMap<>(); + itemMap.put(IdentityConstants.XDMKeys.ID, item.id); + itemMap.put(IdentityConstants.XDMKeys.AUTHENTICATED_STATE, "ambiguous"); + itemMap.put(IdentityConstants.XDMKeys.PRIMARY, item.isPrimary); + List> nameSpaceItems = allItems.get(item.namespace); + + if (nameSpaceItems == null) { + nameSpaceItems = new ArrayList<>(); + } + + nameSpaceItems.add(itemMap); + allItems.put(item.namespace, nameSpaceItems); + } + + final Map identityMapDict = new HashMap<>(); + identityMapDict.put(IdentityConstants.XDMKeys.IDENTITY_MAP, allItems); + return identityMapDict; + } + + /** + * Helper method to build remove identity request event with XDM formatted Identity jsonString + */ + static Event buildRemoveIdentityRequestWithJSONString(final String jsonStr) throws Exception { + final JSONObject jsonObject = new JSONObject(jsonStr); + final Map xdmData = JSONUtils.toMap(jsonObject); + return buildRemoveIdentityRequest(xdmData); + } + + /** + * Helper method to build remove identity request event with XDM formatted Identity map + */ + static Event buildRemoveIdentityRequest(final Map map) { + return new Event.Builder( + "Remove Identity Event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REMOVE_IDENTITY + ) + .setEventData(map) + .build(); + } + + /** + * Helper method to build update identity request event with XDM formatted Identity jsonString + */ + static Event buildUpdateIdentityRequestJSONString(final String jsonStr) throws Exception { + final JSONObject jsonObject = new JSONObject(jsonStr); + final Map xdmData = JSONUtils.toMap(jsonObject); + return buildUpdateIdentityRequest(xdmData); + } + + /** + * Helper method to build update identity request event with XDM formatted Identity map + */ + static Event buildUpdateIdentityRequest(final Map map) { + return new Event.Builder( + "Update Identity Event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.UPDATE_IDENTITY + ) + .setEventData(map) + .build(); + } + + /** + * Serialize the given {@code jsonString} to a JSON Object, then flattens to {@code Map}. + * If the provided string is not in JSON structure an {@link JSONException} is thrown. + * + * @param jsonString the string in JSON structure to flatten + * @return new map with flattened structure + */ + static Map flattenJSONString(final String jsonString) throws JSONException { + JSONObject jsonObject = new JSONObject(jsonString); + Map persistenceValueMap = JSONUtils.toMap(jsonObject); + return flattenMap(persistenceValueMap); + } + + /** + * Serialize the given {@code map} to a JSON Object, then flattens to {@code Map}. + * For example, a JSON such as "{xdm: {stitchId: myID, eventType: myType}}" is flattened + * to two map elements "xdm.stitchId" = "myID" and "xdm.eventType" = "myType". + * + * @param map map with JSON structure to flatten + * @return new map with flattened structure + */ + static Map flattenMap(final Map map) { + if (map == null || map.isEmpty()) { + return Collections.emptyMap(); + } + + try { + JSONObject jsonObject = new JSONObject(map); + Map payloadMap = new HashMap<>(); + addKeys("", new ObjectMapper().readTree(jsonObject.toString()), payloadMap); + return payloadMap; + } catch (IOException e) { + Log.error(LOG_TAG, LOG_SOURCE, "Failed to parse JSON object to tree structure."); + } + + return Collections.emptyMap(); + } + + /** + * Deserialize {@code JsonNode} and flatten to provided {@code map}. + * For example, a JSON such as "{xdm: {stitchId: myID, eventType: myType}}" is flattened + * to two map elements "xdm.stitchId" = "myID" and "xdm.eventType" = "myType". + *

+ * Method is called recursively. To use, call with an empty path such as + * {@code addKeys("", new ObjectMapper().readTree(JsonNodeAsString), map);} + * + * @param currentPath the path in {@code JsonNode} to process + * @param jsonNode {@link JsonNode} to deserialize + * @param map {@code Map} instance to store flattened JSON result + * @see Stack Overflow post + */ + static void addKeys(String currentPath, JsonNode jsonNode, Map map) { + if (jsonNode.isObject()) { + ObjectNode objectNode = (ObjectNode) jsonNode; + Iterator> iter = objectNode.fields(); + String pathPrefix = currentPath.isEmpty() ? "" : currentPath + "."; + + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + addKeys(pathPrefix + entry.getKey(), entry.getValue(), map); + } + } else if (jsonNode.isArray()) { + ArrayNode arrayNode = (ArrayNode) jsonNode; + + for (int i = 0; i < arrayNode.size(); i++) { + addKeys(currentPath + "[" + i + "]", arrayNode.get(i), map); + } + } else if (jsonNode.isValueNode()) { + ValueNode valueNode = (ValueNode) jsonNode; + map.put(currentPath, valueNode.asText()); + } + } + + /** + * Class similar to {@link IdentityItem} for a specific namespace used for easier testing. + * For simplicity this class does not involve authenticatedState and primary key + */ + static class TestItem { + + private final String namespace; + private final String id; + private final boolean isPrimary = false; + + public TestItem(String namespace, String id) { + this.namespace = namespace; + this.id = id; + } + } + + /** + * Retrieves identities from Identity extension synchronously + * @return a {@code Map} of identities if retrieved successfully; + * null in case of a failure to retrieve it within timeout. + */ + static Map getIdentitiesSync() { + try { + final HashMap getIdentityResponse = new HashMap<>(); + final ADBCountDownLatch latch = new ADBCountDownLatch(1); + Identity.getIdentities( + new AdobeCallbackWithError() { + @Override + public void call(final IdentityMap identities) { + getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, identities); + latch.countDown(); + } + + @Override + public void fail(final AdobeError adobeError) { + getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.ERROR, adobeError); + latch.countDown(); + } + } + ); + latch.await(2000, TimeUnit.MILLISECONDS); + + return getIdentityResponse; + } catch (Exception exp) { + return null; + } + } + + /** + * Retrieves Experience Cloud Id from Identity extension synchronously + * @return an ECID if retrieved successfully; + * null in case of a failure to retrieve it within timeout. + */ + static String getExperienceCloudIdSync() { + try { + final HashMap getExperienceCloudIdResponse = new HashMap<>(); + final ADBCountDownLatch latch = new ADBCountDownLatch(1); + Identity.getExperienceCloudId( + new AdobeCallback() { + @Override + public void call(final String ecid) { + getExperienceCloudIdResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, ecid); + latch.countDown(); + } + } + ); + latch.await(2000, TimeUnit.MILLISECONDS); + return getExperienceCloudIdResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); + } catch (Exception exp) { + return null; + } + } + + /** + * Retrieves url variables from Identity extension synchronously + * @return a url variable string if retrieved successfully; + * null in case of a failure to retrieve it within timeout. + */ + static String getUrlVariablesSync() { + try { + final HashMap getUrlVariablesResponse = new HashMap<>(); + final ADBCountDownLatch latch = new ADBCountDownLatch(1); + Identity.getUrlVariables( + new AdobeCallback() { + @Override + public void call(final String urlVariables) { + getUrlVariablesResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, urlVariables); + latch.countDown(); + } + } + ); + latch.await(2000, TimeUnit.MILLISECONDS); + return getUrlVariablesResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); + } catch (Exception exp) { + return null; + } + } + + static IdentityMap createIdentityMap(final String namespace, final String id) { + return createIdentityMap(namespace, id, AuthenticatedState.AMBIGUOUS, false); + } + + static IdentityMap createIdentityMap( + final String namespace, + final String id, + final AuthenticatedState state, + final boolean isPrimary + ) { + IdentityMap map = new IdentityMap(); + IdentityItem item = new IdentityItem(id, state, isPrimary); + map.addItem(item, namespace); + return map; + } +} diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java index 3653c9d7..92261db0 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java @@ -11,14 +11,15 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.TestHelper.getXDMSharedStateFor; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createXDMIdentityMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.*; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.createXDMIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; import static org.junit.Assert.assertEquals; -import com.adobe.marketing.mobile.TestHelper; -import com.adobe.marketing.mobile.TestPersistenceHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; +import com.adobe.marketing.mobile.util.JSONUtils; import java.util.Map; import org.json.JSONObject; import org.junit.Rule; @@ -41,10 +42,10 @@ public void testOnBootUp_LoadsAllIdentitiesFromPreference() throws Exception { // test setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityTestUtil.TestItem("ECID", "primaryECID"), - new IdentityTestUtil.TestItem("ECID", "secondaryECID"), - new IdentityTestUtil.TestItem("Email", "example@email.com"), - new IdentityTestUtil.TestItem("UserId", "JohnDoe") + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("ECID", "secondaryECID"), + new IdentityAndroidTestUtil.TestItem("Email", "example@email.com"), + new IdentityAndroidTestUtil.TestItem("UserId", "JohnDoe") ) ); registerEdgeIdentityExtension(); @@ -62,7 +63,7 @@ public void testOnBootUp_LoadsAllIdentitiesFromPreference() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(12, persistedMap.size()); // 3 for ECID and 3 for secondaryECID + 6 } // -------------------------------------------------------------------------------------------- diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java index e20b7005..870e51ae 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java @@ -11,14 +11,15 @@ package com.adobe.marketing.mobile.edge.identity; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createXDMIdentityMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.*; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.*; import static org.junit.Assert.*; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.MobilePrivacyStatus; -import com.adobe.marketing.mobile.TestHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestHelper; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; @@ -45,7 +46,10 @@ public void testECID_autoGeneratedWhenBooted() throws InterruptedException { public void testECID_loadedFromPersistence() throws Exception { // setup setEdgeIdentityPersistence( - createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("ECID", "secondaryECID")) + createXDMIdentityMap( + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("ECID", "secondaryECID") + ) ); registerEdgeIdentityExtension(); @@ -58,7 +62,7 @@ public void testECID_loadedFromPersistence() throws Exception { public void testECID_edgePersistenceTakesPreferenceOverDirectExtension() throws Exception { // setup setIdentityDirectPersistedECID("legacyECID"); - setEdgeIdentityPersistence(CreateIdentityMap("ECID", "edgeECID").asXDMMap()); + setEdgeIdentityPersistence(IdentityAndroidTestUtil.createIdentityMap("ECID", "edgeECID").asXDMMap()); registerEdgeIdentityExtension(); // verify @@ -112,7 +116,10 @@ public void testECID_whenBothExtensionRegistered_migrationPath() throws Exceptio public void testECID_onResetClearsOldECID() throws Exception { // setup setEdgeIdentityPersistence( - createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("ECID", "secondaryECID")) + createXDMIdentityMap( + new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), + new IdentityAndroidTestUtil.TestItem("ECID", "secondaryECID") + ) ); registerEdgeIdentityExtension(); @@ -179,7 +186,7 @@ public void testECID_AreDifferentAfterResetIdentitiesAndPrivacyChange() throws E public void testECID_DirectEcidIsRemovedOnPrivacyOptOut() throws Exception { // setup setIdentityDirectPersistedECID("legacyECID"); - setEdgeIdentityPersistence(CreateIdentityMap("ECID", "edgeECID").asXDMMap()); + setEdgeIdentityPersistence(IdentityAndroidTestUtil.createIdentityMap("ECID", "edgeECID").asXDMMap()); registerBothIdentityExtensions(); // verify ECID diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java index 40e8465f..29898f95 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java @@ -11,17 +11,21 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.TestHelper.getXDMSharedStateFor; -import static com.adobe.marketing.mobile.TestHelper.resetTestExpectations; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.flattenMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.getExperienceCloudIdSync; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.resetTestExpectations; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import com.adobe.marketing.mobile.AdobeCallback; import com.adobe.marketing.mobile.MobileCore; -import com.adobe.marketing.mobile.TestHelper; -import com.adobe.marketing.mobile.TestPersistenceHelper; +import com.adobe.marketing.mobile.edge.identity.util.ADBCountDownLatch; +import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; +import com.adobe.marketing.mobile.edge.identity.util.TestHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; +import com.adobe.marketing.mobile.util.JSONUtils; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -33,46 +37,8 @@ public class IdentityFunctionalTestUtil { * Register's Edge Identity Extension and start the Core */ static void registerEdgeIdentityExtension() throws InterruptedException { - com.adobe.marketing.mobile.edge.identity.Identity.registerExtension(); - - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - MobileCore.start( - new AdobeCallback() { - @Override - public void call(Object o) { - latch.countDown(); - } - } - ); - - latch.await(1000, TimeUnit.MILLISECONDS); - TestHelper.waitForThreads(2000); - resetTestExpectations(); - } - - /** - * Register's Identity Direct Extension and start the Core - */ - static void registerIdentityDirectExtension() throws Exception { - HashMap config = new HashMap() { - { - put("global.privacy", "optedin"); - put("experienceCloud.org", "testOrg@AdobeOrg"); - put("experienceCloud.server", "notaserver"); - } - }; - MobileCore.updateConfiguration(config); - com.adobe.marketing.mobile.Identity.registerExtension(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - MobileCore.start( - new AdobeCallback() { - @Override - public void call(Object o) { - latch.countDown(); - } - } - ); + MobileCore.registerExtensions(Arrays.asList(Identity.EXTENSION), o -> latch.countDown()); latch.await(1000, TimeUnit.MILLISECONDS); TestHelper.waitForThreads(2000); @@ -92,17 +58,10 @@ static void registerBothIdentityExtensions() throws Exception { }; MobileCore.updateConfiguration(config); - com.adobe.marketing.mobile.edge.identity.Identity.registerExtension(); - com.adobe.marketing.mobile.Identity.registerExtension(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - MobileCore.start( - new AdobeCallback() { - @Override - public void call(Object o) { - latch.countDown(); - } - } + MobileCore.registerExtensions( + Arrays.asList(Identity.EXTENSION, com.adobe.marketing.mobile.Identity.EXTENSION), + o -> latch.countDown() ); latch.await(); @@ -206,7 +165,7 @@ static void verifyPrimaryECID(final String primaryECID) throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(primaryECID, persistedMap.get("identityMap.ECID[0].id")); } @@ -224,7 +183,7 @@ static void verifySecondaryECID(final String secondaryECID) throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(secondaryECID, persistedMap.get("identityMap.ECID[1].id")); } } diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java index bc187a84..27df8d6e 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java @@ -11,16 +11,22 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.TestHelper.*; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createIdentityMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getIdentitiesSync; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getUrlVariablesSync; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.setupConfiguration; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.*; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.*; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.TestHelper; -import com.adobe.marketing.mobile.TestPersistenceHelper; +import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; +import com.adobe.marketing.mobile.edge.identity.util.TestHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; +import com.adobe.marketing.mobile.util.JSONUtils; import java.util.List; import java.util.Map; import org.json.JSONObject; @@ -63,11 +69,11 @@ public void testGetExtensionVersionAPI() { @Test public void testRegisterExtensionAPI() throws InterruptedException { // test - // Consent.registerExtension() is called in the setup method + // Identity.registerExtension() is called in the setup method // verify that the extension is registered with the correct version details Map sharedStateMap = flattenMap( - getSharedStateFor(IdentityTestConstants.SharedStateName.EVENT_HUB, 1000) + getSharedStateFor(IdentityTestConstants.SharedStateName.EVENT_HUB, 5000) ); assertEquals( IdentityConstants.EXTENSION_VERSION, @@ -83,7 +89,7 @@ public void testRegisterExtensionAPI() throws InterruptedException { public void testUpdateIdentitiesAPI() throws Exception { // test Identity.updateIdentities( - CreateIdentityMap("Email", "example@email.com", AuthenticatedState.AUTHENTICATED, true) + createIdentityMap("Email", "example@email.com", AuthenticatedState.AUTHENTICATED, true) ); TestHelper.waitForThreads(2000); @@ -99,7 +105,7 @@ public void testUpdateIdentitiesAPI() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(6, persistedMap.size()); // 3 for ECID and 3 for Email assertEquals("example@email.com", persistedMap.get("identityMap.Email[0].id")); assertEquals("true", persistedMap.get("identityMap.Email[0].primary")); @@ -147,12 +153,12 @@ public void testUpdateAPI_emptyData() throws Exception { @Test public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { // test - Identity.updateIdentities(CreateIdentityMap("Email", "example@email.com")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("Email", "example@email.com")); Identity.updateIdentities( - CreateIdentityMap("Email", "example@email.com", AuthenticatedState.AUTHENTICATED, true) + createIdentityMap("Email", "example@email.com", AuthenticatedState.AUTHENTICATED, true) ); Identity.updateIdentities( - CreateIdentityMap("Email", "example@email.com", AuthenticatedState.LOGGED_OUT, false) + createIdentityMap("Email", "example@email.com", AuthenticatedState.LOGGED_OUT, false) ); TestHelper.waitForThreads(2000); @@ -168,7 +174,7 @@ public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(6, persistedMap.size()); // 3 for ECID and 3 for Email assertEquals("example@email.com", persistedMap.get("identityMap.Email[0].id")); assertEquals("false", persistedMap.get("identityMap.Email[0].primary")); @@ -178,13 +184,13 @@ public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { @Test public void testUpdateAPI_withReservedNamespaces() throws Exception { // test - Identity.updateIdentities(CreateIdentityMap("ECID", "newECID")); - Identity.updateIdentities(CreateIdentityMap("GAID", "")); - Identity.updateIdentities(CreateIdentityMap("IDFA", "")); - Identity.updateIdentities(CreateIdentityMap("IDFa", "")); - Identity.updateIdentities(CreateIdentityMap("gaid", "")); - Identity.updateIdentities(CreateIdentityMap("ecid", "")); - Identity.updateIdentities(CreateIdentityMap("idfa", "")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("ECID", "newECID")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("GAID", "")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("IDFA", "")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("IDFa", "")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("gaid", "")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("ecid", "")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("idfa", "")); TestHelper.waitForThreads(2000); // verify xdm shared state does not get updated @@ -197,7 +203,7 @@ public void testUpdateAPI_withReservedNamespaces() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(3, persistedMap.size()); // 3 for ECID assertNotEquals("newECID", persistedMap.get("identityMap.ECID[0].id")); // ECID doesn't get replaced by API } @@ -226,7 +232,7 @@ public void testUpdateAPI_multipleNamespaceMap() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(15, persistedMap.size()); // 3 for ECID + 12 for new identities assertEquals("primary@email.com", persistedMap.get("identityMap.Email[0].id")); assertEquals("secondary@email.com", persistedMap.get("identityMap.Email[1].id")); @@ -254,7 +260,7 @@ public void testUpdateAPI_caseSensitiveNamespacesForCustomIdentifiers() throws E IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(9, persistedMap.size()); // 3 for ECID + 6 for new identities assertEquals("primary@email.com", persistedMap.get("identityMap.Email[0].id")); assertEquals("secondary@email.com", persistedMap.get("identityMap.email[0].id")); @@ -381,7 +387,7 @@ public void testRemoveIdentity() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(3, persistedMap.size()); // 3 for ECID } @@ -401,7 +407,7 @@ public void testRemoveIdentity_nonExistentNamespace() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(3, persistedMap.size()); // 3 for ECID } @@ -409,7 +415,7 @@ public void testRemoveIdentity_nonExistentNamespace() throws Exception { public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { // setup // update Identities through API - Identity.updateIdentities(CreateIdentityMap("Email", "example@email.com")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("Email", "example@email.com")); // test Identity.removeIdentity(new IdentityItem("example@email.com"), "email"); @@ -425,7 +431,7 @@ public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(6, persistedMap.size()); // 3 for ECID + 3 for Email } @@ -433,7 +439,7 @@ public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { public void testRemoveIdentity_nonExistentItem() throws Exception { // setup // update Identities through API - Identity.updateIdentities(CreateIdentityMap("Email", "example@email.com")); + Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("Email", "example@email.com")); // test Identity.removeIdentity(new IdentityItem("secondary@email.com"), "Email"); @@ -449,7 +455,7 @@ public void testRemoveIdentity_nonExistentItem() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(6, persistedMap.size()); // 3 for ECID + 3 for Email } @@ -472,7 +478,7 @@ public void testRemoveIdentity_doesNotRemoveECID() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(3, persistedMap.size()); // 3 for ECID that still exists } } diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java index 985ff51d..a1810a70 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java @@ -11,15 +11,17 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.TestHelper.*; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.*; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.*; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.MobileCore; -import com.adobe.marketing.mobile.TestHelper; -import com.adobe.marketing.mobile.TestPersistenceHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestHelper; +import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; +import com.adobe.marketing.mobile.util.JSONUtils; import java.util.List; import java.util.Map; import org.json.JSONObject; @@ -84,7 +86,7 @@ public void testReset_ClearsAllIDAndDispatchesResetComplete() throws Exception { IdentityConstants.DataStoreKey.DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES ); - Map persistedMap = flattenMap(IdentityTestUtil.toMap(new JSONObject(persistedJson))); + Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(3, persistedMap.size()); // 3 for ECID assertEquals(newECID, persistedMap.get("identityMap.ECID[0].id")); } diff --git a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/ADBCountDownLatch.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/ADBCountDownLatch.java similarity index 85% rename from code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/ADBCountDownLatch.java rename to code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/ADBCountDownLatch.java index 4bf97273..3d36853c 100644 --- a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/ADBCountDownLatch.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/ADBCountDownLatch.java @@ -1,5 +1,5 @@ /* - Copyright 2021 Adobe. All rights reserved. + 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 @@ -9,12 +9,15 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile.edge.identity; +package com.adobe.marketing.mobile.edge.identity.util; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +/** + * Test utility class that facilitates waiting. + */ public class ADBCountDownLatch { private final CountDownLatch latch; @@ -40,10 +43,6 @@ public void countDown() { latch.countDown(); } - public long getCount() { - return latch.getCount(); - } - public int getInitialCount() { return initialCount; } @@ -54,6 +53,6 @@ public int getCurrentCount() { @Override public String toString() { - return String.format("%s, initial: %d, current: %d", latch.toString(), initialCount, currentCount.get()); + return String.format("%s, initial: %d, current: %d", latch, initialCount, currentCount.get()); } } diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java new file mode 100644 index 00000000..9e6cffc5 --- /dev/null +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java @@ -0,0 +1,42 @@ +/* + 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.util; + +public class IdentityTestConstants { + + public static final String LOG_TAG = "EdgeIdentity"; + + public static final class DataStoreKey { + + public static final String CONFIG_DATASTORE = "AdobeMobile_ConfigState"; + public static final String IDENTITY_DATASTORE = "com.adobe.edge.identity"; + public static final String IDENTITY_DIRECT_DATASTORE = "visitorIDServiceDataStore"; + + private DataStoreKey() {} + } + + public static final class SharedStateName { + + public static final String CONFIG = "com.adobe.module.configuration"; + public static final String EVENT_HUB = "com.adobe.module.eventhub"; + + private SharedStateName() {} + } + + public static final class GetIdentitiesHelper { + + public static final String VALUE = "getConsentValue"; + public static final String ERROR = "getConsentError"; + + private GetIdentitiesHelper() {} + } +} diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MockNetworkService.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MockNetworkService.java similarity index 80% rename from code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MockNetworkService.java rename to code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MockNetworkService.java index 537e14b1..56f96bc9 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MockNetworkService.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MockNetworkService.java @@ -9,9 +9,12 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile; +package com.adobe.marketing.mobile.edge.identity.util; + +import static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; import com.adobe.marketing.mobile.services.HttpConnecting; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.services.NetworkCallback; import com.adobe.marketing.mobile.services.NetworkRequest; import com.adobe.marketing.mobile.services.Networking; @@ -24,9 +27,9 @@ */ class MockNetworkService implements Networking { - private static String TAG = "MockNetworkService"; + private static final String LOG_SOURCE = "MockNetworkService"; - private static HttpConnecting dummyConnection = new HttpConnecting() { + private static final HttpConnecting dummyConnection = new HttpConnecting() { @Override public InputStream getInputStream() { return new ByteArrayInputStream("{}".getBytes()); @@ -62,7 +65,7 @@ public void connectAsync(NetworkRequest networkRequest, NetworkCallback networkC return; } - MobileCore.log(LoggingMode.DEBUG, TAG, "Received async request '" + networkRequest.getUrl() + "', ignoring."); + Log.debug(LOG_TAG, LOG_SOURCE, "Received async request '" + networkRequest.getUrl() + "', ignoring."); if (networkCallback != null) { networkCallback.call(dummyConnection); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java similarity index 69% rename from code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java rename to code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java index 4b7478ee..9a4d95e9 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MonitorExtension.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java @@ -9,9 +9,23 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile; - -import com.adobe.marketing.mobile.edge.identity.ADBCountDownLatch; +package com.adobe.marketing.mobile.edge.identity.util; + +import static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; + +import androidx.annotation.NonNull; +import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; +import com.adobe.marketing.mobile.Extension; +import com.adobe.marketing.mobile.ExtensionApi; +import com.adobe.marketing.mobile.ExtensionError; +import com.adobe.marketing.mobile.ExtensionErrorCallback; +import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.SharedStateResolution; +import com.adobe.marketing.mobile.SharedStateResult; +import com.adobe.marketing.mobile.services.Log; +import com.adobe.marketing.mobile.util.DataReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -22,44 +36,40 @@ * A third party extension class aiding for assertion against dispatched events, shared state * and XDM shared state. */ -class MonitorExtension extends Extension { +public class MonitorExtension extends Extension { - private static final String LOG_TAG = "MonitorExtension"; + public static final Class EXTENSION = MonitorExtension.class; + private static final String LOG_SOURCE = "MonitorExtension"; private static final Map> receivedEvents = new HashMap<>(); private static final Map expectedEvents = new HashMap<>(); protected MonitorExtension(ExtensionApi extensionApi) { super(extensionApi); - extensionApi.registerWildcardListener( - MonitorListener.class, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.ERROR, - LOG_TAG, - "There was an error registering Extension Listener: " + extensionError.getErrorName() - ); - } - } - ); } + @NonNull @Override protected String getName() { return "MonitorExtension"; } + @Override + protected void onRegistered() { + super.onRegistered(); + + getApi().registerEventListener(EventType.WILDCARD, EventSource.WILDCARD, this::wildcardProcessor); + } + public static void registerExtension() { MobileCore.registerExtension( MonitorExtension.class, new ExtensionErrorCallback() { @Override public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.ERROR, + Log.error( LOG_TAG, + LOG_SOURCE, "There was an error registering the Monitor extension: " + extensionError.getErrorName() ); } @@ -77,15 +87,7 @@ public static void unregisterExtension() { TestConstants.EventSource.UNREGISTER ) .build(); - MobileCore.dispatchEvent( - event, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log(LoggingMode.ERROR, LOG_TAG, "Failed to unregister Monitor extension."); - } - } - ); + MobileCore.dispatchEvent(event); } /** @@ -111,7 +113,7 @@ public static Map> getReceivedEvents() { * Resets the map of received and expected events. */ public static void reset() { - MobileCore.log(LoggingMode.VERBOSE, LOG_TAG, "Reset expected and received events."); + Log.trace(LOG_TAG, LOG_SOURCE, "Reset expected and received events."); receivedEvents.clear(); expectedEvents.clear(); } @@ -123,7 +125,7 @@ public static void reset() { * All other events are added to the map of received events. If the event is in the map * of expected events, its latch is counted down. * - * @param event + * @param event the event to be processed */ public void wildcardProcessor(final Event event) { if (TestConstants.EventType.MONITOR.equalsIgnoreCase(event.getType())) { @@ -140,10 +142,10 @@ public void wildcardProcessor(final Event event) { EventSpec eventSpec = new EventSpec(event.getSource(), event.getType()); - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Received and processing event " + eventSpec); + Log.debug(LOG_TAG, LOG_SOURCE, "Received and processing event " + eventSpec); if (!receivedEvents.containsKey(eventSpec)) { - receivedEvents.put(eventSpec, new ArrayList()); + receivedEvents.put(eventSpec, new ArrayList<>()); } receivedEvents.get(eventSpec).add(event); @@ -155,43 +157,44 @@ public void wildcardProcessor(final Event event) { /** * Processor which unregisters this extension. - * @param event + * @param event the event to be processed */ private void processUnregisterRequest(final Event event) { - MobileCore.log(LoggingMode.DEBUG, LOG_TAG, "Unregistering the Monitor Extension."); + Log.debug(LOG_TAG, LOG_SOURCE, "Unregistering the Monitor Extension."); getApi().unregisterExtension(); } /** * Processor which retrieves and dispatches the XDM shared state for the state owner specified * in the request. - * @param event + * @param event the event to be processed */ private void processXDMSharedStateRequest(final Event event) { - EventData eventData = event.getData(); + final Map eventData = event.getEventData(); if (eventData == null) { return; } - String stateOwner = eventData.optString(TestConstants.EventDataKey.STATE_OWNER, null); + final String stateOwner = DataReader.optString(eventData, TestConstants.EventDataKey.STATE_OWNER, null); if (stateOwner == null) { return; } - EventData sharedState = getApi().getXDMSharedEventState(stateOwner, event); + final SharedStateResult sharedStateResult = getApi() + .getXDMSharedState(stateOwner, event, false, SharedStateResolution.LAST_SET); Event responseEvent = new Event.Builder( "Get Shared State Response", TestConstants.EventType.MONITOR, TestConstants.EventSource.XDM_SHARED_STATE_RESPONSE ) - .setEventData(sharedState == null ? null : sharedState.toObjectMap()) - .setPairID(event.getResponsePairID()) + .setEventData(sharedStateResult == null ? null : sharedStateResult.getValue()) + .inResponseToEvent(event) .build(); - MobileCore.dispatchResponseEvent(responseEvent, event, null); + MobileCore.dispatchEvent(responseEvent); } /** @@ -200,54 +203,31 @@ private void processXDMSharedStateRequest(final Event event) { * @param event */ private void processSharedStateRequest(final Event event) { - EventData eventData = event.getData(); + final Map eventData = event.getEventData(); if (eventData == null) { return; } - String stateOwner = eventData.optString(TestConstants.EventDataKey.STATE_OWNER, null); + final String stateOwner = DataReader.optString(eventData, TestConstants.EventDataKey.STATE_OWNER, null); if (stateOwner == null) { return; } - EventData sharedState = getApi().getSharedEventState(stateOwner, event); + final SharedStateResult sharedStateResult = getApi() + .getSharedState(stateOwner, event, false, SharedStateResolution.LAST_SET); Event responseEvent = new Event.Builder( "Get Shared State Response", TestConstants.EventType.MONITOR, TestConstants.EventSource.SHARED_STATE_RESPONSE ) - .setEventData(sharedState == null ? null : sharedState.toObjectMap()) - .setPairID(event.getResponsePairID()) + .setEventData(sharedStateResult == null ? null : sharedStateResult.getValue()) + .inResponseToEvent(event) .build(); - MobileCore.dispatchResponseEvent(responseEvent, event, null); - } - - /** - * Listener class - */ - public static class MonitorListener extends ExtensionListener { - - protected MonitorListener(ExtensionApi extension, String type, String source) { - super(extension, type, source); - } - - @Override - public void hear(Event event) { - MonitorExtension extension = getParentExtension(); - - if (extension != null) { - extension.wildcardProcessor(event); - } - } - - @Override - protected MonitorExtension getParentExtension() { - return (MonitorExtension) super.getParentExtension(); - } + MobileCore.dispatchEvent(responseEvent); } /** diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestConstants.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestConstants.java similarity index 96% rename from code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestConstants.java rename to code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestConstants.java index 674fb1e4..69045499 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestConstants.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestConstants.java @@ -9,7 +9,7 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile; +package com.adobe.marketing.mobile.edge.identity.util; /** * Class to maintain test constants. diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java similarity index 91% rename from code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestHelper.java rename to code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java index 387ef5da..1e8c5b5f 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java @@ -9,8 +9,9 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile; +package com.adobe.marketing.mobile.edge.identity.util; +import static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -19,8 +20,14 @@ import android.app.Instrumentation; import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; -import com.adobe.marketing.mobile.MonitorExtension.EventSpec; -import com.adobe.marketing.mobile.edge.identity.ADBCountDownLatch; +import com.adobe.marketing.mobile.AdobeCallbackWithError; +import com.adobe.marketing.mobile.AdobeError; +import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.LoggingMode; +import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.MobileCoreHelper; +import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension.EventSpec; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.services.ServiceProvider; import java.util.ArrayList; import java.util.Collections; @@ -39,7 +46,7 @@ */ public class TestHelper { - private static final String TAG = "TestHelper"; + private static final String LOG_SOURCE = "TestHelper"; static final int WAIT_TIMEOUT_MS = 1000; static final int WAIT_EVENT_TIMEOUT_MS = 2000; static Application defaultApplication; @@ -81,24 +88,14 @@ public void evaluate() throws Throwable { try { base.evaluate(); } catch (Throwable e) { - MobileCore.log(LoggingMode.DEBUG, "SetupCoreRule", "Wait after test failure."); + Log.debug(LOG_TAG, "SetupCoreRule", "Wait after test failure."); throw e; // rethrow test failure } finally { // After test execution - MobileCore.log( - LoggingMode.DEBUG, - "SetupCoreRule", - "Finished '" + description.getMethodName() + "'" - ); + Log.debug(LOG_TAG, "SetupCoreRule", "Finished '" + description.getMethodName() + "'"); waitForThreads(5000); // wait to allow thread to run after test execution - Core core = MobileCore.getCore(); - if (core != null && core.eventHub != null) { - core.eventHub.shutdown(); - core.eventHub = null; - } - - MobileCore.setCore(null); + MobileCoreHelper.resetSDK(); TestPersistenceHelper.resetKnownPersistence(); resetTestExpectations(); } @@ -157,16 +154,12 @@ public static void waitForThreads(final int timeoutMillis) { Set threadSet = getEligibleThreads(); while (threadSet.size() > 0 && ((System.currentTimeMillis() - startTime) < timeoutTestMillis)) { - MobileCore.log( - LoggingMode.DEBUG, - TAG, - "waitForThreads - Still waiting for " + threadSet.size() + " thread(s)" - ); + Log.debug(LOG_TAG, LOG_SOURCE, "waitForThreads - Still waiting for " + threadSet.size() + " thread(s)"); for (Thread t : threadSet) { - MobileCore.log( - LoggingMode.DEBUG, - TAG, + Log.debug( + LOG_TAG, + LOG_SOURCE, "waitForThreads - Waiting for thread " + t.getName() + " (" + t.getId() + ")" ); boolean done = false; @@ -189,15 +182,15 @@ public static void waitForThreads(final int timeoutMillis) { } if (timedOut) { - MobileCore.log( - LoggingMode.DEBUG, - TAG, + Log.debug( + LOG_TAG, + LOG_SOURCE, "waitForThreads - Timeout out waiting for thread " + t.getName() + " (" + t.getId() + ")" ); } else { - MobileCore.log( - LoggingMode.DEBUG, - TAG, + Log.debug( + LOG_TAG, + LOG_SOURCE, "waitForThreads - Done waiting for thread " + t.getName() + " (" + t.getId() + ")" ); } @@ -206,7 +199,7 @@ public static void waitForThreads(final int timeoutMillis) { threadSet = getEligibleThreads(); } - MobileCore.log(LoggingMode.DEBUG, TAG, "waitForThreads - All known threads are terminated."); + Log.debug(LOG_TAG, LOG_SOURCE, "waitForThreads - All known threads are terminated."); } /** @@ -257,7 +250,7 @@ private static boolean isAppThread(final Thread t) { * Resets the network and event test expectations. */ public static void resetTestExpectations() { - MobileCore.log(LoggingMode.DEBUG, TAG, "Resetting functional test expectations for events"); + Log.debug(LOG_TAG, LOG_SOURCE, "Resetting functional test expectations for events"); MonitorExtension.reset(); } @@ -376,9 +369,9 @@ public static void assertUnexpectedEvents(final boolean shouldWait) throws Inter receivedEvent.getValue().size() ) ); - MobileCore.log( - LoggingMode.DEBUG, - TAG, + Log.debug( + LOG_TAG, + LOG_SOURCE, String.format( "Received unexpected event with type: %s source: %s", receivedEvent.getKey().type, @@ -477,7 +470,13 @@ public static Map getSharedStateFor(final String stateOwner, int final Map sharedState = new HashMap<>(); MobileCore.dispatchEventWithResponseCallback( event, - new AdobeCallback() { + 5000L, + new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + Log.error(LOG_TAG, LOG_SOURCE, "Failed to get shared state for " + stateOwner + ": " + adobeError); + } + @Override public void call(Event event) { if (event.getEventData() != null) { @@ -486,16 +485,6 @@ public void call(Event event) { latch.countDown(); } - }, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.ERROR, - TAG, - "Failed to get shared state for " + stateOwner + ": " + extensionError - ); - } } ); @@ -532,7 +521,13 @@ public static Map getXDMSharedStateFor(final String stateOwner, final Map sharedState = new HashMap<>(); MobileCore.dispatchEventWithResponseCallback( event, - new AdobeCallback() { + 5000L, + new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + Log.debug(LOG_TAG, LOG_SOURCE, "Failed to get shared state for " + stateOwner + ": " + adobeError); + } + @Override public void call(Event event) { if (event.getEventData() != null) { @@ -541,16 +536,6 @@ public void call(Event event) { latch.countDown(); } - }, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - MobileCore.log( - LoggingMode.ERROR, - TAG, - "Failed to get shared state for " + stateOwner + ": " + extensionError - ); - } } ); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestPersistenceHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestPersistenceHelper.java similarity index 97% rename from code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestPersistenceHelper.java rename to code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestPersistenceHelper.java index 65f8fae9..84de4b78 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/TestPersistenceHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestPersistenceHelper.java @@ -9,14 +9,13 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile; +package com.adobe.marketing.mobile.edge.identity.util; import static org.junit.Assert.fail; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; -import com.adobe.marketing.mobile.edge.identity.IdentityTestConstants; import java.util.ArrayList; /** 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 ac595451..ef6f2c77 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 @@ -22,7 +22,6 @@ import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; import com.adobe.marketing.mobile.services.Log; -import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.StringUtils; import com.adobe.marketing.mobile.util.TimeUtils; import java.util.HashMap; @@ -95,6 +94,8 @@ protected String getVersion() { */ @Override protected void onRegistered() { + super.onRegistered(); + // GENERIC_IDENTITY event listeners getApi() .registerEventListener( @@ -176,7 +177,7 @@ void handleRequestIdentity(@NonNull final Event event) { * * @param event the identity request {@link Event} */ - private void handleUrlVariablesRequest(@NonNull final Event event) { + void handleUrlVariablesRequest(@NonNull final Event event) { final SharedStateResult configSharedStateResult = sharedStateHandle.getSharedState( IdentityConstants.SharedState.Configuration.NAME, event @@ -224,7 +225,7 @@ private void handleUrlVariablesRequest(@NonNull final Event event) { * @param event the identity request {@link Event} * @param urlVariables {@link String} representing the urlVariables encoded string */ - void handleUrlVariableResponse(@NonNull final Event event, final String urlVariables) { + private void handleUrlVariableResponse(@NonNull final Event event, final String urlVariables) { handleUrlVariableResponse(event, urlVariables, null); } @@ -235,7 +236,11 @@ void handleUrlVariableResponse(@NonNull final Event event, final String urlVaria * @param urlVariables {@link String} representing the urlVariables encoded string * @param errorMsg {@link String} representing error encountered while generating the urlVariables string */ - void handleUrlVariableResponse(@NonNull final Event event, final String urlVariables, final String errorMsg) { + private void handleUrlVariableResponse( + @NonNull final Event event, + final String urlVariables, + final String errorMsg + ) { Event responseEvent = new Event.Builder( IdentityConstants.EventNames.IDENTITY_RESPONSE_URL_VARIABLES, IdentityConstants.EventType.EDGE_IDENTITY, diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index 2abf6c67..080d4302 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -13,12 +13,12 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; -import com.adobe.marketing.mobile.services.DataStoring; import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.DataReader; @@ -51,17 +51,10 @@ class IdentityState { this.identityProperties = (persistedProperties != null) ? persistedProperties : new IdentityProperties(); } - /** - * @return the current bootup status - */ - @VisibleForTesting - boolean hasBooted() { - return hasBooted; - } - /** * @return The current {@link IdentityProperties} for this identity state */ + @NonNull IdentityProperties getIdentityProperties() { return identityProperties; } diff --git a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java b/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java index ee8f58e2..59a97a99 100644 --- a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java +++ b/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java @@ -11,12 +11,10 @@ package com.adobe.marketing.mobile.edge.identity; -import com.adobe.marketing.mobile.AdobeCallback; -import com.adobe.marketing.mobile.AdobeCallbackWithError; -import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.JSONUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -29,8 +27,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -39,102 +35,6 @@ */ class IdentityTestUtil { - /** - * Method to serialize jsonObject to Map. - * - * @param jsonObject the {@link JSONObject} to be serialized - * @return a {@link Map} representing the serialized JSONObject - */ - - static Map toMap(final JSONObject jsonObject) { - if (jsonObject == null) { - return null; - } - - Map jsonAsMap = new HashMap(); - Iterator keysIterator = jsonObject.keys(); - - while (keysIterator.hasNext()) { - String nextKey = keysIterator.next(); - Object value = null; - Object returnValue; - - try { - value = jsonObject.get(nextKey); - } catch (JSONException e) { - MobileCore.log( - LoggingMode.DEBUG, - "Functional Test", - "toMap - Unable to convert jsonObject to Map for key " + nextKey + ", skipping." - ); - } - - if (value == null) { - continue; - } - - if (value instanceof JSONObject) { - returnValue = toMap((JSONObject) value); - } else if (value instanceof JSONArray) { - returnValue = toList((JSONArray) value); - } else { - returnValue = value; - } - - jsonAsMap.put(nextKey, returnValue); - } - - return jsonAsMap; - } - - /** - * Converts provided {@link JSONArray} into {@link List} for any number of levels which can be used as event data - * This method is recursive. - * The elements for which the conversion fails will be skipped. - * - * @param jsonArray to be converted - * @return {@link List} containing the elements from the provided json, null if {@code jsonArray} is null - */ - static List toList(final JSONArray jsonArray) { - if (jsonArray == null) { - return null; - } - - List jsonArrayAsList = new ArrayList(); - int size = jsonArray.length(); - - for (int i = 0; i < size; i++) { - Object value = null; - Object returnValue; - - try { - value = jsonArray.get(i); - } catch (JSONException e) { - MobileCore.log( - LoggingMode.DEBUG, - "Functional Test", - "toList - Unable to convert jsonObject to List for index " + i + ", skipping." - ); - } - - if (value == null) { - continue; - } - - if (value instanceof JSONObject) { - returnValue = toMap((JSONObject) value); - } else if (value instanceof JSONArray) { - returnValue = toList((JSONArray) value); - } else { - returnValue = value; - } - - jsonArrayAsList.add(returnValue); - } - - return jsonArrayAsList; - } - /** * Helper method to create IdentityXDM Map using {@link TestItem}s */ @@ -166,7 +66,7 @@ static Map createXDMIdentityMap(TestItem... items) { */ static Event buildRemoveIdentityRequestWithJSONString(final String jsonStr) throws Exception { final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = Utils.toMap(jsonObject); + final Map xdmData = JSONUtils.toMap(jsonObject); return buildRemoveIdentityRequest(xdmData); } @@ -188,7 +88,7 @@ static Event buildRemoveIdentityRequest(final Map map) { */ static Event buildUpdateIdentityRequestJSONString(final String jsonStr) throws Exception { final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = Utils.toMap(jsonObject); + final Map xdmData = JSONUtils.toMap(jsonObject); return buildUpdateIdentityRequest(xdmData); } @@ -214,7 +114,7 @@ static Event buildUpdateIdentityRequest(final Map map) { */ static Map flattenJSONString(final String jsonString) throws JSONException { JSONObject jsonObject = new JSONObject(jsonString); - Map persistenceValueMap = Utils.toMap(jsonObject); + Map persistenceValueMap = JSONUtils.toMap(jsonObject); return flattenMap(persistenceValueMap); } @@ -284,9 +184,9 @@ private static void addKeys(String currentPath, JsonNode jsonNode, Map getIdentitiesSync() { - try { - final HashMap getIdentityResponse = new HashMap<>(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - Identity.getIdentities( - new AdobeCallbackWithError() { - @Override - public void call(final IdentityMap identities) { - getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, identities); - latch.countDown(); - } - - @Override - public void fail(final AdobeError adobeError) { - getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.ERROR, adobeError); - latch.countDown(); - } - } - ); - latch.await(2000, TimeUnit.MILLISECONDS); - - return getIdentityResponse; - } catch (Exception exp) { - return null; - } - } - - static String getExperienceCloudIdSync() { - try { - final HashMap getExperienceCloudIdResponse = new HashMap<>(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - Identity.getExperienceCloudId( - new AdobeCallback() { - @Override - public void call(final String ecid) { - getExperienceCloudIdResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, ecid); - latch.countDown(); - } - } - ); - latch.await(2000, TimeUnit.MILLISECONDS); - return getExperienceCloudIdResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); - } catch (Exception exp) { - return null; - } - } - - static String getUrlVariablesSync() { - try { - final HashMap getUrlVariablesResponse = new HashMap<>(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - Identity.getUrlVariables( - new AdobeCallback() { - @Override - public void call(final String urlVariables) { - getUrlVariablesResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, urlVariables); - latch.countDown(); - } - } - ); - latch.await(2000, TimeUnit.MILLISECONDS); - return getUrlVariablesResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); - } catch (Exception exp) { - return null; - } - } - - static IdentityMap CreateIdentityMap(final String namespace, final String id) { - return CreateIdentityMap(namespace, id, AuthenticatedState.AMBIGUOUS, false); - } - - static IdentityMap CreateIdentityMap( - final String namespace, - final String id, - final AuthenticatedState state, - final boolean isPrimary - ) { - IdentityMap map = new IdentityMap(); - IdentityItem item = new IdentityItem(id, state, isPrimary); - map.addItem(item, namespace); - return map; - } } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java index 6483db01..2dbee4f4 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java @@ -13,7 +13,6 @@ import static junit.framework.Assert.assertTrue; import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNotNull; import static org.junit.Assert.assertNotEquals; @@ -21,9 +20,9 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.junit.Assert; import org.junit.Test; -@SuppressWarnings("unchecked") public class ECIDTests { @Test @@ -112,15 +111,15 @@ public void testECID_equals() { ECID a = new ECID(); ECID b = new ECID(a.toString()); - assertTrue(a.equals(b)); - assertTrue(b.equals(a)); - assertTrue(a.equals(a)); - assertTrue(b.equals(b)); + Assert.assertEquals(a, b); + Assert.assertEquals(b, a); + Assert.assertEquals(a, a); + Assert.assertEquals(b, b); - assertFalse(a.equals(null)); - assertFalse(a.equals(new ECID())); + Assert.assertNotNull(a); + assertNotEquals(a, new ECID()); - assertFalse(a.equals(new NotECID(a.toString()))); + assertNotEquals(a, new NotECID(a.toString())); } private class NotECID { diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java index bd6203b0..95136a19 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java @@ -17,7 +17,9 @@ import static org.junit.Assert.assertTrue; import com.adobe.marketing.mobile.Event; +import java.util.Collections; import java.util.HashMap; +import java.util.Map; import org.junit.Test; public class EventUtilsTests { @@ -289,6 +291,96 @@ public void test_getAdId_whenValid_thenValid() { assertEquals("adId", EventUtils.getAdId(event)); } + // ====================================================================================================================== + // Tests for method : isSharedStateUpdateFor(final String stateOwnerName, final Event event) + // ====================================================================================================================== + + @Test + public void test_isSharedStateUpdateFor_stateOwnerIsNull() { + final Event event = new Event.Builder( + "Shared state event", + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .setEventData(Collections.EMPTY_MAP) + .build(); + + assertFalse(EventUtils.isSharedStateUpdateFor(null, event)); + } + + @Test + public void test_isSharedStateUpdateFor_eventIsNull() { + assertFalse(EventUtils.isSharedStateUpdateFor(IdentityConstants.SharedState.Configuration.NAME, null)); + } + + @Test + public void test_isSharedStateUpdateFor_stateOwnerValueIsNull() { + final Event event = new Event.Builder( + "Shared state event", + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .setEventData(Collections.singletonMap(IdentityConstants.EventDataKeys.STATE_OWNER, null)) + .build(); + + assertFalse(EventUtils.isSharedStateUpdateFor(IdentityConstants.EventType.GENERIC_IDENTITY, event)); + } + + @Test + public void test_isSharedStateUpdateFor_stateOwnerValueIsEmpty() { + final Event event = new Event.Builder( + "Shared state event", + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .setEventData(Collections.singletonMap(IdentityConstants.EventDataKeys.STATE_OWNER, "")) + .build(); + + assertFalse(EventUtils.isSharedStateUpdateFor(IdentityConstants.EXTENSION_NAME, event)); + } + + @Test + public void test_isSharedStateUpdateFor_stateOwnerValueExists() { + final Event event = new Event.Builder( + "Shared state event", + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY + ) + .setEventData( + Collections.singletonMap(IdentityConstants.EventDataKeys.STATE_OWNER, "com.adobe.module.configuration") + ) + .build(); + + assertTrue(EventUtils.isSharedStateUpdateFor("com.adobe.module.configuration", event)); + assertFalse(EventUtils.isSharedStateUpdateFor(IdentityConstants.EXTENSION_NAME, event)); + } + + // ====================================================================================================================== + // Tests for method : getECID(final Map identityDirectSharedState) + // ====================================================================================================================== + @Test + public void test_getECID_valueExists() { + final Map identityDirectState = Collections.singletonMap( + IdentityConstants.SharedState.IdentityDirect.ECID, + "SomeRandomECID" + ); + assertEquals("SomeRandomECID", EventUtils.getECID(identityDirectState).toString()); + } + + @Test + public void test_getECID_valueDoesNotExist() { + assertNull(EventUtils.getECID(Collections.EMPTY_MAP)); + } + + @Test + public void test_getECID_valueIsNull() { + final Map identityDirectState = Collections.singletonMap( + IdentityConstants.SharedState.IdentityDirect.ECID, + null + ); + assertNull(EventUtils.getECID(identityDirectState)); + } + // Test helpers /** 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 c156d2de..2e313f0b 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 @@ -12,43 +12,36 @@ package com.adobe.marketing.mobile.edge.identity; import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.*; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.buildUpdateIdentityRequest; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; 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; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.powermock.api.mockito.PowerMockito.when; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionErrorCallback; -import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.SharedStateResolution; +import com.adobe.marketing.mobile.SharedStateResult; +import com.adobe.marketing.mobile.SharedStateStatus; +import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.json.JSONObject; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; -@RunWith(PowerMockRunner.class) -@PrepareForTest({ Event.class, MobileCore.class, ExtensionApi.class, IdentityState.class }) public class IdentityExtensionTests { private IdentityExtension extension; @@ -57,110 +50,61 @@ public class IdentityExtensionTests { ExtensionApi mockExtensionApi; @Mock - Application mockApplication; - - @Mock - Context mockContext; - - @Mock - SharedPreferences mockSharedPreference; - - @Mock - SharedPreferences.Editor mockSharedPreferenceEditor; + IdentityState mockIdentityState; @Before public void setup() { - PowerMockito.mockStatic(MobileCore.class); - - Mockito.when(MobileCore.getApplication()).thenReturn(mockApplication); - Mockito.when(mockApplication.getApplicationContext()).thenReturn(mockContext); - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito.when(mockSharedPreference.edit()).thenReturn(mockSharedPreferenceEditor); - - extension = new IdentityExtension(mockExtensionApi); - - // simulate bootup - extension.bootupIfReady(); - verify(mockExtensionApi, times(1)) - .setXDMSharedEventState(any(Map.class), nullable(Event.class), any(ExtensionErrorCallback.class)); - clearInvocations(mockExtensionApi); + MockitoAnnotations.openMocks(this); } // ======================================================================================== - // constructor + // onRegistered // ======================================================================================== @Test - public void test_ListenersRegistration() { - // setup - final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass( - ExtensionErrorCallback.class - ); - + public void test_onRegistered_registersListeners() { // test extension = new IdentityExtension(mockExtensionApi); + extension.onRegistered(); - // verify 2 listeners are registered - verify(mockExtensionApi, times(7)) - .registerEventListener(anyString(), anyString(), any(Class.class), any(ExtensionErrorCallback.class)); - - // verify listeners are registered with correct event source and type - verify(mockExtensionApi, times(1)) + // verify + verify(mockExtensionApi) .registerEventListener( eq(IdentityConstants.EventType.GENERIC_IDENTITY), eq(IdentityConstants.EventSource.REQUEST_CONTENT), - eq(ListenerIdentityRequestContent.class), - callbackCaptor.capture() + any() ); - verify(mockExtensionApi, times(1)) + verify(mockExtensionApi) + .registerEventListener( + eq(IdentityConstants.EventType.GENERIC_IDENTITY), + eq(IdentityConstants.EventSource.REQUEST_RESET), + any() + ); + verify(mockExtensionApi) .registerEventListener( eq(IdentityConstants.EventType.EDGE_IDENTITY), eq(IdentityConstants.EventSource.REQUEST_IDENTITY), - eq(ListenerEdgeIdentityRequestIdentity.class), - callbackCaptor.capture() + any() ); - verify(mockExtensionApi, times(1)) + verify(mockExtensionApi) .registerEventListener( eq(IdentityConstants.EventType.EDGE_IDENTITY), eq(IdentityConstants.EventSource.UPDATE_IDENTITY), - eq(ListenerEdgeIdentityUpdateIdentity.class), - callbackCaptor.capture() + any() ); - verify(mockExtensionApi, times(1)) + verify(mockExtensionApi) .registerEventListener( eq(IdentityConstants.EventType.EDGE_IDENTITY), eq(IdentityConstants.EventSource.REMOVE_IDENTITY), - eq(ListenerEdgeIdentityRemoveIdentity.class), - callbackCaptor.capture() - ); - verify(mockExtensionApi, times(1)) - .registerEventListener( - eq(IdentityConstants.EventType.GENERIC_IDENTITY), - eq(IdentityConstants.EventSource.REQUEST_RESET), - eq(ListenerIdentityRequestReset.class), - callbackCaptor.capture() + any() ); - verify(mockExtensionApi, times(1)) + verify(mockExtensionApi) .registerEventListener( eq(IdentityConstants.EventType.HUB), eq(IdentityConstants.EventSource.SHARED_STATE), - eq(ListenerHubSharedState.class), - callbackCaptor.capture() - ); - verify(mockExtensionApi, times(1)) - .registerEventListener( - eq(IdentityConstants.EventType.HUB), - eq(IdentityConstants.EventSource.BOOTED), - eq(ListenerEventHubBoot.class), - callbackCaptor.capture() + any() ); - // verify the callback - ExtensionErrorCallback extensionErrorCallback = callbackCaptor.getValue(); - Assert.assertNotNull("The extension callback should not be null", extensionErrorCallback); - // TODO - enable when ExtensionError creation is available - //extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); + verifyNoMoreInteractions(mockExtensionApi); } // ======================================================================================== @@ -169,7 +113,8 @@ public void test_ListenersRegistration() { @Test public void test_getName() { // test - String moduleName = extension.getName(); + extension = new IdentityExtension(mockExtensionApi); + final String moduleName = extension.getName(); assertEquals("getName should return the correct module name", IdentityConstants.EXTENSION_NAME, moduleName); } @@ -179,7 +124,8 @@ public void test_getName() { @Test public void test_getVersion() { // test - String moduleVersion = extension.getVersion(); + extension = new IdentityExtension(mockExtensionApi); + final String moduleVersion = extension.getVersion(); assertEquals( "getVersion should return the correct module version", IdentityConstants.EXTENSION_VERSION, @@ -188,246 +134,282 @@ public void test_getVersion() { } // ======================================================================================== - // handleIdentityRequest + // readyForEvent(Event event) // ======================================================================================== - @Test - public void test_handleIdentityRequest_nullEvent_shouldNotThrow() { - // test - extension.handleIdentityRequest(null); + public void test_readyForEvent_cannotBoot() { + // setup + when(mockIdentityState.bootupIfReady(any())).thenReturn(false); + + // test and verify + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + assertFalse(extension.readyForEvent(buildUpdateIdentityRequest(Collections.EMPTY_MAP))); } @Test - public void test_handleIdentityRequest_generatesNewECID() { + public void test_readyForEvent_GetUrlVariablesRequestButConfigurationStateUnavailable() { // setup + when(mockIdentityState.bootupIfReady(any())).thenReturn(true); Event event = new Event.Builder( "Test event", IdentityConstants.EventType.EDGE_IDENTITY, IdentityConstants.EventSource.REQUEST_IDENTITY ) + .setEventData( + new HashMap() { + { + put("urlvariables", true); + } + } + ) .build(); - final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); - final ArgumentCaptor requestEventCaptor = ArgumentCaptor.forClass(Event.class); - - // test - extension.handleIdentityRequest(event); - - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchResponseEvent( - responseEventCaptor.capture(), - requestEventCaptor.capture(), - any(ExtensionErrorCallback.class) - ); - // verify response event containing ECID is dispatched - Event ecidResponseEvent = responseEventCaptor.getAllValues().get(0); - final IdentityMap identityMap = IdentityMap.fromXDMMap(ecidResponseEvent.getEventData()); - final String ecid = identityMap.getIdentityItemsForNamespace("ECID").get(0).getId(); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(null); - assertNotNull(ecid); - assertTrue(ecid.length() > 0); + // test and verify + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + assertFalse(extension.readyForEvent(event)); } @Test - public void test_handleIdentityRequest_loadsPersistedECID() { + public void test_readyForEvent_GetUrlVariablesRequest_ConfigurationStatePending() { // setup - ECID existingECID = new ECID(); - IdentityProperties persistedProps = new IdentityProperties(); - persistedProps.setECID(existingECID); - PowerMockito.stub(PowerMockito.method(IdentityState.class, "getIdentityProperties")).toReturn(persistedProps); - + when(mockIdentityState.bootupIfReady(any())).thenReturn(true); Event event = new Event.Builder( "Test event", IdentityConstants.EventType.EDGE_IDENTITY, IdentityConstants.EventSource.REQUEST_IDENTITY ) + .setEventData( + new HashMap() { + { + put("urlvariables", true); + } + } + ) .build(); - final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); - final ArgumentCaptor requestEventCaptor = ArgumentCaptor.forClass(Event.class); - // test - extension = new IdentityExtension(mockExtensionApi); - extension.handleIdentityRequest(event); - - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchResponseEvent( - responseEventCaptor.capture(), - requestEventCaptor.capture(), - any(ExtensionErrorCallback.class) - ); - - // verify response event containing ECID is dispatched - Event ecidResponseEvent = responseEventCaptor.getAllValues().get(0); - final IdentityMap identityMap = IdentityMap.fromXDMMap(ecidResponseEvent.getEventData()); - final String ecid = identityMap.getIdentityItemsForNamespace("ECID").get(0).getId(); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(new SharedStateResult(SharedStateStatus.PENDING, null)); - assertEquals(existingECID.toString(), ecid); + // test and verify + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + assertFalse(extension.readyForEvent(event)); } @Test - public void test_handleIdentityRequest_noIdentifiers_emptyXDMIdentityMap() { - // Setup - IdentityProperties emptyProps = new IdentityProperties(); - PowerMockito.stub(PowerMockito.method(IdentityState.class, "getIdentityProperties")).toReturn(emptyProps); - + public void test_readyForEvent_GetUrlVariablesRequest_ConfigurationStateSet() { + // setup + when(mockIdentityState.bootupIfReady(any())).thenReturn(true); Event event = new Event.Builder( "Test event", IdentityConstants.EventType.EDGE_IDENTITY, IdentityConstants.EventSource.REQUEST_IDENTITY ) + .setEventData( + new HashMap() { + { + put("urlvariables", true); + } + } + ) .build(); - final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); - final ArgumentCaptor requestEventCaptor = ArgumentCaptor.forClass(Event.class); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, Collections.EMPTY_MAP)); - // Test - extension.handleIdentityRequest(event); - - // Verify event dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchResponseEvent( - responseEventCaptor.capture(), - requestEventCaptor.capture(), - any(ExtensionErrorCallback.class) - ); + // test and verify + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + assertTrue(extension.readyForEvent(event)); + } - // Verify response event containing ECID is dispatched - Event ecidResponseEvent = responseEventCaptor.getAllValues().get(0); - final Map xdmData = ecidResponseEvent.getEventData(); - final Map identityMap = (Map) xdmData.get("identityMap"); + @Test + public void test_readyForEvent_OtherEvent_canBoot() { + // setup + when(mockIdentityState.bootupIfReady(any())).thenReturn(true); - assertTrue(identityMap.isEmpty()); + // test and verify + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + assertTrue(extension.readyForEvent(buildUpdateIdentityRequest(Collections.EMPTY_MAP))); } + // ======================================================================================== + // handleRequestIdentity + // ======================================================================================== + @Test - public void test_handleIdentityResetRequest() { + public void test_handleRequestIdentity_DispatchesResponseEvent_PersistedECIDExists() { // setup + final IdentityProperties properties = new IdentityProperties(); + properties.setECID(new ECID()); // simulate persisted ECID + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + Event event = new Event.Builder( "Test event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_RESET + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY ) .build(); - final ArgumentCaptor sharedStateCaptor = ArgumentCaptor.forClass(Map.class); - final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); // test - extension.handleRequestReset(event); + extension.handleRequestIdentity(event); - // verify - verify(mockExtensionApi, times(1)) - .setXDMSharedEventState(sharedStateCaptor.capture(), eq(event), any(ExtensionErrorCallback.class)); - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEvent(eventCaptor.capture(), any(ExtensionErrorCallback.class)); + // verify that an event is dispatched + verify(mockExtensionApi).dispatch(responseEventCaptor.capture()); - Map sharedState = flattenMap(sharedStateCaptor.getValue()); - assertTrue(sharedState.get("identityMap.ECID[0].id").length() > 0); + // verify that it is a response event to the incident event + final Event capturedResponseEvent = responseEventCaptor.getValue(); + assertEquals(capturedResponseEvent.getResponseID(), event.getUniqueIdentifier()); + + // verify that the data sent in the response event contains the ecid from properties + final Event ecidResponseEvent = responseEventCaptor.getAllValues().get(0); + final IdentityMap identityMap = IdentityMap.fromXDMMap(ecidResponseEvent.getEventData()); + assertNotNull(identityMap); + final String ecid = identityMap.getIdentityItemsForNamespace("ECID").get(0).getId(); + assertEquals(ecid, properties.getECID().toString()); } - @Test - public void test_handleHubSharedState_updateLegacyEcidOnDirectIdentityStateChange() { - final ECID existingECID = new ECID(); - setupExistingIdentityProps(existingECID); - setIdentityDirectSharedState("1234"); + // ======================================================================================== + // handleIdentityDirectECIDUpdate + // ======================================================================================== - Event event = new Event.Builder( - "Test event", + @Test + public void test_handleIdentityDirectECIDUpdate_notAnIdentityDirectStateUpdate() { + final Event event = new Event.Builder( + "Not an IdentityDirect State event", IdentityConstants.EventType.HUB, IdentityConstants.EventSource.SHARED_STATE ) .setEventData( new HashMap() { { - put( - IdentityConstants.EventDataKeys.STATE_OWNER, - IdentityConstants.SharedState.IdentityDirect.NAME - ); + put(IdentityConstants.EventDataKeys.STATE_OWNER, "Some.Other.Extension.Name"); } } ) .build(); - extension.handleHubSharedState(event); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + extension.handleIdentityDirectECIDUpdate(event); - final ArgumentCaptor> sharedStateCaptor = ArgumentCaptor.forClass(Map.class); - verify(mockExtensionApi, times(1)) - .setXDMSharedEventState(sharedStateCaptor.capture(), eq(event), any(ExtensionErrorCallback.class)); - Map sharedState = sharedStateCaptor.getValue(); - assertEquals("1234", flattenMap(sharedState).get("identityMap.ECID[1].id")); // Legacy ECID is set as a secondary ECID - - final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - // verify no event dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); - MobileCore.dispatchEvent(eventCaptor.capture(), any(ExtensionErrorCallback.class)); - assertTrue(eventCaptor.getAllValues().isEmpty()); + verify(mockExtensionApi, never()).getSharedState(any(), any(), anyBoolean(), any()); + verify(mockIdentityState, never()).updateLegacyExperienceCloudId(any()); + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); } @Test - public void test_handleHubSharedState_noOpNullEvent() { - setIdentityDirectSharedState("1234"); - - extension.handleHubSharedState(null); - - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); - } - - @Test - public void test_handleHubSharedState_noOpNullEventData() { - setIdentityDirectSharedState("1234"); - - Event event = new Event.Builder( - "Test event", + public void test_handleIdentityDirectECIDUpdate_identityDirectStateResultIsNull() { + final Event event = new Event.Builder( + "IdentityDirect State event", IdentityConstants.EventType.HUB, IdentityConstants.EventSource.SHARED_STATE ) + .setEventData( + new HashMap() { + { + put( + IdentityConstants.EventDataKeys.STATE_OWNER, + IdentityConstants.SharedState.IdentityDirect.NAME + ); + } + } + ) .build(); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(null); - extension.handleHubSharedState(event); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + extension.handleIdentityDirectECIDUpdate(event); - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); + verify(mockExtensionApi) + .getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ); + verify(mockIdentityState, never()).updateLegacyExperienceCloudId(any()); + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); } @Test - public void test_handleHubSharedState_noOpNotDirectIdentityStateChange() { - setIdentityDirectSharedState("1234"); - - Event event = new Event.Builder( - "Test event", + public void test_handleIdentityDirectECIDUpdate_identityDirectStateIsNull() { + final Event event = new Event.Builder( + "IdentityDirect State event", IdentityConstants.EventType.HUB, IdentityConstants.EventSource.SHARED_STATE ) .setEventData( new HashMap() { { - put(IdentityConstants.EventDataKeys.STATE_OWNER, "com.adobe.module.configuration"); + put( + IdentityConstants.EventDataKeys.STATE_OWNER, + IdentityConstants.SharedState.IdentityDirect.NAME + ); } } ) .build(); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, null)); - extension.handleHubSharedState(event); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + extension.handleIdentityDirectECIDUpdate(event); - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); + verify(mockExtensionApi) + .getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ); + verify(mockIdentityState, never()).updateLegacyExperienceCloudId(any()); + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); } @Test - public void test_handleHubSharedState_noOpNoDirectIdentitySharedState() { - when( - mockExtensionApi.getSharedEventState( - eq(IdentityConstants.SharedState.IdentityDirect.NAME), - any(Event.class), - any(ExtensionErrorCallback.class) - ) - ) - .thenReturn(null); - - Event event = new Event.Builder( - "Test event", + public void test_handleIdentityDirectECIDUpdate_legacyEcidUpdateFailed() { + final Event event = new Event.Builder( + "IdentityDirect State event", IdentityConstants.EventType.HUB, IdentityConstants.EventSource.SHARED_STATE ) @@ -443,18 +425,40 @@ public void test_handleHubSharedState_noOpNoDirectIdentitySharedState() { ) .build(); - extension.handleHubSharedState(event); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn( + new SharedStateResult( + SharedStateStatus.SET, + Collections.singletonMap(IdentityConstants.SharedState.IdentityDirect.ECID, null) + ) + ); + when(mockIdentityState.updateLegacyExperienceCloudId(any())).thenReturn(false); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + extension.handleIdentityDirectECIDUpdate(event); - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); + verify(mockExtensionApi) + .getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ); + verify(mockIdentityState).updateLegacyExperienceCloudId(null); + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); } @Test - public void test_handleHubSharedState_doesNotShareStateIfLegacyECIDDoesNotChange() { - setIdentityDirectSharedState("1234"); - - Event event = new Event.Builder( - "Test event", + public void test_handleIdentityDirectECIDUpdate_identityDirectStateValidECID() { + final Event event = new Event.Builder( + "IdentityDirect State event", IdentityConstants.EventType.HUB, IdentityConstants.EventSource.SHARED_STATE ) @@ -470,14 +474,45 @@ public void test_handleHubSharedState_doesNotShareStateIfLegacyECIDDoesNotChange ) .build(); - // IdentityState.updateLegacyExperienceCloudId returns false if Legacy ECID was not updated - PowerMockito.stub(PowerMockito.method(IdentityState.class, "updateLegacyExperienceCloudId")).toReturn(false); + final ECID legacyEcid = new ECID(); - extension.handleHubSharedState(event); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn( + new SharedStateResult( + SharedStateStatus.SET, + new HashMap() { + { + { + put(IdentityConstants.SharedState.IdentityDirect.ECID, legacyEcid.toString()); + } + } + } + ) + ); + when(mockIdentityState.updateLegacyExperienceCloudId(any())).thenReturn(true); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + when(mockIdentityState.getIdentityProperties()).thenReturn(new IdentityProperties()); + + extension.handleIdentityDirectECIDUpdate(event); - final ArgumentCaptor> sharedStateCaptor = ArgumentCaptor.forClass(Map.class); - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(sharedStateCaptor.capture(), eq(event), any(ExtensionErrorCallback.class)); + verify(mockExtensionApi) + .getSharedState( + IdentityConstants.SharedState.IdentityDirect.NAME, + event, + false, + SharedStateResolution.LAST_SET + ); + verify(mockIdentityState).updateLegacyExperienceCloudId(legacyEcid); + verify(mockExtensionApi) + .createXDMSharedState(mockIdentityState.getIdentityProperties().toXDMData(false), event); } // ======================================================================================== @@ -485,13 +520,7 @@ public void test_handleHubSharedState_doesNotShareStateIfLegacyECIDDoesNotChange // ======================================================================================== @Test - public void test_handleUrlVariablesRequest_nullEvent_shouldNotThrow() { - // test - extension.handleUrlVariablesRequest(null); - } - - @Test - public void test_handleUrlVariablesRequest_whenConfigAndECIDNotPresent_returnsNull() { + public void test_handleUrlVariablesRequest_whenOrgIdAndECIDNotPresent_returnsNull() { // setup Event event = new Event.Builder( "Test event", @@ -506,34 +535,42 @@ public void test_handleUrlVariablesRequest_whenConfigAndECIDNotPresent_returnsNu } ) .build(); + // Simulate null config state + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(null); + + final IdentityProperties properties = new IdentityProperties(); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); // test extension.handleUrlVariablesRequest(event); - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchResponseEvent( - responseEventCaptor.capture(), - any(Event.class), - any(ExtensionErrorCallback.class) - ); + // verify that response event is dispatched + verify(mockExtensionApi).dispatch(responseEventCaptor.capture()); - Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); - final Map data = urlVariablesResponseEvent.getEventData(); + // verify that the response event contains "urlvariables" property + final Event capturedResponseEvent = responseEventCaptor.getValue(); + final Map data = capturedResponseEvent.getEventData(); assertTrue(data.containsKey("urlvariables")); - String urlvariables = (String) data.get("urlvariables"); + // verify that the response event data has no values for "urlvariables" key + final String urlvariables = (String) data.get("urlvariables"); assertNull(urlvariables); } @Test - public void test_handleUrlVariablesRequest_whenOrgIdAndECIDPresent_returnsValidUrlVariablesString() { + public void test_handleUrlVariablesRequest_whenOrgIdPresentAndECIDNotPresent_returnsNull() { // 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, @@ -547,38 +584,53 @@ public void test_handleUrlVariablesRequest_whenOrgIdAndECIDPresent_returnsValidU } ) .build(); + + // Simulate valid config state + final SharedStateResult configSharedStateResult = new SharedStateResult( + SharedStateStatus.SET, + Collections.singletonMap( + IdentityConstants.SharedState.Configuration.EXPERIENCE_CLOUD_ORGID, + "SomeOrgId@AdobeOrg" + ) + ); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(configSharedStateResult); + + final IdentityProperties properties = new IdentityProperties(); // Simulate Absent ECID + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); // test extension.handleUrlVariablesRequest(event); - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchResponseEvent( - responseEventCaptor.capture(), - any(Event.class), - any(ExtensionErrorCallback.class) - ); + // verify that response event is dispatched + verify(mockExtensionApi).dispatch(responseEventCaptor.capture()); + final Event capturedResponseEvent = responseEventCaptor.getValue(); - Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); - final Map data = urlVariablesResponseEvent.getEventData(); - String urlvariables = (String) data.get("urlvariables"); + // verify that the response event is sent to the incident event + assertEquals(event.getUniqueIdentifier(), capturedResponseEvent.getResponseID()); - String expectedUrlVariableTSString = "adobe_mc=TS%3"; - String expectedUrlVariableIdentifiersString = "%7CMCMID%3D" + testECID + "%7CMCORGID%3Dtest-org-id%40AdobeOrg"; + // verify that the response event contains "urlvariables" property + final Map data = capturedResponseEvent.getEventData(); + assertTrue(data.containsKey("urlvariables")); - assertNotNull(urlvariables); - assertTrue(urlvariables.contains("adobe_mc=")); - assertTrue(urlvariables.contains(expectedUrlVariableTSString)); - assertTrue(urlvariables.contains(expectedUrlVariableIdentifiersString)); + // verify that the response event data has no values for "urlvariables" key + final String urlvariables = (String) data.get("urlvariables"); + assertNull(urlvariables); } @Test - public void test_handleUrlVariablesRequest_whenOrgIdMissing_returnsValidNull() { + public void test_handleUrlVariablesRequest_whenOrgIdAndECIDPresent_returnsValidUrlVariables() { // setup - ECID testECID = new ECID(); - extension.state.getIdentityProperties().setECID(testECID); - Event event = new Event.Builder( "Test event", IdentityConstants.EventType.EDGE_IDENTITY, @@ -592,63 +644,55 @@ public void test_handleUrlVariablesRequest_whenOrgIdMissing_returnsValidNull() { } ) .build(); - final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); - // test - extension.handleUrlVariablesRequest(event); - - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchResponseEvent( - responseEventCaptor.capture(), - any(Event.class), - any(ExtensionErrorCallback.class) + // Simulate valid config state + final SharedStateResult configSharedStateResult = new SharedStateResult( + SharedStateStatus.SET, + Collections.singletonMap( + IdentityConstants.SharedState.Configuration.EXPERIENCE_CLOUD_ORGID, + "SomeOrgId@AdobeOrg" + ) ); + when( + mockExtensionApi.getSharedState( + IdentityConstants.SharedState.Configuration.NAME, + event, + false, + SharedStateResolution.LAST_SET + ) + ) + .thenReturn(configSharedStateResult); - Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); - final Map data = urlVariablesResponseEvent.getEventData(); - String urlvariables = (String) data.get("urlvariables"); + final IdentityProperties properties = new IdentityProperties(); + properties.setECID(new ECID()); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); - assertNull(urlvariables); - } + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); - @Test - public void test_handleUrlVariablesRequest_whenECIDMissing_returnsValidNull() { - // setup - extension.state.getIdentityProperties().setECID(null); - setConfigurationSharedState("test-org-id@AdobeOrg"); + // test + extension.handleUrlVariablesRequest(event); - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .setEventData( - new HashMap() { - { - put("urlvariables", true); - } - } - ) - .build(); + // verify that response event is dispatched final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); + verify(mockExtensionApi).dispatch(responseEventCaptor.capture()); + final Event capturedResponseEvent = responseEventCaptor.getValue(); - // test - extension.handleUrlVariablesRequest(event); + // verify that the response event is sent to the incident event + assertEquals(event.getUniqueIdentifier(), capturedResponseEvent.getResponseID()); - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchResponseEvent( - responseEventCaptor.capture(), - any(Event.class), - any(ExtensionErrorCallback.class) - ); + // verify that the response event contains "urlvariables" property + final Map data = capturedResponseEvent.getEventData(); + assertTrue(data.containsKey("urlvariables")); - Event urlVariablesResponseEvent = responseEventCaptor.getAllValues().get(0); - final Map data = urlVariablesResponseEvent.getEventData(); - String urlvariables = (String) data.get("urlvariables"); + final String urlvariables = (String) data.get("urlvariables"); + final String expectedUrlVariableTSString = "adobe_mc=TS%3"; + final String expectedUrlVariableIdentifiersString = + "%7CMCMID%3D" + properties.getECID() + "%7CMCORGID%3DSomeOrgId%40AdobeOrg"; - assertNull(urlvariables); + assertNotNull(urlvariables); + assertTrue(urlvariables.contains("adobe_mc=")); + assertTrue(urlvariables.contains(expectedUrlVariableTSString)); + assertTrue(urlvariables.contains(expectedUrlVariableIdentifiersString)); } // ======================================================================================== @@ -658,42 +702,91 @@ public void test_handleUrlVariablesRequest_whenECIDMissing_returnsValidNull() { @Test public void test_handleUpdateIdentities_whenValidData_updatesCustomerIdentifiers_updatesSharedState() { // setup - MockIdentityState mockIdentityState = new MockIdentityState(new IdentityProperties()); - extension.state = mockIdentityState; + final IdentityProperties properties = new IdentityProperties(); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + + // simulate an update to the identity properties + doAnswer(invocation -> { + final IdentityMap arg = (IdentityMap) invocation.getArgument(0); + properties.updateCustomerIdentifiers(arg); + return null; + }) + .when(mockIdentityState) + .updateCustomerIdentifiers(any()); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test - Map identityXDM = createXDMIdentityMap( + final Map identityXDM = createXDMIdentityMap( new TestItem("id1", "somevalue"), new TestItem("id2", "othervalue") ); - Event updateIdentityEvent = buildUpdateIdentityRequest(identityXDM); + final Event updateIdentityEvent = new Event.Builder( + "Update Identity Event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.UPDATE_IDENTITY + ) + .setEventData(identityXDM) + .build(); extension.handleUpdateIdentities(updateIdentityEvent); // verify identifiers updated - assertEquals(1, mockIdentityState.updateCustomerIdentifiersCalledTimes); - assertEquals(identityXDM, mockIdentityState.updateCustomerIdentifiersParams.get(0).asXDMMap()); + final ArgumentCaptor identityMapCaptor = ArgumentCaptor.forClass(IdentityMap.class); + verify(mockIdentityState).updateCustomerIdentifiers(identityMapCaptor.capture()); + assertEquals(identityXDM, identityMapCaptor.getValue().asXDMMap()); - // verify no event dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); - MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); + verify(mockExtensionApi).createXDMSharedState(properties.toXDMData(false), updateIdentityEvent); + verify(mockExtensionApi, never()).dispatch(any()); } @Test public void test_handleUpdateIdentities_nullEventData_returns() { // setup - MockIdentityState mockIdentityState = new MockIdentityState(new IdentityProperties()); - extension.state = mockIdentityState; + final IdentityProperties properties = new IdentityProperties(); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test - Event updateIdentityEvent = buildUpdateIdentityRequest(null); + final Event updateIdentityEvent = new Event.Builder( + "Update Identity Event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.UPDATE_IDENTITY + ) // no event data + .build(); extension.handleUpdateIdentities(updateIdentityEvent); - // verify identifiers not updated - assertEquals(0, mockIdentityState.updateCustomerIdentifiersCalledTimes); + // verify that identifiers are not updated + verify(mockIdentityState, never()).updateCustomerIdentifiers(any()); + // verify that no shared state is created + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); + // verify that no event is dispatched + verify(mockExtensionApi, never()).dispatch(any()); + } - // verify shared state - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); + @Test + public void test_handleUpdateIdentities_EmptyEventData_returns() { + // setup + final IdentityProperties properties = new IdentityProperties(); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + + // test + final Event updateIdentityEvent = new Event.Builder( + "Update Identity Event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.UPDATE_IDENTITY + ) + .setEventData(new HashMap<>()) + .build(); + extension.handleUpdateIdentities(updateIdentityEvent); + + // verify that identifiers are not updated + verify(mockIdentityState, never()).updateCustomerIdentifiers(any()); + // verify that no shared state is created + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); + // verify that no event is dispatched + verify(mockExtensionApi, never()).dispatch(any()); } // ======================================================================================== @@ -706,126 +799,79 @@ public void test_handleRemoveIdentity_whenValidData_removesCustomerIdentifiers_u new TestItem("UserId", "secretID"), new TestItem("PushId", "token") ); - MockIdentityState mockIdentityState = new MockIdentityState(new IdentityProperties(identityXDM)); - extension.state = mockIdentityState; + final IdentityProperties properties = new IdentityProperties(identityXDM); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + final IdentityMap args = (IdentityMap) invocation.getArgument(0); + properties.removeCustomerIdentifiers(args); + return null; + } + } + ) + .when(mockIdentityState) + .removeCustomerIdentifiers(any()); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test - Map removedIdentityXDM = createXDMIdentityMap(new TestItem("UserId", "secretID")); - Event removeIdentityEvent = buildRemoveIdentityRequest(removedIdentityXDM); + final Map removedIdentityXDM = createXDMIdentityMap(new TestItem("UserId", "secretID")); + final Event removeIdentityEvent = buildRemoveIdentityRequest(removedIdentityXDM); extension.handleRemoveIdentity(removeIdentityEvent); // verify identifiers removed - assertEquals(1, mockIdentityState.removeCustomerIdentifiersCalledTimes); - assertEquals(removedIdentityXDM, mockIdentityState.removeCustomerIdentifiersParams.get(0).asXDMMap()); + final ArgumentCaptor removedIdentityMapCaptor = ArgumentCaptor.forClass(IdentityMap.class); + verify(mockIdentityState).removeCustomerIdentifiers(removedIdentityMapCaptor.capture()); + assertEquals( + IdentityMap.fromXDMMap(removedIdentityXDM).toString(), + removedIdentityMapCaptor.getValue().toString() + ); // verify shared state - final ArgumentCaptor sharedStateCaptor = ArgumentCaptor.forClass(Map.class); - verify(mockExtensionApi, times(1)) - .setXDMSharedEventState( - sharedStateCaptor.capture(), - eq(removeIdentityEvent), - any(ExtensionErrorCallback.class) - ); - - // verify no event dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); - MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); + final Map expectedState = properties.toXDMData(false); + final ArgumentCaptor> stateCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockExtensionApi).createXDMSharedState(stateCaptor.capture(), eq(removeIdentityEvent)); + assertEquals(expectedState, stateCaptor.getValue()); } @Test public void test_handleRemoveIdentity_whenNullData_returns() { // setup - Map identityXDM = createXDMIdentityMap(new TestItem("PushId", "token")); - MockIdentityState mockIdentityState = new MockIdentityState(new IdentityProperties(identityXDM)); - extension.state = mockIdentityState; + Map identityXDM = createXDMIdentityMap( + new TestItem("UserId", "secretID"), + new TestItem("PushId", "token") + ); + final IdentityProperties properties = new IdentityProperties(identityXDM); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test - Event removeIdentityEvent = buildRemoveIdentityRequest(null); + final Event removeIdentityEvent = buildRemoveIdentityRequest(null); extension.handleRemoveIdentity(removeIdentityEvent); // verify identifiers not removed - assertEquals(0, mockIdentityState.removeCustomerIdentifiersCalledTimes); - - // verify shared state - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), eq(removeIdentityEvent), any(ExtensionErrorCallback.class)); - } - - @Test - public void test_processCachedEvents_returnsWhenNotBooted() { - Map identityXDM = createXDMIdentityMap(new TestItem("space", "moon")); - MockIdentityState mockIdentityState = new MockIdentityState(new IdentityProperties(identityXDM)); - extension.state = mockIdentityState; - - // test - extension.processAddEvent(buildUpdateIdentityRequest(identityXDM)); - extension.processAddEvent(buildUpdateIdentityRequest(identityXDM)); + verify(mockIdentityState, never()).removeCustomerIdentifiers(any()); - // verify - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); + // verify shared state is never created + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); } @Test - public void test_processAddEvent_nullEvent_returns() { - Map identityXDM = createXDMIdentityMap(new TestItem("space", "moon")); - MockIdentityState mockIdentityState = new MockIdentityState(new IdentityProperties(identityXDM)); - extension.state = mockIdentityState; - mockIdentityState.hasBooted = true; + public void test_handleRemoveIdentity_eventWithEmptyData_returns() { + // setup + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test - extension.processAddEvent(null); + final Event notARemoveIdentityEvent = buildRemoveIdentityRequest(Collections.EMPTY_MAP); + extension.handleRemoveIdentity(notARemoveIdentityEvent); - // verify - verify(mockExtensionApi, times(0)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); - } - - @Test - public void test_processCachedEvents_processesWhenBooted() { - Map identityXDM = createXDMIdentityMap(new TestItem("space", "moon")); - MockIdentityState mockIdentityState = new MockIdentityState(new IdentityProperties(identityXDM)); - mockIdentityState.hasBooted = true; - extension.state = mockIdentityState; - - // test - extension.processAddEvent(buildUpdateIdentityRequest(identityXDM)); - extension.processAddEvent(buildRemoveIdentityRequest(identityXDM)); - extension.processAddEvent( - new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .setEventData( - new HashMap() { - { - put("urlVariables", true); - } - } - ) - .build() - ); - extension.processAddEvent( - new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build() - ); - extension.processAddEvent( - new Event.Builder( - "Test event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_RESET - ) - .build() - ); + // verify identifiers not removed + verify(mockIdentityState, never()).removeCustomerIdentifiers(any()); - // verify - verify(mockExtensionApi, times(3)) - .setXDMSharedEventState(any(Map.class), any(Event.class), any(ExtensionErrorCallback.class)); // request identity does not update shared state + // verify shared state is never created + verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); } // ======================================================================================== @@ -833,7 +879,7 @@ public void test_processCachedEvents_processesWhenBooted() { // ======================================================================================== @Test - public void test_handleRequestContent_processesWhenBooted() { + public void test_handleRequestContent() { // Setup final Event event = new Event.Builder( "Test Ad ID event", @@ -849,77 +895,60 @@ public void test_handleRequestContent_processesWhenBooted() { ) .build(); - final ArgumentCaptor sharedStateCaptor = ArgumentCaptor.forClass(Map.class); - final ArgumentCaptor consentEventCaptor = ArgumentCaptor.forClass(Event.class); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // Test extension.handleRequestContent(event); - // Verify - consent event should be dispatched - verify(mockExtensionApi, times(1)) - .setXDMSharedEventState(sharedStateCaptor.capture(), eq(event), any(ExtensionErrorCallback.class)); - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEvent(consentEventCaptor.capture(), any(ExtensionErrorCallback.class)); - - // Verify shared state - Map sharedState = flattenMap(sharedStateCaptor.getValue()); - assertTrue(sharedState.get("identityMap.GAID[0].id").length() > 0); - assertEquals("adId", sharedState.get("identityMap.GAID[0].id")); - assertEquals("ambiguous", sharedState.get("identityMap.GAID[0].authenticatedState")); - assertEquals("false", sharedState.get("identityMap.GAID[0].primary")); - - // Verify consent event - Event consentEvent = consentEventCaptor.getAllValues().get(0); - Map consentEventData = flattenMap(consentEvent.getEventData()); - assertEquals("GAID", consentEventData.get("consents.adID.idType")); - assertEquals("y", consentEventData.get("consents.adID.val")); + verify(mockIdentityState).updateAdvertisingIdentifier(eq(event), any(SharedStateCallback.class)); } - // ======================================================================================== - // private helper methods - // ======================================================================================== - - private void setupExistingIdentityProps(final ECID ecid) { - IdentityProperties persistedProps = new IdentityProperties(); - persistedProps.setECID(ecid); - final JSONObject jsonObject = new JSONObject(persistedProps.toXDMData(false)); - final String propsJSON = jsonObject.toString(); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) - .thenReturn(propsJSON); - } - - private void setIdentityDirectSharedState(final String ecid) { - when( - mockExtensionApi.getSharedEventState( - eq(IdentityConstants.SharedState.IdentityDirect.NAME), - any(Event.class), - any(ExtensionErrorCallback.class) - ) + @Test + public void test_handleRequestContent_EventIsNotAdIdEvent() { + // Setup + final Event event = new Event.Builder( + "Not an Ad ID event", + IdentityConstants.EventType.GENERIC_IDENTITY, + IdentityConstants.EventSource.REQUEST_CONTENT ) - .thenReturn( + .setEventData( new HashMap() { { - put(IdentityConstants.SharedState.IdentityDirect.ECID, ecid); + put(IdentityConstants.EventDataKeys.URL_VARIABLES, "adId"); } } - ); + ) + .build(); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + + // Test + extension.handleRequestContent(event); + + verify(mockIdentityState, never()).updateAdvertisingIdentifier(eq(event), any(SharedStateCallback.class)); } - private void setConfigurationSharedState(final String orgId) { - when( - mockExtensionApi.getSharedEventState( - eq("com.adobe.module.configuration"), - any(Event.class), - any(ExtensionErrorCallback.class) - ) + // ======================================================================================== + // handleRequestReset + // ======================================================================================== + + @Test + public void test_handleRequestReset() { + final IdentityProperties properties = new IdentityProperties(); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); + + Event resetEvent = new Event.Builder( + "Test event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REQUEST_IDENTITY ) - .thenReturn( - new HashMap() { - { - put("experienceCloud.org", orgId); - } - } - ); + .build(); + + extension.handleRequestReset(resetEvent); + + verify(mockIdentityState).resetIdentifiers(); + verify(mockExtensionApi).createXDMSharedState(properties.toXDMData(false), resetEvent); // will fail because of new ecid } } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java index 0f4efa24..331f48ad 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import com.adobe.marketing.mobile.util.JSONUtils; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -294,7 +295,7 @@ public void test_FromData() throws Exception { "}"; final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = Utils.toMap(jsonObject); + final Map xdmData = JSONUtils.toMap(jsonObject); // test IdentityMap map = IdentityMap.fromXDMMap(xdmData); @@ -331,7 +332,7 @@ public void testFromXDMMap_InvalidNamespace() throws Exception { "}"; final JSONObject jsonObject = new JSONObject(invalidJsonStr); - final Map xdmData = Utils.toMap(jsonObject); + final Map xdmData = JSONUtils.toMap(jsonObject); // test IdentityMap map = IdentityMap.fromXDMMap(xdmData); @@ -359,7 +360,7 @@ public void testFromXDMMap_InvalidItemForNamespace() throws Exception { "}"; final JSONObject jsonObject = new JSONObject(invalidJsonStr); - final Map xdmData = Utils.toMap(jsonObject); + final Map xdmData = JSONUtils.toMap(jsonObject); // test IdentityMap map = IdentityMap.fromXDMMap(xdmData); @@ -376,7 +377,7 @@ public void testFromXDMMap_InvalidIdentityMap() throws Exception { final String invalidJsonStr = "{\"identityMap\": [\"not a map\"]}"; final JSONObject jsonObject = new JSONObject(invalidJsonStr); - final Map xdmData = Utils.toMap(jsonObject); + final Map xdmData = JSONUtils.toMap(jsonObject); // test IdentityMap map = IdentityMap.fromXDMMap(xdmData); diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java index 7302a471..68339f26 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java @@ -334,12 +334,162 @@ public void test_getsetAdId_whenEmpty_thenNull() { // Verify assertNull(advertisingIdentifier); } + // ====================================================================================================================== - // Tests for "updateCustomerIdentifiers" is already covered in "handleUpdateRequest" tests in IdentityExtensionTests + // Tests for updateCustomerIdentifiers() // ====================================================================================================================== + @Test + public void test_updateCustomerIdentifiers_validProperties() { + // Setup + IdentityProperties props = new IdentityProperties(); + props.setECID(new ECID("internalECID")); + + // Test + final Map customerIdentifierUpdate = createXDMIdentityMap( + new IdentityTestUtil.TestItem("UserId", "somevalue") + ); + props.updateCustomerIdentifiers(IdentityMap.fromXDMMap(customerIdentifierUpdate)); + + // Verify + final Map expectedProperties = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("UserId", "somevalue") + ); + assertEquals(expectedProperties, props.toXDMData(false)); + } + + @Test + public void test_updateCustomerIdentifiers_doesNotUpdateReservedNamespace() { + // Setup + IdentityProperties props = new IdentityProperties(); + props.setECID(new ECID("internalECID")); + + // Test + final Map customerIdentifierUpdate = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "somevalue"), + new IdentityTestUtil.TestItem("GAID", "somevalue"), + new IdentityTestUtil.TestItem("IDFA", "somevalue"), + new IdentityTestUtil.TestItem("IdFA", "somevalue"), + new IdentityTestUtil.TestItem("gaid", "somevalue"), + new IdentityTestUtil.TestItem("UserId", "somevalue") + ); + props.updateCustomerIdentifiers(IdentityMap.fromXDMMap(customerIdentifierUpdate)); + + // Verify + final Map expectedProperties = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("UserId", "somevalue") + ); + assertEquals(expectedProperties, props.toXDMData(false)); + } + + @Test + public void test_updateCustomerIdentifiers_storesAllIdentifiersCaseSensitively() { + // Setup + IdentityProperties props = new IdentityProperties(); + props.setECID(new ECID("internalECID")); + + // Test + final Map customerIdentifierUpdate = createXDMIdentityMap( + new IdentityTestUtil.TestItem("caseSensitive", "somevalue"), + new IdentityTestUtil.TestItem("CASESENSITIVE", "SOMEVALUE") + ); + props.updateCustomerIdentifiers(IdentityMap.fromXDMMap(customerIdentifierUpdate)); + + // Verify + final Map expectedProperties = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("caseSensitive", "somevalue"), + new IdentityTestUtil.TestItem("CASESENSITIVE", "SOMEVALUE") + ); + assertEquals(expectedProperties, props.toXDMData(false)); + } + // ====================================================================================================================== - // Tests for "removeCustomerIdentifiers" is already covered in handleRemoveRequest tests in IdentityExtensionTests + // Tests for removeCustomerIdentifiers() // ====================================================================================================================== + @Test + public void test_removeCustomerIdentifiers_removesIdentifiers() { + // Setup + IdentityProperties props = new IdentityProperties(); + props.setECID(new ECID("internalECID")); + final Map customerIdentifierUpdate = createXDMIdentityMap( + new IdentityTestUtil.TestItem("UserId", "secretID"), + new IdentityTestUtil.TestItem("PushId", "token") + ); + props.updateCustomerIdentifiers(IdentityMap.fromXDMMap(customerIdentifierUpdate)); + + // test + final Map removedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("UserId", "secretID") + ); + props.removeCustomerIdentifiers(IdentityMap.fromXDMMap(removedIdentityXDM)); + + // Verify + final Map expectedProperties = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("PushId", "token") + ); + assertEquals(expectedProperties, props.toXDMData(false)); + } + + @Test + public void test_removeCustomerIdentifiers_doesNotRemoveReservedNamespaces() { + // Setup + IdentityProperties props = new IdentityProperties(); + final ECID initialECID = new ECID(); + props.setECID(initialECID); + props.setAdId("initialADID"); + + // test + final Map removedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("GAID", "initialECID"), + new IdentityTestUtil.TestItem("ECID", initialECID.toString()) + ); + props.removeCustomerIdentifiers(IdentityMap.fromXDMMap(removedIdentityXDM)); + + // Verify + IdentityProperties expectedProperties = new IdentityProperties(); + expectedProperties.setECID(initialECID); + expectedProperties.setAdId("initialADID"); + + assertEquals(expectedProperties.toXDMData(false), props.toXDMData(false)); + } + + @Test + public void test_removeCustomerIdentifiers_removesCaseSensitively() { + // Setup + IdentityProperties props = new IdentityProperties(); + props.setECID(new ECID("internalECID")); + final Map customerIdentifierUpdate = createXDMIdentityMap( + new IdentityTestUtil.TestItem("caseSensitive", "somevalue"), + new IdentityTestUtil.TestItem("CASESENSITIVE", "SOMEVALUE") + ); + props.updateCustomerIdentifiers(IdentityMap.fromXDMMap(customerIdentifierUpdate)); + + // check that initial contents are expected + final Map expectedProperties = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("caseSensitive", "somevalue"), + new IdentityTestUtil.TestItem("CASESENSITIVE", "SOMEVALUE") + ); + assertEquals(expectedProperties, props.toXDMData(false)); + + // test + final Map removedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("caseSensitive", "somevalue") + ); + + props.removeCustomerIdentifiers(IdentityMap.fromXDMMap(removedIdentityXDM)); + + // verify + final Map expectedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("CASESENSITIVE", "SOMEVALUE") + ); + + assertEquals(expectedIdentityXDM, props.toXDMData(false)); + } } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java index 2e39d3df..59d19029 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java @@ -12,7 +12,6 @@ package com.adobe.marketing.mobile.edge.identity; import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.createXDMIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.flattenJSONString; import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.flattenMap; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -21,223 +20,230 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.SharedStateResult; +import com.adobe.marketing.mobile.SharedStateStatus; +import com.adobe.marketing.mobile.services.DataStoring; +import com.adobe.marketing.mobile.services.NamedCollection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.json.JSONObject; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +import org.mockito.MockitoAnnotations; -@RunWith(PowerMockRunner.class) -@PrepareForTest({ MobileCore.class }) public class IdentityStateTests { @Mock - Application mockApplication; + private DataStoring mockDataStoreService; @Mock - Context mockContext; + private NamedCollection mockEdgeIdentityNamedCollection; @Mock - SharedPreferences mockSharedPreference; + private NamedCollection mockDirectIdentityNamedCollection; @Mock - SharedPreferences.Editor mockSharedPreferenceEditor; + private IdentityStorageManager mockIdentityStorageManager; + @Mock private SharedStateCallback mockSharedStateCallback; - private Map hubSharedState; - private Map identityDirectSharedState; - private int setXDMSharedEventStateCalledTimes; @Before public void before() throws Exception { - PowerMockito.mockStatic(MobileCore.class); - - Mockito.when(MobileCore.getApplication()).thenReturn(mockApplication); - Mockito.when(mockApplication.getApplicationContext()).thenReturn(mockContext); - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito.when(mockSharedPreference.edit()).thenReturn(mockSharedPreferenceEditor); - - setXDMSharedEventStateCalledTimes = 0; - mockSharedStateCallback = - new SharedStateCallback() { - @Override - public Map getSharedState(final String stateOwner, final Event event) { - if (IdentityConstants.SharedState.Hub.NAME.equals(stateOwner)) { - return hubSharedState; - } else if (IdentityConstants.SharedState.IdentityDirect.NAME.equals(stateOwner)) { - return identityDirectSharedState; - } + MockitoAnnotations.openMocks(this); - return null; - } - - @Override - public boolean setXDMSharedEventState(Map state, Event event) { - setXDMSharedEventStateCalledTimes++; - return true; - } - }; + when(mockDataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.DATASTORE_NAME)) + .thenReturn(mockEdgeIdentityNamedCollection); + when(mockDataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME)) + .thenReturn(mockDirectIdentityNamedCollection); } @Test - public void testHasBooted() { - IdentityState state = new IdentityState(new IdentityProperties()); + public void testBootUpIfReady_persistedECIDIsReused() { + final IdentityProperties persistedProperties = new IdentityProperties(); + final ECID persistedECID = new ECID(); + persistedProperties.setECID(persistedECID); - // test - assertFalse(state.hasBooted()); + when(mockIdentityStorageManager.loadPropertiesFromPersistence()).thenReturn(persistedProperties); + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); - state.bootupIfReady(mockSharedStateCallback); - assertTrue(state.hasBooted()); + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + assertEquals(persistedProperties.getECID(), identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager, never()).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback) + .createXDMSharedState(identityState.getIdentityProperties().toXDMData(false), null); } @Test - public void testBootupIfReady_GeneratesECID() { - // setup - IdentityState state = new IdentityState(new IdentityProperties()); - assertNull(state.getIdentityProperties().getECID()); + public void testBootUpIfReady_waitsForHubSharedState_hubStateIsNull() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)).thenReturn(null); - // test - state.bootupIfReady(mockSharedStateCallback); - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); // saves to data store - - // verify - assertNotNull(state.getIdentityProperties().getECID()); - assertEquals(1, setXDMSharedEventStateCalledTimes); + assertFalse(identityState.bootupIfReady(mockSharedStateCallback)); } @Test - public void testBootupIfReady_LoadsDirectIdentityECID() { - // setup - ECID ecid = new ECID(); - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(ecid.toString()); - - IdentityState state = new IdentityState(new IdentityProperties()); - assertNull(state.getIdentityProperties().getECID()); + public void testBootUpIfReady_waitsForHubSharedState_hubStateIsPending() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.PENDING, Collections.EMPTY_MAP)); - // test - state.bootupIfReady(mockSharedStateCallback); - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); // saves to data store - - // verify - assertEquals(ecid, state.getIdentityProperties().getECID()); - assertEquals(1, setXDMSharedEventStateCalledTimes); + assertFalse(identityState.bootupIfReady(mockSharedStateCallback)); } @Test - public void testBootupIfReady_GeneratesECIDWhenDirectECIDIsNullInPersistence() { - // setup - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(null); - - IdentityState state = new IdentityState(new IdentityProperties()); - assertNull(state.getIdentityProperties().getECID()); + public void testBootUpIfReady_waitsForHubSharedState_hubStateIsSet() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, Collections.EMPTY_MAP)); - // test - state.bootupIfReady(mockSharedStateCallback); - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); // saves to data store - - // verify - assertNotNull(state.getIdentityProperties().getECID()); - assertEquals(1, setXDMSharedEventStateCalledTimes); + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + assertNotNull(identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback) + .createXDMSharedState(identityState.getIdentityProperties().toXDMData(false), null); } @Test - public void testBootupIfReady_LoadsFromPersistence() { - // setup - IdentityState state = new IdentityState(new IdentityProperties()); - - IdentityProperties persistedProps = new IdentityProperties(); - persistedProps.setECID(new ECID()); - final JSONObject jsonObject = new JSONObject(persistedProps.toXDMData(false)); - final String propsJSON = jsonObject.toString(); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) - .thenReturn(propsJSON); - - // test - state.bootupIfReady(mockSharedStateCallback); - verify(mockSharedPreferenceEditor, never()).apply(); + public void testBootUpIfReady_reUsesIdentityDirectEcidWhenAvailableAndNoPersistedECIDExists() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + final Map hubSharedState = new HashMap<>(); + hubSharedState.put( + "extensions", + new HashMap() { + { + put( + "com.adobe.module.identity", + new HashMap() { + { + put("friendlyName", "Identity"); + put("version", "2.0.0"); + } + } + ); + } + } + ); - // verify - assertEquals(persistedProps.getECID().toString(), state.getIdentityProperties().getECID().toString()); - assertEquals(1, setXDMSharedEventStateCalledTimes); - } + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + final ECID fetchedDirectIdentityECID = new ECID(); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.IdentityDirect.NAME, null)) + .thenReturn( + new SharedStateResult( + SharedStateStatus.SET, + Collections.singletonMap( + IdentityConstants.SharedState.IdentityDirect.ECID, + fetchedDirectIdentityECID.toString() + ) + ) + ); + + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + assertNotNull(identityState.getIdentityProperties().getECID()); + assertEquals(fetchedDirectIdentityECID, identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback) + .createXDMSharedState(identityState.getIdentityProperties().toXDMData(false), null); + } + + @Test + public void testBootUpIfReady_prefersPersistedIdentityDirectEcidOverFetchingFromState() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + + final ECID persistedDirectIdentityECID = new ECID(); + when(mockIdentityStorageManager.loadEcidFromDirectIdentityPersistence()) + .thenReturn(persistedDirectIdentityECID); + + final Map hubSharedState = new HashMap<>(); + hubSharedState.put( + "extensions", + new HashMap() { + { + put( + "com.adobe.module.identity", + new HashMap() { + { + put("friendlyName", "Identity"); + put("version", "2.0.0"); + } + } + ); + } + } + ); - @Test - public void testBootupIfReady_IfReadyLoadsFromPersistenceWhenDirectECIDIsValid() { - // setup - ECID ecid = new ECID(); - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(ecid.toString()); - - IdentityState state = new IdentityState(new IdentityProperties()); - - IdentityProperties persistedProps = new IdentityProperties(); - persistedProps.setECID(new ECID()); - final JSONObject jsonObject = new JSONObject(persistedProps.toXDMData(false)); - final String propsJSON = jsonObject.toString(); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) - .thenReturn(propsJSON); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + final ECID fetchedIdentityDirectECID = new ECID(); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.IdentityDirect.NAME, null)) + .thenReturn( + new SharedStateResult( + SharedStateStatus.SET, + Collections.singletonMap( + IdentityConstants.SharedState.IdentityDirect.ECID, + fetchedIdentityDirectECID.toString() // not the same persisted Identity ECID for the sake of tests + ) + ) + ); + + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + assertNotNull(identityState.getIdentityProperties().getECID()); + assertEquals(persistedDirectIdentityECID, identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback) + .createXDMSharedState(identityState.getIdentityProperties().toXDMData(false), null); + } + + @Test + public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered_stateIsNone() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + final Map hubSharedState = new HashMap<>(); + hubSharedState.put( + "extensions", + new HashMap() { + { + put( + "com.adobe.module.identity", + new HashMap() { + { + put("friendlyName", "Identity"); + put("version", "2.0.0"); + } + } + ); + } + } + ); - // test - state.bootupIfReady(mockSharedStateCallback); - verify(mockSharedPreferenceEditor, never()).apply(); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.IdentityDirect.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.NONE, null)); - // verify - assertEquals(persistedProps.getECID(), state.getIdentityProperties().getECID()); - assertEquals(1, setXDMSharedEventStateCalledTimes); + assertFalse(identityState.bootupIfReady(mockSharedStateCallback)); + assertNull(identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager, never()).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback, times(0)).createXDMSharedState(any(), any()); } @Test - public void testBootupIfReady_whenIdentityDirectRegistered_onFirstBoot_waitsForIdentityDirectECID() { - // setup - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(null); - - IdentityState state = new IdentityState(new IdentityProperties()); - - // test - hubSharedState = new HashMap<>(); + public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered_stateIsPending() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + final Map hubSharedState = new HashMap<>(); hubSharedState.put( "extensions", new HashMap() { @@ -247,36 +253,29 @@ public void testBootupIfReady_whenIdentityDirectRegistered_onFirstBoot_waitsForI new HashMap() { { put("friendlyName", "Identity"); - put("version", "1.2.2"); + put("version", "2.0.0"); } } ); } } ); - state.bootupIfReady(mockSharedStateCallback); - verify(mockSharedPreferenceEditor, never()).apply(); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.IdentityDirect.NAME, null)) + .thenReturn(null); - // verify - assertNull(state.getIdentityProperties().getECID()); - assertEquals(0, setXDMSharedEventStateCalledTimes); + assertFalse(identityState.bootupIfReady(mockSharedStateCallback)); + assertNull(identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager, never()).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback, times(0)).createXDMSharedState(any(), any()); } @Test - public void testBootupIfReady_whenIdentityDirectRegistered_onFirstBoot_usesIdentityDirectECID() { - // setup - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(null); - - IdentityState state = new IdentityState(new IdentityProperties()); - - // test - hubSharedState = new HashMap<>(); + public void testBootUpIfReady_waitsForPendingIdentityDirectStateIfRegistered_stateIsNull() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + final Map hubSharedState = new HashMap<>(); hubSharedState.put( "extensions", new HashMap() { @@ -286,38 +285,29 @@ public void testBootupIfReady_whenIdentityDirectRegistered_onFirstBoot_usesIdent new HashMap() { { put("friendlyName", "Identity"); - put("version", "1.2.2"); + put("version", "2.0.0"); } } ); } } ); - identityDirectSharedState = new HashMap<>(); - identityDirectSharedState.put("mid", "1234"); - state.bootupIfReady(mockSharedStateCallback); - // verify - assertEquals("1234", state.getIdentityProperties().getECID().toString()); // ECID from Identity direct - assertNull(state.getIdentityProperties().getECIDSecondary()); // should be null - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); - assertEquals(1, setXDMSharedEventStateCalledTimes); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.IdentityDirect.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.PENDING, null)); + + assertFalse(identityState.bootupIfReady(mockSharedStateCallback)); + assertNull(identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager, never()).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback, times(0)).createXDMSharedState(any(), any()); } @Test - public void testBootupIfReady_whenIdentityDirectRegistered_onFirstBoot_whenIdentityDirectECIDNull_generatesNew() { - // setup - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(null); - - IdentityState state = new IdentityState(new IdentityProperties()); - - // test - hubSharedState = new HashMap<>(); + public void testBootUpIfReady_regeneratesECIDWhenIdentityDirectStateECIDIsNull() { + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + final Map hubSharedState = new HashMap<>(); hubSharedState.put( "extensions", new HashMap() { @@ -327,21 +317,73 @@ public void testBootupIfReady_whenIdentityDirectRegistered_onFirstBoot_whenIdent new HashMap() { { put("friendlyName", "Identity"); - put("version", "1.2.2"); + put("version", "2.0.0"); } } ); } } ); - identityDirectSharedState = new HashMap<>(); // no mid key - state.bootupIfReady(mockSharedStateCallback); - // verify - assertNotNull("1234", state.getIdentityProperties().getECID()); // new ECID generated - assertNull(state.getIdentityProperties().getECIDSecondary()); // should be null - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); - assertEquals(1, setXDMSharedEventStateCalledTimes); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.IdentityDirect.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, null)); + + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + assertNotNull(identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback) + .createXDMSharedState(identityState.getIdentityProperties().toXDMData(false), null); + } + + @Test + public void testBootUpIfReady_generatesNewECIDWhenDirectStateAndPersistedStateUnavailable() { + // No persisted properties + final IdentityProperties persistedProperties = new IdentityProperties(); + when(mockIdentityStorageManager.loadPropertiesFromPersistence()).thenReturn(persistedProperties); + + // No Identity direct extensions + final Map hubSharedState = new HashMap<>(); + hubSharedState.put("extensions", new HashMap()); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + assertNotNull(identityState.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback) + .createXDMSharedState(identityState.getIdentityProperties().toXDMData(false), null); + } + + @Test + public void testBootUpIfReady_doesNotBootMoreThanOnce() { + // Simulate generating a new ECID and booting + // No persisted properties + final IdentityProperties persistedProperties = new IdentityProperties(); + when(mockIdentityStorageManager.loadPropertiesFromPersistence()).thenReturn(persistedProperties); + + // No Identity direct extensions + final Map hubSharedState = new HashMap<>(); + hubSharedState.put("extensions", new HashMap()); + when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) + .thenReturn(new SharedStateResult(SharedStateStatus.SET, hubSharedState)); + + final IdentityState identityState = new IdentityState(mockIdentityStorageManager); + + // Verify that extension has booted + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + + // Try calling boot up if ready once more + assertTrue(identityState.bootupIfReady(mockSharedStateCallback)); + + assertNotNull(identityState.getIdentityProperties().getECID()); + // verify that properties are set and saved only once + verify(mockIdentityStorageManager, times(1)).savePropertiesToPersistence(identityState.getIdentityProperties()); + verify(mockSharedStateCallback, times(1)) + .createXDMSharedState(identityState.getIdentityProperties().toXDMData(false), null); } // ====================================================================================================================== @@ -351,11 +393,11 @@ public void testBootupIfReady_whenIdentityDirectRegistered_onFirstBoot_whenIdent @Test public void testResetIdentifiers() { // setup - IdentityState state = new IdentityState(new IdentityProperties()); + final IdentityState state = new IdentityState(mockIdentityStorageManager); state.getIdentityProperties().setECID(new ECID()); state.getIdentityProperties().setECIDSecondary(new ECID()); state.getIdentityProperties().setAdId("adID"); - ECID existingEcid = state.getIdentityProperties().getECID(); + final ECID existingEcid = state.getIdentityProperties().getECID(); // test state.resetIdentifiers(); @@ -365,11 +407,11 @@ public void testResetIdentifiers() { assertFalse(state.getIdentityProperties().getECID().toString().isEmpty()); // ECID should not be empty assertNull(state.getIdentityProperties().getECIDSecondary()); // should be cleared assertNull(state.getIdentityProperties().getAdId()); // should be cleared - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); // should save to data store - + verify(mockIdentityStorageManager, times(1)).savePropertiesToPersistence(state.getIdentityProperties()); // should save to data store // Verify consent event not sent (or any event). Consent should not be dispatched by resetIdentifiers - PowerMockito.verifyStatic(MobileCore.class, Mockito.never()); - MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); + + // PowerMockito.verifyStatic(MobileCore.class, Mockito.never()); + // MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); } // ====================================================================================================================== @@ -379,32 +421,31 @@ public void testResetIdentifiers() { @Test public void testUpdateCustomerIdentifiers_happy() throws Exception { // setup - IdentityProperties properties = new IdentityProperties(); - IdentityState state = new IdentityState(properties); + final IdentityState state = new IdentityState(mockIdentityStorageManager); // test - Map identityXDM = createXDMIdentityMap(new IdentityTestUtil.TestItem("UserId", "secretID")); + final Map identityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("UserId", "secretID") + ); state.updateCustomerIdentifiers(IdentityMap.fromXDMMap(identityXDM)); // verify persistence - final ArgumentCaptor persistenceValueCaptor = ArgumentCaptor.forClass(String.class); - verify(mockSharedPreferenceEditor, times(1)) - .putString(eq(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES), persistenceValueCaptor.capture()); - Map persistedData = flattenJSONString(persistenceValueCaptor.getAllValues().get(0)); - assertEquals("secretID", persistedData.get("identityMap.UserId[0].id")); - assertEquals("ambiguous", persistedData.get("identityMap.UserId[0].authenticatedState")); - assertEquals("false", persistedData.get("identityMap.UserId[0].primary")); + final ArgumentCaptor identityPropertiesArgumentCaptor = ArgumentCaptor.forClass( + IdentityProperties.class + ); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityPropertiesArgumentCaptor.capture()); + final IdentityProperties capturedIdentityProperties = identityPropertiesArgumentCaptor.getValue(); + assertEquals(identityXDM, capturedIdentityProperties.toXDMData(false)); } @Test public void testUpdateCustomerIdentifiers_doesNotUpdateReservedNamespace() throws Exception { // setup - IdentityProperties properties = new IdentityProperties(); - properties.setECID(new ECID("internalECID")); - IdentityState state = new IdentityState(properties); + IdentityState state = new IdentityState(mockIdentityStorageManager); + state.getIdentityProperties().setECID(new ECID("internalECID")); // test - Map identityXDM = createXDMIdentityMap( + final Map inputIdentityXDM = createXDMIdentityMap( new IdentityTestUtil.TestItem("ECID", "somevalue"), new IdentityTestUtil.TestItem("GAID", "somevalue"), new IdentityTestUtil.TestItem("IDFA", "somevalue"), @@ -412,166 +453,172 @@ public void testUpdateCustomerIdentifiers_doesNotUpdateReservedNamespace() throw new IdentityTestUtil.TestItem("gaid", "somevalue"), new IdentityTestUtil.TestItem("UserId", "somevalue") ); - state.updateCustomerIdentifiers(IdentityMap.fromXDMMap(identityXDM)); + state.updateCustomerIdentifiers(IdentityMap.fromXDMMap(inputIdentityXDM)); // verify persistence - final ArgumentCaptor persistenceValueCaptor = ArgumentCaptor.forClass(String.class); - verify(mockSharedPreferenceEditor, times(1)) - .putString(eq(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES), persistenceValueCaptor.capture()); - Map persistedData = flattenJSONString(persistenceValueCaptor.getAllValues().get(0)); - assertEquals(6, persistedData.size()); // USERID identifier and initial ECID - assertEquals("somevalue", persistedData.get("identityMap.UserId[0].id")); - assertEquals("ambiguous", persistedData.get("identityMap.UserId[0].authenticatedState")); - assertEquals("false", persistedData.get("identityMap.UserId[0].primary")); - assertEquals("internalECID", persistedData.get("identityMap.ECID[0].id")); // verify that the ECID is not disturbed - assertEquals("ambiguous", persistedData.get("identityMap.ECID[0].authenticatedState")); - assertEquals("false", persistedData.get("identityMap.ECID[0].primary")); + final ArgumentCaptor identityPropertiesArgumentCaptor = ArgumentCaptor.forClass( + IdentityProperties.class + ); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityPropertiesArgumentCaptor.capture()); + final IdentityProperties capturedIdentityProperties = identityPropertiesArgumentCaptor.getValue(); + final Map expectedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("UserId", "somevalue") + ); + assertEquals(expectedIdentityXDM, capturedIdentityProperties.toXDMData(false)); } @Test public void testUpdateCustomerIdentifiers_whenCaseSensitiveNamespace_storesAll() throws Exception { // setup - IdentityProperties properties = new IdentityProperties(); - properties.setECID(new ECID("internalECID")); - IdentityState state = new IdentityState(properties); + final IdentityState state = new IdentityState(mockIdentityStorageManager); + state.getIdentityProperties().setECID(new ECID("internalECID")); // test - Map identityXDM = createXDMIdentityMap( + final Map identityXDM = createXDMIdentityMap( new IdentityTestUtil.TestItem("caseSensitive", "somevalue"), new IdentityTestUtil.TestItem("CASESENSITIVE", "SOMEVALUE") ); state.updateCustomerIdentifiers(IdentityMap.fromXDMMap(identityXDM)); - final ArgumentCaptor sharedStateCaptor = ArgumentCaptor.forClass(Map.class); - - // verify shared state - final ArgumentCaptor persistenceValueCaptor = ArgumentCaptor.forClass(String.class); - verify(mockSharedPreferenceEditor, times(1)) - .putString(eq(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES), persistenceValueCaptor.capture()); - Map persistedData = flattenJSONString(persistenceValueCaptor.getAllValues().get(0)); - assertEquals(9, persistedData.size()); // updated ids + ECID - assertEquals("somevalue", persistedData.get("identityMap.caseSensitive[0].id")); - assertEquals("SOMEVALUE", persistedData.get("identityMap.CASESENSITIVE[0].id")); - assertEquals("internalECID", persistedData.get("identityMap.ECID[0].id")); + // verify persistence + final ArgumentCaptor identityPropertiesArgumentCaptor = ArgumentCaptor.forClass( + IdentityProperties.class + ); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityPropertiesArgumentCaptor.capture()); + final Map expectedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("caseSensitive", "somevalue"), + new IdentityTestUtil.TestItem("CASESENSITIVE", "SOMEVALUE") + ); + final IdentityProperties capturedIdentityProperties = identityPropertiesArgumentCaptor.getValue(); + assertEquals(expectedIdentityXDM, capturedIdentityProperties.toXDMData(false)); } + // ====================================================================================================================== + // Tests for method : removeCustomerIdentifiers(final IdentityMap map) + // ====================================================================================================================== + @Test public void testRemoveCustomerIdentifiers_happy() throws Exception { // setup - Map identityXDM = createXDMIdentityMap( + final Map initialIdentityXDM = createXDMIdentityMap( new IdentityTestUtil.TestItem("UserId", "secretID"), new IdentityTestUtil.TestItem("PushId", "token") ); - IdentityProperties properties = new IdentityProperties(identityXDM); - IdentityState state = new IdentityState(properties); + final IdentityState state = new IdentityState(mockIdentityStorageManager); + state.getIdentityProperties().updateCustomerIdentifiers(IdentityMap.fromXDMMap(initialIdentityXDM)); + state.getIdentityProperties().setECID(new ECID("internalECID")); // test - Map removedIdentityXDM = createXDMIdentityMap( + final Map removedIdentityXDM = createXDMIdentityMap( new IdentityTestUtil.TestItem("UserId", "secretID") ); state.removeCustomerIdentifiers(IdentityMap.fromXDMMap(removedIdentityXDM)); - // verify - final ArgumentCaptor persistenceValueCaptor = ArgumentCaptor.forClass(String.class); - verify(mockSharedPreferenceEditor, times(1)) - .putString(eq(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES), persistenceValueCaptor.capture()); - Map persistedData = flattenJSONString(persistenceValueCaptor.getAllValues().get(0)); - assertEquals(3, persistedData.size()); - assertNull(persistedData.get("identityMap.UserId[0].id")); - assertEquals("token", persistedData.get("identityMap.PushId[0].id")); + final ArgumentCaptor identityPropertiesArgumentCaptor = ArgumentCaptor.forClass( + IdentityProperties.class + ); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityPropertiesArgumentCaptor.capture()); + final Map expectedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("ECID", "internalECID"), + new IdentityTestUtil.TestItem("PushId", "token") + ); + final IdentityProperties capturedIdentityProperties = identityPropertiesArgumentCaptor.getValue(); + assertEquals(expectedIdentityXDM, capturedIdentityProperties.toXDMData(false)); } @Test public void testRemoveCustomerIdentifiers_doesNotRemoveReservedNamespace() throws Exception { - // setup - Map identityXDM = createXDMIdentityMap( - new IdentityTestUtil.TestItem("GAID", "someGAID"), - new IdentityTestUtil.TestItem("ECID", "someECID"), - new IdentityTestUtil.TestItem("IDFA", "someIDFA") - ); - IdentityProperties properties = new IdentityProperties(identityXDM); - IdentityState state = new IdentityState(properties); + final IdentityState state = new IdentityState(mockIdentityStorageManager); + final ECID initialECID = new ECID(); + state.getIdentityProperties().setECID(initialECID); + state.getIdentityProperties().setAdId("initialADID"); + + final IdentityProperties initialProperties = state.getIdentityProperties(); // test - Map removedIdentityXDM = createXDMIdentityMap( - new IdentityTestUtil.TestItem("GAID", "someGAID"), - new IdentityTestUtil.TestItem("ecid", "someECID"), - new IdentityTestUtil.TestItem("Idfa", "someIDFA") + final Map removedIdentityXDM = createXDMIdentityMap( + new IdentityTestUtil.TestItem("GAID", "initialECID"), + new IdentityTestUtil.TestItem("ECID", initialECID.toString()) ); state.removeCustomerIdentifiers(IdentityMap.fromXDMMap(removedIdentityXDM)); - // verify - final ArgumentCaptor persistenceValueCaptor = ArgumentCaptor.forClass(String.class); - verify(mockSharedPreferenceEditor, times(1)) - .putString(eq(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES), persistenceValueCaptor.capture()); - Map persistedData = flattenJSONString(persistenceValueCaptor.getAllValues().get(0)); - assertEquals(9, persistedData.size()); - assertEquals("someGAID", persistedData.get("identityMap.GAID[0].id")); - assertEquals("someECID", persistedData.get("identityMap.ECID[0].id")); - assertEquals("someIDFA", persistedData.get("identityMap.IDFA[0].id")); + final ArgumentCaptor identityPropertiesArgumentCaptor = ArgumentCaptor.forClass( + IdentityProperties.class + ); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityPropertiesArgumentCaptor.capture()); + final IdentityProperties capturedProperties = identityPropertiesArgumentCaptor.getValue(); + assertEquals(initialProperties.toXDMData(false), capturedProperties.toXDMData(false)); } + // ====================================================================================================================== + // Tests for method : updateLegacyExperienceCloudId(final IdentityMap map) + // ====================================================================================================================== + @Test public void testUpdateLegacyExperienceCloudId() { - IdentityState state = new IdentityState(new IdentityProperties()); - state.getIdentityProperties().setECID(new ECID()); - ECID legacyEcid = new ECID(); + final IdentityState state = new IdentityState(mockIdentityStorageManager); + final ECID ecid = new ECID(); + state.getIdentityProperties().setECID(ecid); + final ECID legacyEcid = new ECID(); // test state.updateLegacyExperienceCloudId(legacyEcid); // verify assertEquals(legacyEcid, state.getIdentityProperties().getECIDSecondary()); - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); + assertEquals(ecid, state.getIdentityProperties().getECID()); + verify(mockIdentityStorageManager).savePropertiesToPersistence(state.getIdentityProperties()); } @Test public void testUpdateLegacyExperienceCloudId_notSetWhenECIDSame() { - IdentityState state = new IdentityState(new IdentityProperties()); - ECID legacyEcid = new ECID(); + final IdentityState state = new IdentityState(mockIdentityStorageManager); + final ECID legacyEcid = new ECID(); state.getIdentityProperties().setECID(legacyEcid); state.updateLegacyExperienceCloudId(legacyEcid); // verify assertNull(state.getIdentityProperties().getECIDSecondary()); - verify(mockSharedPreferenceEditor, Mockito.times(0)).apply(); + verify(mockIdentityStorageManager, times(0)).savePropertiesToPersistence(any()); } @Test public void testUpdateLegacyExperienceCloudId_notSetWhenSecondaryECIDSame() { - IdentityState state = new IdentityState(new IdentityProperties()); + final IdentityState state = new IdentityState(mockIdentityStorageManager); state.getIdentityProperties().setECID(new ECID()); - ECID legacyEcid = new ECID(); + final ECID legacyEcid = new ECID(); state.getIdentityProperties().setECIDSecondary(legacyEcid); state.updateLegacyExperienceCloudId(legacyEcid); assertEquals(legacyEcid, state.getIdentityProperties().getECIDSecondary()); - verify(mockSharedPreferenceEditor, Mockito.times(0)).apply(); + verify(mockIdentityStorageManager, times(0)).savePropertiesToPersistence(any()); } @Test public void testUpdateLegacyExperienceCloudId_clearsOnNull() { - IdentityState state = new IdentityState(new IdentityProperties()); + final IdentityState state = new IdentityState(mockIdentityStorageManager); state.getIdentityProperties().setECID(new ECID()); state.getIdentityProperties().setECIDSecondary(new ECID()); state.updateLegacyExperienceCloudId(null); assertNull(state.getIdentityProperties().getECIDSecondary()); - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); + verify(mockIdentityStorageManager, times(1)).savePropertiesToPersistence(state.getIdentityProperties()); } @Test public void testUpdateLegacyExperienceCloudId_notSetWhenExistingIsNull() { - IdentityState state = new IdentityState(new IdentityProperties()); + final IdentityState state = new IdentityState(mockIdentityStorageManager); state.getIdentityProperties().setECID(new ECID()); state.updateLegacyExperienceCloudId(null); assertNull(state.getIdentityProperties().getECIDSecondary()); - verify(mockSharedPreferenceEditor, Mockito.times(0)).apply(); + verify(mockIdentityStorageManager, times(0)).savePropertiesToPersistence(any()); } // ====================================================================================================================== @@ -581,11 +628,11 @@ public void testUpdateLegacyExperienceCloudId_notSetWhenExistingIsNull() { // With consent change @Test public void testUpdateAdvertisingIdentifier_notSet_whenInitializingIdentityState() { - IdentityState state = new IdentityState(new IdentityProperties()); + final IdentityState state = new IdentityState(mockIdentityStorageManager); state.getIdentityProperties().setECID(new ECID()); assertNull(state.getIdentityProperties().getAdId()); - verify(mockSharedPreferenceEditor, Mockito.times(0)).apply(); + verify(mockIdentityStorageManager, times(0)).savePropertiesToPersistence(any()); } @Test @@ -683,47 +730,42 @@ private void assertUpdateAdvertisingIdentifier( boolean isSharedStateUpdateExpected ) throws Exception { // Setup - IdentityState state = new IdentityState((new IdentityProperties())); + + final IdentityState state = new IdentityState(mockIdentityStorageManager); state.getIdentityProperties().setECID(new ECID()); state.getIdentityProperties().setAdId(persistedAdId); - - Event event = fakeGenericIdentityEvent(newAdId); - state.updateAdvertisingIdentifier(event, mockSharedStateCallback); - - // Verify consent event - if (expectedConsent == null) { - PowerMockito.verifyStatic(MobileCore.class, Mockito.never()); - MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); - } else { - final ArgumentCaptor consentEventCaptor = ArgumentCaptor.forClass(Event.class); - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEvent(consentEventCaptor.capture(), any(ExtensionErrorCallback.class)); - - Event consentEvent = consentEventCaptor.getAllValues().get(0); - - Map consentEventData = flattenMap(consentEvent.getEventData()); - // `flattenMap` allows for checking the keys' hierarchy and literal values simultaneously - assertEquals("GAID", consentEventData.get("consents.adID.idType")); - assertEquals(expectedConsent, consentEventData.get("consents.adID.val")); + final Event event = fakeGenericIdentityEvent(newAdId); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + state.updateAdvertisingIdentifier(event, mockSharedStateCallback); + + // Verify consent event + if (expectedConsent == null) { + mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(any()), never()); + } else { + final ArgumentCaptor consentEventCaptor = ArgumentCaptor.forClass(Event.class); + mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(consentEventCaptor.capture()), times(1)); + + final Event consentEvent = consentEventCaptor.getValue(); + + final Map consentEventData = flattenMap(consentEvent.getEventData()); + // `flattenMap` allows for checking the keys' hierarchy and literal values simultaneously + assertEquals("GAID", consentEventData.get("consents.adID.idType")); + assertEquals(expectedConsent, consentEventData.get("consents.adID.val")); + } } if (isSharedStateUpdateExpected) { - // Verify persistent store - final ArgumentCaptor persistenceValueCaptor = ArgumentCaptor.forClass(String.class); - verify(mockSharedPreferenceEditor, times(1)) - .putString(eq(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES), persistenceValueCaptor.capture()); - Map persistedData = flattenJSONString(persistenceValueCaptor.getAllValues().get(0)); - verifyFlatIdentityMap(persistedData, expectedAdId, state.getIdentityProperties().getECID().toString()); - - // Verify shared state and properties - assertEquals(1, setXDMSharedEventStateCalledTimes); + final ArgumentCaptor identityPropertiesArgumentCaptor = ArgumentCaptor.forClass( + IdentityProperties.class + ); + verify(mockIdentityStorageManager).savePropertiesToPersistence(identityPropertiesArgumentCaptor.capture()); + final IdentityProperties capturedProperties = identityPropertiesArgumentCaptor.getValue(); + final Map flatMap = flattenMap(capturedProperties.toXDMData(false)); + verifyFlatIdentityMap(flatMap, expectedAdId, state.getIdentityProperties().getECID().toString()); + mockSharedStateCallback.createXDMSharedState(capturedProperties.toXDMData(false), event); } else { - // Verify persistent store - verify(mockSharedPreferenceEditor, never()) - .putString(eq(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES), any(String.class)); - - // Verify shared state and properties - assertEquals(0, setXDMSharedEventStateCalledTimes); + verify(mockIdentityStorageManager, times(0)).savePropertiesToPersistence(any()); + mockSharedStateCallback.createXDMSharedState(any(), any()); } // Verify identity map final Map flatIdentityMap = flattenMap(state.getIdentityProperties().toXDMData(false)); @@ -779,6 +821,5 @@ private void verifyFlatIdentityMap( assertEquals("false", flatIdentityMap.get("identityMap.ECID[0].primary")); assertEquals(expectedECID, flatIdentityMap.get("identityMap.ECID[0].id")); assertEquals("ambiguous", flatIdentityMap.get("identityMap.ECID[0].authenticatedState")); - return; } } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManagerTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManagerTests.java new file mode 100644 index 00000000..d719f107 --- /dev/null +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManagerTests.java @@ -0,0 +1,208 @@ +/* + 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.adobe.marketing.mobile.services.DataStoring; +import com.adobe.marketing.mobile.services.NamedCollection; +import com.adobe.marketing.mobile.services.ServiceProvider; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class IdentityStorageManagerTests { + + @Mock + private ServiceProvider mockServiceProvider; + + private MockedStatic mockedStaticServiceProvider; + + @Mock + private DataStoring mockDataStoreService; + + @Mock + private NamedCollection mockEdgeIdentityNamedCollection; + + @Mock + private NamedCollection mockDirectIdentityNamedCollection; + + @Before + public void before() throws Exception { + MockitoAnnotations.openMocks(this); + + mockedStaticServiceProvider = Mockito.mockStatic(ServiceProvider.class); + mockedStaticServiceProvider.when(ServiceProvider::getInstance).thenReturn(mockServiceProvider); + when(mockServiceProvider.getDataStoreService()).thenReturn(mockDataStoreService); + + when(mockDataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.DATASTORE_NAME)) + .thenReturn(mockEdgeIdentityNamedCollection); + when(mockDataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME)) + .thenReturn(mockDirectIdentityNamedCollection); + } + + @Test + public void testLoadPropertiesFromPersistence_edgeIdentityDataStoreIsNull() { + // setup + when(mockDataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.DATASTORE_NAME)).thenReturn(null); + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + // test + final IdentityProperties identityProperties = identityStorageManager.loadPropertiesFromPersistence(); + + // verify + assertNull(identityProperties); + } + + @Test + public void testLoadPropertiesFromPersistence_identityPropertiesAreEmpty() { + when(mockEdgeIdentityNamedCollection.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) + .thenReturn(null); + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + // test + final IdentityProperties identityProperties = identityStorageManager.loadPropertiesFromPersistence(); + + // verify + assertNull(identityProperties); + } + + @Test + public void testLoadPropertiesFromPersistence_identityPropertiesIsInvalid() { + when(mockEdgeIdentityNamedCollection.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) + .thenReturn("{someinvalidjson}"); + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + // test + final IdentityProperties identityProperties = identityStorageManager.loadPropertiesFromPersistence(); + + // verify + assertNull(identityProperties); + } + + @Test + public void testLoadPropertiesFromPersistence_validJSON() { + // setup + final IdentityProperties persistedProps = new IdentityProperties(); + persistedProps.setECID(new ECID()); + final JSONObject jsonObject = new JSONObject(persistedProps.toXDMData(false)); + final String propsJSON = jsonObject.toString(); + + when(mockEdgeIdentityNamedCollection.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) + .thenReturn(propsJSON); + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + // test + IdentityProperties props = identityStorageManager.loadPropertiesFromPersistence(); + + // verify + assertEquals(persistedProps.toXDMData(false), props.toXDMData(false)); + } + + @Test + public void testSavePropertiesToPersistence_edgeIdentityStoreIsNull() { + // setup + when(mockDataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.DATASTORE_NAME)).thenReturn(null); + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + // test + final IdentityProperties identityProperties = new IdentityProperties(); + identityStorageManager.savePropertiesToPersistence(identityProperties); + + // verify + verify(mockEdgeIdentityNamedCollection, never()).setString(any(), any()); + } + + @Test + public void testSavePropertiesToPersistence_nullProps() { + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + // test + identityStorageManager.savePropertiesToPersistence(null); + + // verify + verify(mockEdgeIdentityNamedCollection, times(1)).remove(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES); + } + + @Test + public void testSavePropertiesToPersistence_validProps() { + // test + final IdentityProperties properties = new IdentityProperties(); + properties.setECID(new ECID()); + + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + identityStorageManager.savePropertiesToPersistence(properties); + + // verify + final JSONObject jsonObject = new JSONObject(properties.toXDMData(false)); + final String expectedJSON = jsonObject.toString(); + verify(mockEdgeIdentityNamedCollection) + .setString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, expectedJSON); + } + + @Test + public void testLoadEcidFromDirectIdentityPersistence_DirectIdentityStoreIsNull() { + when(mockDataStoreService.getNamedCollection(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME)) + .thenReturn(null); + + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + assertNull(identityStorageManager.loadEcidFromDirectIdentityPersistence()); + } + + @Test + public void testLoadEcidFromDirectIdentityPersistence_loadValidECID() { + final ECID ecid = new ECID(); + when(mockDirectIdentityNamedCollection.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) + .thenReturn(ecid.toString()); + + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + assertEquals(ecid, identityStorageManager.loadEcidFromDirectIdentityPersistence()); + } + + @Test + public void testLoadEcidFromDirectIdentityPersistence_whenNullECID() { + when(mockDirectIdentityNamedCollection.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) + .thenReturn(null); + + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + assertNull(identityStorageManager.loadEcidFromDirectIdentityPersistence()); + } + + @Test + public void testLoadEcidFromDirectIdentityPersistence_whenEmptyECID() { + when(mockDirectIdentityNamedCollection.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) + .thenReturn(""); + + final IdentityStorageManager identityStorageManager = new IdentityStorageManager(mockDataStoreService); + + assertNull(identityStorageManager.loadEcidFromDirectIdentityPersistence()); + } + + @After + public void teardown() { + mockedStaticServiceProvider.close(); + } +} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageServiceTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageServiceTests.java deleted file mode 100644 index ce229389..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageServiceTests.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - 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 static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; -import com.adobe.marketing.mobile.MobileCore; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; - -@RunWith(PowerMockRunner.class) -@PrepareForTest({ MobileCore.class }) -public class IdentityStorageServiceTests { - - @Mock - Application mockApplication; - - @Mock - Context mockContext; - - @Mock - SharedPreferences mockSharedPreference; - - @Mock - SharedPreferences.Editor mockSharedPreferenceEditor; - - @Before - public void before() throws Exception { - PowerMockito.mockStatic(MobileCore.class); - - Mockito.when(MobileCore.getApplication()).thenReturn(mockApplication); - Mockito.when(mockApplication.getApplicationContext()).thenReturn(mockContext); - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito.when(mockSharedPreference.edit()).thenReturn(mockSharedPreferenceEditor); - } - - @Test - public void testLoadPropertiesFromPersistence_nullSharedPrefs() { - // setup - Mockito.when(mockApplication.getApplicationContext()).thenReturn(null); - - // test - IdentityProperties props = IdentityStorageService.loadPropertiesFromPersistence(); - - // verify - assertNull(props); - } - - @Test - public void testLoadPropertiesFromPersistence_emptyPrefs() { - // setup - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) - .thenReturn(null); - - // test - IdentityProperties props = IdentityStorageService.loadPropertiesFromPersistence(); - - // verify - assertNull(props); - } - - @Test - public void testLoadPropertiesFromPersistence_invalidJSON() { - // setup - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) - .thenReturn("{"); - - // test - IdentityProperties props = IdentityStorageService.loadPropertiesFromPersistence(); - - // verify - assertNull(props); - } - - @Test - public void testLoadPropertiesFromPersistence_validJSON() { - // setup - IdentityProperties persistedProps = new IdentityProperties(); - persistedProps.setECID(new ECID()); - final JSONObject jsonObject = new JSONObject(persistedProps.toXDMData(false)); - final String propsJSON = jsonObject.toString(); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, null)) - .thenReturn(propsJSON); - - // test - IdentityProperties props = IdentityStorageService.loadPropertiesFromPersistence(); - - // verify - assertEquals(persistedProps.toXDMData(false), props.toXDMData(false)); - } - - @Test - public void testSavePropertiesToPersistence_nullSharedPrefs() { - // setup - Mockito.when(mockApplication.getApplicationContext()).thenReturn(null); - - // test - IdentityProperties props = new IdentityProperties(); - IdentityStorageService.savePropertiesToPersistence(props); - - // verify - verify(mockSharedPreferenceEditor, never()).apply(); - } - - @Test - public void testSavePropertiesToPersistence_nullEditor() { - // setup - Mockito.when(mockSharedPreference.edit()).thenReturn(null); - - // test - IdentityProperties props = new IdentityProperties(); - IdentityStorageService.savePropertiesToPersistence(props); - - // verify - verify(mockSharedPreferenceEditor, never()).apply(); - } - - @Test - public void testSavePropertiesToPersistence_nullProps() { - // test - IdentityStorageService.savePropertiesToPersistence(null); - - // verify - verify(mockSharedPreferenceEditor, Mockito.times(1)).remove(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES); - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); - } - - @Test - public void testSavePropertiesToPersistence_validProps() { - // test - IdentityProperties props = new IdentityProperties(); - props.setECID(new ECID()); - IdentityStorageService.savePropertiesToPersistence(props); - - // verify - final JSONObject jsonObject = new JSONObject(props.toXDMData(false)); - final String expectedJSON = jsonObject.toString(); - verify(mockSharedPreferenceEditor, Mockito.times(1)) - .putString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, expectedJSON); - verify(mockSharedPreferenceEditor, Mockito.times(1)).apply(); - } - - @Test - public void testLoadEcidFromDirectIdentityPersistence_loadECID() { - ECID ecid = new ECID(); - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(ecid.toString()); - - assertEquals(ecid, IdentityStorageService.loadEcidFromDirectIdentityPersistence()); - } - - @Test - public void testLoadEcidFromDirectIdentityPersistence_whenNullECID() { - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(null); - - assertNull(IdentityStorageService.loadEcidFromDirectIdentityPersistence()); - } - - @Test - public void testLoadEcidFromDirectIdentityPersistence_whenEmptyECID() { - Mockito - .when(mockContext.getSharedPreferences(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, 0)) - .thenReturn(mockSharedPreference); - Mockito - .when(mockSharedPreference.getString(IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, null)) - .thenReturn(""); - - assertNull(IdentityStorageService.loadEcidFromDirectIdentityPersistence()); - } -} 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 281bf0ef..130c1050 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 @@ -13,39 +13,40 @@ 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.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; -import android.util.Log; import com.adobe.marketing.mobile.AdobeCallback; import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.ExtensionError; import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.util.JSONUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +import org.mockito.MockitoAnnotations; -@RunWith(PowerMockRunner.class) -@PrepareForTest({ MobileCore.class }) public class IdentityTests { @Before public void setup() { - PowerMockito.mockStatic(MobileCore.class); + MockitoAnnotations.openMocks(this); } // ======================================================================================== @@ -57,7 +58,7 @@ public void test_extensionVersionAPI() { // test String extensionVersion = Identity.extensionVersion(); assertEquals( - "The Extension version API returns the correct value", + "The Extension version API should return the correct value", IdentityConstants.EXTENSION_VERSION, extensionVersion ); @@ -68,22 +69,26 @@ public void test_extensionVersionAPI() { // ======================================================================================== @Test public void testRegistration() { - // test - Identity.registerExtension(); - final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass( - ExtensionErrorCallback.class - ); - - // The identity extension should register with core - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.registerExtension(ArgumentMatchers.eq(IdentityExtension.class), callbackCaptor.capture()); - - // verify the callback - ExtensionErrorCallback extensionErrorCallback = callbackCaptor.getValue(); - assertNotNull("The extension callback should not be null", extensionErrorCallback); - // TODO - enable when ExtensionError creation is available - // should not crash on calling the callback - //extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.registerExtension(); + + // verify + final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass( + ExtensionErrorCallback.class + ); + mockedStaticMobileCore.verify(() -> + MobileCore.registerExtension(eq(IdentityExtension.class), callbackCaptor.capture()) + ); + + final ExtensionErrorCallback extensionErrorCallback = callbackCaptor.getValue(); + assertNotNull("The extension callback should not be null", extensionErrorCallback); + + // verify that the callback invocation does not throw an exception + extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); + } catch (final Exception e) { + fail(); + } } // ======================================================================================== @@ -93,135 +98,194 @@ public void testRegistration() { public void testGetExperienceCloudId() { // setup final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); - final ArgumentCaptor extensionErrorCallbackCaptor = ArgumentCaptor.forClass( - ExtensionErrorCallback.class + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class ); + final List callbackReturnValues = new ArrayList<>(); - // test - Identity.getExperienceCloudId( - new AdobeCallback() { - @Override - public void call(String s) { - callbackReturnValues.add(s); - } - } - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getExperienceCloudId(callbackReturnValues::add); + + //verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + + // verify the dispatched event details + final Event dispatchedEvent = eventCaptor.getValue(); + assertEquals(IdentityConstants.EventNames.IDENTITY_REQUEST_IDENTITY_ECID, dispatchedEvent.getName()); + assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); + assertNull(dispatchedEvent.getEventData()); + + // verify callback responses + final ECID ecid = new ECID(); + final Map ecidDict = new HashMap<>(); + ecidDict.put("id", ecid.toString()); + final ArrayList ecidArr = new ArrayList<>(); + ecidArr.add(ecidDict); + final Map identityMap = new HashMap<>(); + identityMap.put("ECID", ecidArr); + final Map xdmData = new HashMap<>(); + xdmData.put("identityMap", identityMap); + + assertNotNull(adobeCallbackCaptor.getValue()); + adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(xdmData)); + assertEquals(ecid.toString(), callbackReturnValues.get(0)); + } catch (Exception e) { + fail(); + } + } - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - eventCaptor.capture(), - adobeCallbackCaptor.capture(), - extensionErrorCallbackCaptor.capture() + @Test + public void testGetExperienceCloudId_invokeCallbackOnfail() { + // setup + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; + final Map errorCapture = new HashMap<>(); + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); + } - // verify the dispatched event details - Event dispatchedEvent = eventCaptor.getValue(); - assertEquals(IdentityConstants.EventNames.IDENTITY_REQUEST_IDENTITY_ECID, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY.toLowerCase(), dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY.toLowerCase(), dispatchedEvent.getSource()); - assertTrue(dispatchedEvent.getEventData().isEmpty()); + @Override + public void call(String ecid) {} + }; - // verify callback responses - ECID ecid = new ECID(); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getExperienceCloudId(callbackWithError); + + //verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } - Map ecidDict = new HashMap<>(); - ecidDict.put("id", ecid.toString()); - ArrayList ecidArr = new ArrayList<>(); - ecidArr.add(ecidDict); - Map identityMap = new HashMap<>(); - identityMap.put("ECID", ecidArr); - Map xdmData = new HashMap<>(); - xdmData.put("identityMap", identityMap); + // set response event to null + adobeCallbackCaptor.getValue().fail(AdobeError.UNEXPECTED_ERROR); - adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(xdmData)); - assertEquals(ecid.toString(), callbackReturnValues.get(0)); - // TODO - enable when ExtensionError creation is available - // should not crash on calling the callback - //extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); + // verify + assertTrue(((boolean) errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED))); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } @Test public void testGetExperienceCloudId_nullCallback() { - // test - Identity.getExperienceCloudId(null); - - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); - MobileCore.dispatchEventWithResponseCallback( - any(Event.class), - any(AdobeCallback.class), - any(ExtensionErrorCallback.class) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getExperienceCloudId(null); + + //verify + mockedStaticMobileCore.verify( + () -> + MobileCore.dispatchEventWithResponseCallback( + any(Event.class), + anyLong(), + any(AdobeCallbackWithError.class) + ), + times(0) + ); + } } @Test public void testGetExperienceCloudId_nullResponseEvent() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); - final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override - public void call(Object o) {} + public void call(String ecid) {} }; - // test - Identity.getExperienceCloudId(callbackWithError); - - // verify if the event is dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - any(Event.class), - adobeCallbackCaptor.capture(), - any(ExtensionErrorCallback.class) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getExperienceCloudId(callbackWithError); + + //verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } // 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)); + assertTrue(((boolean) errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED))); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } @Test public void testGetExperienceCloudId_invalidEventData() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); - final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override - public void call(Object o) {} + public void call(String ecid) {} }; - // test - Identity.getExperienceCloudId(callbackWithError); - - // verify if the event is dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - any(Event.class), - adobeCallbackCaptor.capture(), - any(ExtensionErrorCallback.class) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getExperienceCloudId(callbackWithError); + + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } // set response event to null Map eventData = new HashMap<>(); @@ -229,8 +293,8 @@ public void call(Object o) {} adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(eventData)); // verify - assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); - assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + assertTrue((boolean) errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } @Test @@ -238,8 +302,11 @@ public void testGetExperienceCloudId_missingECID() { // setup final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { @@ -251,16 +318,19 @@ public void fail(AdobeError adobeError) { public void call(Object o) {} }; - // test - Identity.getExperienceCloudId(callbackWithError); - - // verify if the event is dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - any(Event.class), - adobeCallbackCaptor.capture(), - any(ExtensionErrorCallback.class) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getExperienceCloudId(callbackWithError); + + //verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } // set response event to map missing ECID Map emptyXDMData = new HashMap<>(); @@ -275,174 +345,203 @@ public void call(Object o) {} // getUrlVariables API // ======================================================================================== @Test - public void testGetUrlVariables() throws InterruptedException { + 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 ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class ); final List callbackReturnValues = new ArrayList<>(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - // test - Identity.getUrlVariables( - new AdobeCallback() { - @Override - public void call(String s) { - callbackReturnValues.add(s); - latch.countDown(); + + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getUrlVariables( + new AdobeCallback() { + @Override + public void call(String s) { + callbackReturnValues.add(s); + } } - } - ); - latch.await(2000, TimeUnit.MILLISECONDS); - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - eventCaptor.capture(), - adobeCallbackCaptor.capture(), - extensionErrorCallbackCaptor.capture() - ); + ); + + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } // verify the dispatched event details - Event dispatchedEvent = eventCaptor.getValue(); + final 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()); + assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); assertTrue(dispatchedEvent.getEventData().containsKey("urlvariables")); assertTrue((boolean) dispatchedEvent.getEventData().get("urlvariables")); // verify callback responses - Map urlVariablesResponse = new HashMap<>(); + final 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) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getUrlVariables(null); + + mockedStaticMobileCore.verify( + () -> + MobileCore.dispatchEventWithResponseCallback( + any(Event.class), + anyLong(), + any(AdobeCallbackWithError.class) + ), + never() + ); + } } @Test public void testGetUrlVariables_nullResponseEvent() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); - final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override - public void call(Object o) {} + public void call(String 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) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getUrlVariables(callbackWithError); + + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } + + final Event dispatchedEvent = eventCaptor.getValue(); + assertEquals(IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, dispatchedEvent.getName()); + assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); + assertTrue(dispatchedEvent.getEventData().containsKey("urlvariables")); + assertTrue((boolean) dispatchedEvent.getEventData().get("urlvariables")); // 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)); + assertEquals(true, errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } @Test public void testGetUrlVariables_invalidEventData() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); - final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override - public void call(Object o) {} + public void call(String 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) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.getUrlVariables(callbackWithError); + + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } // set response event data to not have urlvariables key - Map eventData = new HashMap<>(); + final Map eventData = new HashMap<>(); eventData.put("someKey", "someValue"); eventData.put("urlvariables", true); adobeCallbackCaptor.getValue().call(buildUrlVariablesResponseEvent(eventData)); // verify - assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); - assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + assertEquals(true, errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } @Test public void testGetUrlVariables_NullUrlVariablesStringInResponseData() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); - final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override - public void call(Object o) { - Log.d("test", "test"); - } + public void call(String 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) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getUrlVariables(callbackWithError); + + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } // set response event to have urlvariables map to null value Map nullUrlVariablesData = new HashMap<>(); @@ -450,8 +549,49 @@ public void call(Object o) { adobeCallbackCaptor.getValue().call(buildUrlVariablesResponseEvent(nullUrlVariablesData)); // verify - assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); - assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + assertEquals(true, errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); + } + + @Test + public void testGetUrlVariables_callbackOnFail() { + // setup + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; + final Map errorCapture = new HashMap<>(); + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); + } + + @Override + public void call(String o) {} + }; + + // test + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getUrlVariables(callbackWithError); + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } catch (Exception e) { + fail(); + } + adobeCallbackCaptor.getValue().fail(AdobeError.UNEXPECTED_ERROR); + + // verify + assertEquals(true, errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } // ======================================================================================== @@ -461,70 +601,76 @@ public void call(Object o) { public void testUpdateIdentities() { // setup final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - final ArgumentCaptor extensionErrorCallbackCaptor = ArgumentCaptor.forClass( - ExtensionErrorCallback.class - ); - - // test - IdentityMap map = new IdentityMap(); + final IdentityMap map = new IdentityMap(); map.addItem(new IdentityItem("id", AuthenticatedState.AUTHENTICATED, true), "mainspace"); map.addItem(new IdentityItem("idtwo", AuthenticatedState.LOGGED_OUT, false), "secondspace"); - Identity.updateIdentities(map); - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEvent(eventCaptor.capture(), extensionErrorCallbackCaptor.capture()); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.updateIdentities(map); - // TODO - enable when ExtensionError creation is available - // should not crash on calling the callback - //extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); + mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(eventCaptor.capture())); + } catch (Exception e) { + fail(); + } // verify the dispatched event details Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.UPDATE_IDENTITIES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY.toLowerCase(), dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.UPDATE_IDENTITY.toLowerCase(), dispatchedEvent.getSource()); + assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(IdentityConstants.EventSource.UPDATE_IDENTITY, dispatchedEvent.getSource()); assertEquals(map.asXDMMap(), dispatchedEvent.getEventData()); } @Test - public void testUpdateIdentitiesNullAndEmptyMap() { + public void testUpdateIdentitiesNullMap() { // test - IdentityMap map = new IdentityMap(); - Identity.updateIdentities(map); - Identity.updateIdentities(null); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.updateIdentities(null); - // verify none of these API calls dispatch an event - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); - MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); + // verify that no event is dispatched + mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(any(Event.class)), never()); + } } + @Test + public void testUpdateIdentities_EmptyMap() { + // test + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.updateIdentities(new IdentityMap()); + + // verify that no event is dispatched + mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(any(Event.class)), never()); + } + } + + // ======================================================================================== + // removeIdentity API + // ======================================================================================== + @Test public void testRemoveIdentity() { // setup final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - final ArgumentCaptor extensionErrorCallbackCaptor = ArgumentCaptor.forClass( - ExtensionErrorCallback.class - ); - IdentityItem sampleItem = new IdentityItem("sample", AuthenticatedState.AMBIGUOUS, false); + final IdentityItem sampleItem = new IdentityItem("sample", AuthenticatedState.AMBIGUOUS, false); - // test - Identity.removeIdentity(sampleItem, "namespace"); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.removeIdentity(sampleItem, "namespace"); - // verify dispatch event - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEvent(eventCaptor.capture(), extensionErrorCallbackCaptor.capture()); + mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(eventCaptor.capture())); + } - Event dispatchedEvent = eventCaptor.getValue(); + final Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.REMOVE_IDENTITIES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY.toLowerCase(), dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REMOVE_IDENTITY.toLowerCase(), dispatchedEvent.getSource()); - IdentityMap sampleInputIdentitymap = new IdentityMap(); - sampleInputIdentitymap.addItem(sampleItem, "namespace"); - assertEquals(sampleInputIdentitymap.asXDMMap(), dispatchedEvent.getEventData()); - // TODO - enable when ExtensionError creation is available - // should not crash on calling the callback - //extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); + assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(IdentityConstants.EventSource.REMOVE_IDENTITY, dispatchedEvent.getSource()); + + final IdentityMap expectedIdentityMap = new IdentityMap(); + expectedIdentityMap.addItem(sampleItem, "namespace"); + assertEquals(expectedIdentityMap.asXDMMap(), dispatchedEvent.getEventData()); } @Test @@ -532,14 +678,15 @@ public void testRemoveIdentity_WithInvalidInputs() { // setup IdentityItem sampleItem = new IdentityItem("sample", AuthenticatedState.AMBIGUOUS, false); - // test - Identity.removeIdentity(null, "namespace"); - Identity.removeIdentity(sampleItem, ""); - Identity.removeIdentity(sampleItem, null); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + // test + Identity.removeIdentity(null, "namespace"); + Identity.removeIdentity(sampleItem, ""); + Identity.removeIdentity(sampleItem, null); - // verify none of these API calls dispatch an event - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); - MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); + // verify that no event is dispatched + mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(any(Event.class)), never()); + } } // ======================================================================================== @@ -549,36 +696,37 @@ public void testRemoveIdentity_WithInvalidInputs() { public void testGetIdentities() throws Exception { // setup final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); - final ArgumentCaptor extensionErrorCallbackCaptor = ArgumentCaptor.forClass( - ExtensionErrorCallback.class + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class ); final List callbackReturnValues = new ArrayList<>(); - // test - Identity.getIdentities( - new AdobeCallback() { - @Override - public void call(IdentityMap map) { - callbackReturnValues.add(map); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getIdentities( + new AdobeCallback() { + @Override + public void call(IdentityMap map) { + callbackReturnValues.add(map); + } } - } - ); - - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - eventCaptor.capture(), - adobeCallbackCaptor.capture(), - extensionErrorCallbackCaptor.capture() - ); + ); + + // verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } // verify the dispatched event details - Event dispatchedEvent = eventCaptor.getValue(); + final Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.REQUEST_IDENTITIES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY.toLowerCase(), dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY.toLowerCase(), dispatchedEvent.getSource()); - assertTrue(dispatchedEvent.getEventData().isEmpty()); + assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); + assertNull(dispatchedEvent.getEventData()); // verify callback responses final ECID ecid = new ECID(); @@ -608,11 +756,11 @@ public void call(IdentityMap map) { "}"; final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = Utils.toMap(jsonObject); + final Map xdmData = JSONUtils.toMap(jsonObject); adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(xdmData)); - IdentityItem ecidItem = callbackReturnValues.get(0).getIdentityItemsForNamespace("ECID").get(0); - IdentityItem coreItem = callbackReturnValues.get(0).getIdentityItemsForNamespace("CORE").get(0); + final IdentityItem ecidItem = callbackReturnValues.get(0).getIdentityItemsForNamespace("ECID").get(0); + final IdentityItem coreItem = callbackReturnValues.get(0).getIdentityItemsForNamespace("CORE").get(0); assertEquals(ecid.toString(), ecidItem.getId()); assertEquals(AuthenticatedState.AMBIGUOUS, ecidItem.getAuthenticatedState()); @@ -621,141 +769,197 @@ public void call(IdentityMap map) { assertEquals(coreId, coreItem.getId()); assertEquals(AuthenticatedState.AUTHENTICATED, coreItem.getAuthenticatedState()); assertEquals(false, coreItem.isPrimary()); - // TODO - enable when ExtensionError creation is available - // should not crash on calling the callback - //extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); } @Test public void testGetIdentities_nullCallback() { - // test - Identity.getIdentities(null); - - // verify - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(0)); - MobileCore.dispatchEventWithResponseCallback( - any(Event.class), - any(AdobeCallback.class), - any(ExtensionErrorCallback.class) - ); + // setup + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getIdentities(null); + + // verify + mockedStaticMobileCore.verify( + () -> + MobileCore.dispatchEventWithResponseCallback( + any(Event.class), + anyLong(), + any(AdobeCallbackWithError.class) + ), + never() + ); + } } @Test public void testGetIdentities_nullResponseEvent() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override public void call(Object o) {} }; - // test - Identity.getIdentities(callbackWithError); - - // verify if the event is dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - any(Event.class), - adobeCallbackCaptor.capture(), - any(ExtensionErrorCallback.class) - ); + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getIdentities(callbackWithError); + + // verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } // 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)); + assertTrue((boolean) errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } @Test public void testGetIdentities_invalidEventData() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override public void call(Object o) {} }; - // test - Identity.getIdentities(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 + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getIdentities(callbackWithError); + + // verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } + // set response event Map eventData = new HashMap<>(); eventData.put("someKey", "someValue"); adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(eventData)); // verify - assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); - assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + assertTrue((boolean) errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } @Test public void testGetIdentities_missingIdentityMap() { // setup - final String KEY_IS_ERRORCALLBACK_CALLED = "errorCallBackCalled"; - final String KEY_CAPTUREDERRORCALLBACK = "capturedErrorCallback"; + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); final Map errorCapture = new HashMap<>(); - final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass(AdobeCallback.class); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class + ); final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { - errorCapture.put(KEY_IS_ERRORCALLBACK_CALLED, true); - errorCapture.put(KEY_CAPTUREDERRORCALLBACK, adobeError); + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); } @Override public void call(Object o) {} }; - // test - Identity.getIdentities(callbackWithError); - - // verify if the event is dispatched - PowerMockito.verifyStatic(MobileCore.class, Mockito.times(1)); - MobileCore.dispatchEventWithResponseCallback( - any(Event.class), - adobeCallbackCaptor.capture(), - any(ExtensionErrorCallback.class) + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getIdentities(callbackWithError); + + // verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } + // set response event with empty data + Map eventData = new HashMap<>(); + adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(eventData)); + + // verify + assertTrue((boolean) errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); + } + + @Test + public void testGetIdentities_callbackOnFail() { + // setup + final String KEY_IS_ERROR_CALLBACK_CALLED = "errorCallBackCalled"; + final String KEY_CAPTURED_ERROR_CALLBACK = "capturedErrorCallback"; + final ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + final Map errorCapture = new HashMap<>(); + final ArgumentCaptor adobeCallbackCaptor = ArgumentCaptor.forClass( + AdobeCallbackWithError.class ); + final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { + @Override + public void fail(AdobeError adobeError) { + errorCapture.put(KEY_IS_ERROR_CALLBACK_CALLED, true); + errorCapture.put(KEY_CAPTURED_ERROR_CALLBACK, adobeError); + } - // set response event to map missing IdentityMap - Map emptyXDMData = new HashMap<>(); - adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(emptyXDMData)); + @Override + public void call(Object o) {} + }; + + try (MockedStatic mockedStaticMobileCore = Mockito.mockStatic(MobileCore.class)) { + Identity.getIdentities(callbackWithError); + + // verify + mockedStaticMobileCore.verify(() -> + MobileCore.dispatchEventWithResponseCallback( + eventCaptor.capture(), + eq(500L), + adobeCallbackCaptor.capture() + ) + ); + } + // set response event with empty data + adobeCallbackCaptor.getValue().fail(AdobeError.UNEXPECTED_ERROR); // verify - assertTrue((boolean) errorCapture.get(KEY_IS_ERRORCALLBACK_CALLED)); - assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTUREDERRORCALLBACK)); + assertTrue((boolean) errorCapture.get(KEY_IS_ERROR_CALLBACK_CALLED)); + assertEquals(AdobeError.UNEXPECTED_ERROR, errorCapture.get(KEY_CAPTURED_ERROR_CALLBACK)); } // ======================================================================================== - // Private method + // Private methods // ======================================================================================== private Event buildIdentityResponseEvent(final Map eventData) { return new Event.Builder( diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentityTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentityTests.java deleted file mode 100644 index dab33b3d..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRemoveIdentityTests.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - 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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.MobileCore; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -public class ListenerEdgeIdentityRemoveIdentityTests { - - @Mock - private IdentityExtension mockIdentityExtension; - - private ListenerEdgeIdentityRemoveIdentity listener; - private ExecutorService testExecutor; - - @Before - public void setup() { - testExecutor = Executors.newSingleThreadExecutor(); - mockIdentityExtension = Mockito.mock(IdentityExtension.class); - doReturn(testExecutor).when(mockIdentityExtension).getExecutor(); - MobileCore.start(null); - listener = - spy( - new ListenerEdgeIdentityRemoveIdentity( - null, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY - ) - ); - } - - @Test - public void testHear() throws Exception { - // setup - Event event = new Event.Builder( - "Remove Identity", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).processAddEvent(event); - } - - @Test - public void testHear_WhenParentExtensionNull() throws Exception { - // setup - Event event = new Event.Builder( - "Remove Identity", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY - ) - .build(); - doReturn(null).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } - - @Test - public void testHear_WhenEventNull() throws Exception { - // setup - doReturn(null).when(listener).getIdentityExtension(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(null); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } -} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentityTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentityTests.java deleted file mode 100644 index 91f32e66..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityRequestIdentityTests.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - 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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.MobileCore; -import java.util.HashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -public class ListenerEdgeIdentityRequestIdentityTests { - - @Mock - private IdentityExtension mockIdentityExtension; - - private ListenerEdgeIdentityRequestIdentity listener; - private ExecutorService testExecutor; - - @Before - public void setup() { - testExecutor = Executors.newSingleThreadExecutor(); - mockIdentityExtension = Mockito.mock(IdentityExtension.class); - doReturn(testExecutor).when(mockIdentityExtension).getExecutor(); - MobileCore.start(null); - listener = - spy( - new ListenerEdgeIdentityRequestIdentity( - null, - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - ); - } - - @Test - public void testHear() throws Exception { - // setup - Event event = new Event.Builder( - "Request Identity", - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).processAddEvent(event); - } - - @Test - public void testHear_urlVariables() throws Exception { - // setup - Event event = new Event.Builder( - "Request Identity", - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .setEventData( - new HashMap() { - { - put(IdentityConstants.EventDataKeys.URL_VARIABLES, true); - } - } - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).processAddEvent(event); - } - - @Test - public void testHear_WhenParentExtensionNull() throws Exception { - // setup - Event event = new Event.Builder( - "Request Identity", - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build(); - doReturn(null).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } - - @Test - public void testHear_WhenEventNull() throws Exception { - // setup - doReturn(null).when(listener).getIdentityExtension(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(null); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } -} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentityTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentityTests.java deleted file mode 100644 index 6cf77eb0..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEdgeIdentityUpdateIdentityTests.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - 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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.MobileCore; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -public class ListenerEdgeIdentityUpdateIdentityTests { - - @Mock - private IdentityExtension mockIdentityExtension; - - private ListenerEdgeIdentityUpdateIdentity listener; - private ExecutorService testExecutor; - - @Before - public void setup() { - testExecutor = Executors.newSingleThreadExecutor(); - mockIdentityExtension = Mockito.mock(IdentityExtension.class); - doReturn(testExecutor).when(mockIdentityExtension).getExecutor(); - MobileCore.start(null); - listener = - spy( - new ListenerEdgeIdentityUpdateIdentity( - null, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY - ) - ); - } - - @Test - public void testHear() throws Exception { - // setup - Event event = new Event.Builder( - "Update Identities", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).processAddEvent(event); - } - - @Test - public void testHear_WhenParentExtensionNull() throws Exception { - // setup - Event event = new Event.Builder( - "Update Identities", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY - ) - .build(); - doReturn(null).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(event); - } - - @Test - public void testHear_WhenEventNull() throws Exception { - // setup - doReturn(null).when(listener).getIdentityExtension(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(null); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } -} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBootTest.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBootTest.java deleted file mode 100644 index 460229b7..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerEventHubBootTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - 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 static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.MobileCore; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -public class ListenerEventHubBootTest { - - @Mock - private IdentityExtension mockIdentityExtension; - - private ListenerEventHubBoot listener; - private ExecutorService testExecutor; - - @Before - public void setup() { - testExecutor = Executors.newSingleThreadExecutor(); - mockIdentityExtension = Mockito.mock(IdentityExtension.class); - doReturn(testExecutor).when(mockIdentityExtension).getExecutor(); - MobileCore.start(null); - listener = - spy(new ListenerEventHubBoot(null, IdentityConstants.EventType.HUB, IdentityConstants.EventSource.BOOTED)); - } - - @Test - public void testHear() throws Exception { - // setup - Event event = new Event.Builder( - "Event Hub Boot", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.BOOTED - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).bootupIfReady(); - } - - @Test - public void testHear_WhenParentExtensionNull() throws Exception { - // setup - Event event = new Event.Builder( - "Event Hub Boot", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.BOOTED - ) - .build(); - doReturn(null).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).bootupIfReady(); - } -} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedStateTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedStateTests.java deleted file mode 100644 index 3e24eb86..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerHubSharedStateTests.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - 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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.MobileCore; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -public class ListenerHubSharedStateTests { - - @Mock - private IdentityExtension mockIdentityExtension; - - private ListenerHubSharedState listener; - private ExecutorService testExecutor; - - @Before - public void setup() { - testExecutor = Executors.newSingleThreadExecutor(); - mockIdentityExtension = Mockito.mock(IdentityExtension.class); - doReturn(testExecutor).when(mockIdentityExtension).getExecutor(); - MobileCore.start(null); - listener = - spy( - new ListenerHubSharedState( - null, - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ) - ); - } - - @Test - public void testHear() throws Exception { - // setup - Event event = new Event.Builder( - "Shared State Change", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).handleHubSharedState(event); - } - - @Test - public void testHear_WhenParentExtensionNull() throws Exception { - // setup - Event event = new Event.Builder( - "Shared State Change", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ) - .build(); - doReturn(null).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).handleHubSharedState(any(Event.class)); - } - - @Test - public void testHear_WhenEventNull() throws Exception { - // setup - doReturn(null).when(listener).getIdentityExtension(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(null); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).handleHubSharedState(any(Event.class)); - } -} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContentTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContentTests.java deleted file mode 100644 index 008e88a4..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestContentTests.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - 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.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.MobileCore; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -public class ListenerIdentityRequestContentTests { - - @Mock - private IdentityExtension mockIdentityExtension; - - private ListenerIdentityRequestContent listener; - private ExecutorService testExecutor; - - @Before - public void setup() { - testExecutor = Executors.newSingleThreadExecutor(); - mockIdentityExtension = Mockito.mock(IdentityExtension.class); - doReturn(testExecutor).when(mockIdentityExtension).getExecutor(); - MobileCore.start(null); - listener = - spy( - new ListenerIdentityRequestContent( - null, - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_CONTENT - ) - ); - } - - @Test - public void testHear() throws Exception { - // setup - Event event = new Event.Builder( - "Request Identity", - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).processAddEvent(event); - } - - @Test - public void testHear_WhenParentExtensionNull() throws Exception { - // setup - Event event = new Event.Builder( - "Request Identity", - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build(); - doReturn(null).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } - - @Test - public void testHear_WhenEventNull() throws Exception { - // setup - doReturn(null).when(listener).getIdentityExtension(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(null); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } -} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestResetTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestResetTests.java deleted file mode 100644 index 382d78aa..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ListenerIdentityRequestResetTests.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - 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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.MobileCore; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -public class ListenerIdentityRequestResetTests { - - @Mock - private IdentityExtension mockIdentityExtension; - - private ListenerIdentityRequestReset listener; - private ExecutorService testExecutor; - - @Before - public void setup() { - testExecutor = Executors.newSingleThreadExecutor(); - mockIdentityExtension = Mockito.mock(IdentityExtension.class); - doReturn(testExecutor).when(mockIdentityExtension).getExecutor(); - MobileCore.start(null); - listener = - spy( - new ListenerIdentityRequestReset( - null, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_RESET - ) - ); - } - - @Test - public void testHear() throws Exception { - // setup - Event event = new Event.Builder( - "Request Identity", - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(1)).processAddEvent(event); - } - - @Test - public void testHear_WhenParentExtensionNull() throws Exception { - // setup - Event event = new Event.Builder( - "Request Identity", - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build(); - doReturn(null).when(listener).getIdentityExtension(); - - // test - listener.hear(event); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } - - @Test - public void testHear_WhenEventNull() throws Exception { - // setup - doReturn(null).when(listener).getIdentityExtension(); - doReturn(mockIdentityExtension).when(listener).getIdentityExtension(); - - // test - listener.hear(null); - - // verify - testExecutor.awaitTermination(100, TimeUnit.MILLISECONDS); - verify(mockIdentityExtension, times(0)).processAddEvent(any(Event.class)); - } -} diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/MockIdentityState.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/MockIdentityState.java deleted file mode 100644 index bae44e42..00000000 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/MockIdentityState.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - 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 java.util.ArrayList; -import java.util.List; - -/** - * Partial mock class for {@link IdentityState} to be used for testing - */ -class MockIdentityState extends IdentityState { - - MockIdentityState(final IdentityProperties identityProperties) { - super(identityProperties); - } - - int updateCustomerIdentifiersCalledTimes = 0; - List updateCustomerIdentifiersParams = new ArrayList<>(); - - @Override - void updateCustomerIdentifiers(final IdentityMap map) { - updateCustomerIdentifiersCalledTimes++; - updateCustomerIdentifiersParams.add(map); - } - - int removeCustomerIdentifiersCalledTimes = 0; - List removeCustomerIdentifiersParams = new ArrayList<>(); - - @Override - void removeCustomerIdentifiers(final IdentityMap map) { - removeCustomerIdentifiersCalledTimes++; - removeCustomerIdentifiersParams.add(map); - } - - boolean hasBooted = false; - - @Override - boolean hasBooted() { - return hasBooted; - } -} diff --git a/code/edgeidentity/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/code/edgeidentity/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/code/edgeidentity/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file From afd93c54ee4506f25478c2c9616203f545446297 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 12 Dec 2022 13:03:51 -0800 Subject: [PATCH 10/50] Remove duplicate util classes --- .../edge/identity/IdentityAdIdTest.java | 40 ++- .../identity/IdentityAndroidTestUtil.java | 303 ------------------ .../edge/identity/IdentityBootUpTest.java | 10 +- .../identity/IdentityECIDHandlingTest.java | 16 +- .../identity/IdentityFunctionalTestUtil.java | 295 ++++++++++++++++- .../edge/identity/IdentityPublicAPITest.java | 30 +- .../identity/IdentityResetHandlingTest.java | 4 +- 7 files changed, 333 insertions(+), 365 deletions(-) delete mode 100644 code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java index f7e080ee..b9dc704d 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java @@ -11,8 +11,8 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createXDMIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.createXDMIdentityMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.flattenMap; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.setEdgeIdentityPersistence; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getDispatchedEventsWith; @@ -50,8 +50,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenNewValidAdId() throws E String newAdId = "8d9ca5ff-7e74-44ac-bbcd-7aee7baf4f6c"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) + new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), + new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) ) ); @@ -83,8 +83,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenNonAdId() throws Except String initialAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) + new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), + new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -116,8 +116,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenSameValidAdId() throws String newAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) + new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), + new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -148,8 +148,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenEmptyAdId() throws Exce String newAdId = ""; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) + new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), + new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -180,8 +180,8 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E String newAdId = "00000000-0000-0000-0000-000000000000"; setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("GAID", initialAdId) + new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), + new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) ) ); registerEdgeIdentityExtension(); @@ -209,7 +209,9 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exception { // Test String newAdId = "8d9ca5ff-7e74-44ac-bbcd-7aee7baf4f6c"; - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence( + createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) + ); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); @@ -235,7 +237,9 @@ public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exce @Test public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception { // Test - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence( + createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) + ); registerEdgeIdentityExtension(); dispatchGenericIdentityNonAdIdEvent(); @@ -262,7 +266,9 @@ public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Exception { // Test String newAdId = ""; - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence( + createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) + ); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); @@ -288,7 +294,9 @@ public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Excepti public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws Exception { // Test String newAdId = "00000000-0000-0000-0000-000000000000"; - setEdgeIdentityPersistence(createXDMIdentityMap(new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"))); + setEdgeIdentityPersistence( + createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) + ); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java deleted file mode 100644 index 9ef1bd82..00000000 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAndroidTestUtil.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - 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 static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; - -import com.adobe.marketing.mobile.AdobeCallback; -import com.adobe.marketing.mobile.AdobeCallbackWithError; -import com.adobe.marketing.mobile.AdobeError; -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.edge.identity.util.ADBCountDownLatch; -import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; -import com.adobe.marketing.mobile.services.Log; -import com.adobe.marketing.mobile.util.JSONUtils; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.ValueNode; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Util class used by both Functional and Unit tests - */ -public class IdentityAndroidTestUtil { - - private static final String LOG_SOURCE = "IdentityAndroidTestUtil"; - - /** - * Helper method to create IdentityXDM Map using {@link TestItem}s - */ - static Map createXDMIdentityMap(TestItem... items) { - final Map>> allItems = new HashMap<>(); - - for (TestItem item : items) { - final Map itemMap = new HashMap<>(); - itemMap.put(IdentityConstants.XDMKeys.ID, item.id); - itemMap.put(IdentityConstants.XDMKeys.AUTHENTICATED_STATE, "ambiguous"); - itemMap.put(IdentityConstants.XDMKeys.PRIMARY, item.isPrimary); - List> nameSpaceItems = allItems.get(item.namespace); - - if (nameSpaceItems == null) { - nameSpaceItems = new ArrayList<>(); - } - - nameSpaceItems.add(itemMap); - allItems.put(item.namespace, nameSpaceItems); - } - - final Map identityMapDict = new HashMap<>(); - identityMapDict.put(IdentityConstants.XDMKeys.IDENTITY_MAP, allItems); - return identityMapDict; - } - - /** - * Helper method to build remove identity request event with XDM formatted Identity jsonString - */ - static Event buildRemoveIdentityRequestWithJSONString(final String jsonStr) throws Exception { - final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = JSONUtils.toMap(jsonObject); - return buildRemoveIdentityRequest(xdmData); - } - - /** - * Helper method to build remove identity request event with XDM formatted Identity map - */ - static Event buildRemoveIdentityRequest(final Map map) { - return new Event.Builder( - "Remove Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY - ) - .setEventData(map) - .build(); - } - - /** - * Helper method to build update identity request event with XDM formatted Identity jsonString - */ - static Event buildUpdateIdentityRequestJSONString(final String jsonStr) throws Exception { - final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = JSONUtils.toMap(jsonObject); - return buildUpdateIdentityRequest(xdmData); - } - - /** - * Helper method to build update identity request event with XDM formatted Identity map - */ - static Event buildUpdateIdentityRequest(final Map map) { - return new Event.Builder( - "Update Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY - ) - .setEventData(map) - .build(); - } - - /** - * Serialize the given {@code jsonString} to a JSON Object, then flattens to {@code Map}. - * If the provided string is not in JSON structure an {@link JSONException} is thrown. - * - * @param jsonString the string in JSON structure to flatten - * @return new map with flattened structure - */ - static Map flattenJSONString(final String jsonString) throws JSONException { - JSONObject jsonObject = new JSONObject(jsonString); - Map persistenceValueMap = JSONUtils.toMap(jsonObject); - return flattenMap(persistenceValueMap); - } - - /** - * Serialize the given {@code map} to a JSON Object, then flattens to {@code Map}. - * For example, a JSON such as "{xdm: {stitchId: myID, eventType: myType}}" is flattened - * to two map elements "xdm.stitchId" = "myID" and "xdm.eventType" = "myType". - * - * @param map map with JSON structure to flatten - * @return new map with flattened structure - */ - static Map flattenMap(final Map map) { - if (map == null || map.isEmpty()) { - return Collections.emptyMap(); - } - - try { - JSONObject jsonObject = new JSONObject(map); - Map payloadMap = new HashMap<>(); - addKeys("", new ObjectMapper().readTree(jsonObject.toString()), payloadMap); - return payloadMap; - } catch (IOException e) { - Log.error(LOG_TAG, LOG_SOURCE, "Failed to parse JSON object to tree structure."); - } - - return Collections.emptyMap(); - } - - /** - * Deserialize {@code JsonNode} and flatten to provided {@code map}. - * For example, a JSON such as "{xdm: {stitchId: myID, eventType: myType}}" is flattened - * to two map elements "xdm.stitchId" = "myID" and "xdm.eventType" = "myType". - *

- * Method is called recursively. To use, call with an empty path such as - * {@code addKeys("", new ObjectMapper().readTree(JsonNodeAsString), map);} - * - * @param currentPath the path in {@code JsonNode} to process - * @param jsonNode {@link JsonNode} to deserialize - * @param map {@code Map} instance to store flattened JSON result - * @see Stack Overflow post - */ - static void addKeys(String currentPath, JsonNode jsonNode, Map map) { - if (jsonNode.isObject()) { - ObjectNode objectNode = (ObjectNode) jsonNode; - Iterator> iter = objectNode.fields(); - String pathPrefix = currentPath.isEmpty() ? "" : currentPath + "."; - - while (iter.hasNext()) { - Map.Entry entry = iter.next(); - addKeys(pathPrefix + entry.getKey(), entry.getValue(), map); - } - } else if (jsonNode.isArray()) { - ArrayNode arrayNode = (ArrayNode) jsonNode; - - for (int i = 0; i < arrayNode.size(); i++) { - addKeys(currentPath + "[" + i + "]", arrayNode.get(i), map); - } - } else if (jsonNode.isValueNode()) { - ValueNode valueNode = (ValueNode) jsonNode; - map.put(currentPath, valueNode.asText()); - } - } - - /** - * Class similar to {@link IdentityItem} for a specific namespace used for easier testing. - * For simplicity this class does not involve authenticatedState and primary key - */ - static class TestItem { - - private final String namespace; - private final String id; - private final boolean isPrimary = false; - - public TestItem(String namespace, String id) { - this.namespace = namespace; - this.id = id; - } - } - - /** - * Retrieves identities from Identity extension synchronously - * @return a {@code Map} of identities if retrieved successfully; - * null in case of a failure to retrieve it within timeout. - */ - static Map getIdentitiesSync() { - try { - final HashMap getIdentityResponse = new HashMap<>(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - Identity.getIdentities( - new AdobeCallbackWithError() { - @Override - public void call(final IdentityMap identities) { - getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, identities); - latch.countDown(); - } - - @Override - public void fail(final AdobeError adobeError) { - getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.ERROR, adobeError); - latch.countDown(); - } - } - ); - latch.await(2000, TimeUnit.MILLISECONDS); - - return getIdentityResponse; - } catch (Exception exp) { - return null; - } - } - - /** - * Retrieves Experience Cloud Id from Identity extension synchronously - * @return an ECID if retrieved successfully; - * null in case of a failure to retrieve it within timeout. - */ - static String getExperienceCloudIdSync() { - try { - final HashMap getExperienceCloudIdResponse = new HashMap<>(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - Identity.getExperienceCloudId( - new AdobeCallback() { - @Override - public void call(final String ecid) { - getExperienceCloudIdResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, ecid); - latch.countDown(); - } - } - ); - latch.await(2000, TimeUnit.MILLISECONDS); - return getExperienceCloudIdResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); - } catch (Exception exp) { - return null; - } - } - - /** - * Retrieves url variables from Identity extension synchronously - * @return a url variable string if retrieved successfully; - * null in case of a failure to retrieve it within timeout. - */ - static String getUrlVariablesSync() { - try { - final HashMap getUrlVariablesResponse = new HashMap<>(); - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - Identity.getUrlVariables( - new AdobeCallback() { - @Override - public void call(final String urlVariables) { - getUrlVariablesResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, urlVariables); - latch.countDown(); - } - } - ); - latch.await(2000, TimeUnit.MILLISECONDS); - return getUrlVariablesResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); - } catch (Exception exp) { - return null; - } - } - - static IdentityMap createIdentityMap(final String namespace, final String id) { - return createIdentityMap(namespace, id, AuthenticatedState.AMBIGUOUS, false); - } - - static IdentityMap createIdentityMap( - final String namespace, - final String id, - final AuthenticatedState state, - final boolean isPrimary - ) { - IdentityMap map = new IdentityMap(); - IdentityItem item = new IdentityItem(id, state, isPrimary); - map.addItem(item, namespace); - return map; - } -} diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java index 92261db0..ea4827ad 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java @@ -11,8 +11,6 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createXDMIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.*; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; import static org.junit.Assert.assertEquals; @@ -42,10 +40,10 @@ public void testOnBootUp_LoadsAllIdentitiesFromPreference() throws Exception { // test setEdgeIdentityPersistence( createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("ECID", "secondaryECID"), - new IdentityAndroidTestUtil.TestItem("Email", "example@email.com"), - new IdentityAndroidTestUtil.TestItem("UserId", "JohnDoe") + new TestItem("ECID", "primaryECID"), + new TestItem("ECID", "secondaryECID"), + new TestItem("Email", "example@email.com"), + new TestItem("UserId", "JohnDoe") ) ); registerEdgeIdentityExtension(); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java index 870e51ae..7f57e2a9 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java @@ -11,8 +11,6 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createXDMIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.*; import static org.junit.Assert.*; @@ -46,10 +44,7 @@ public void testECID_autoGeneratedWhenBooted() throws InterruptedException { public void testECID_loadedFromPersistence() throws Exception { // setup setEdgeIdentityPersistence( - createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("ECID", "secondaryECID") - ) + createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("ECID", "secondaryECID")) ); registerEdgeIdentityExtension(); @@ -62,7 +57,7 @@ public void testECID_loadedFromPersistence() throws Exception { public void testECID_edgePersistenceTakesPreferenceOverDirectExtension() throws Exception { // setup setIdentityDirectPersistedECID("legacyECID"); - setEdgeIdentityPersistence(IdentityAndroidTestUtil.createIdentityMap("ECID", "edgeECID").asXDMMap()); + setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap()); registerEdgeIdentityExtension(); // verify @@ -116,10 +111,7 @@ public void testECID_whenBothExtensionRegistered_migrationPath() throws Exceptio public void testECID_onResetClearsOldECID() throws Exception { // setup setEdgeIdentityPersistence( - createXDMIdentityMap( - new IdentityAndroidTestUtil.TestItem("ECID", "primaryECID"), - new IdentityAndroidTestUtil.TestItem("ECID", "secondaryECID") - ) + createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("ECID", "secondaryECID")) ); registerEdgeIdentityExtension(); @@ -186,7 +178,7 @@ public void testECID_AreDifferentAfterResetIdentitiesAndPrivacyChange() throws E public void testECID_DirectEcidIsRemovedOnPrivacyOptOut() throws Exception { // setup setIdentityDirectPersistedECID("legacyECID"); - setEdgeIdentityPersistence(IdentityAndroidTestUtil.createIdentityMap("ECID", "edgeECID").asXDMMap()); + setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap()); registerBothIdentityExtensions(); // verify ECID diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java index 29898f95..e1527d86 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java @@ -11,32 +11,48 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; +import static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.resetTestExpectations; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import com.adobe.marketing.mobile.AdobeCallback; +import com.adobe.marketing.mobile.AdobeCallbackWithError; +import com.adobe.marketing.mobile.AdobeError; +import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.edge.identity.util.ADBCountDownLatch; import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; import com.adobe.marketing.mobile.edge.identity.util.TestHelper; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; +import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.JSONUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.json.JSONException; import org.json.JSONObject; public class IdentityFunctionalTestUtil { + private static final String LOG_SOURCE = "IdentityFunctionalTestUtil"; + /** * Register's Edge Identity Extension and start the Core */ - static void registerEdgeIdentityExtension() throws InterruptedException { + public static void registerEdgeIdentityExtension() throws InterruptedException { final ADBCountDownLatch latch = new ADBCountDownLatch(1); MobileCore.registerExtensions(Arrays.asList(Identity.EXTENSION), o -> latch.countDown()); @@ -48,7 +64,7 @@ static void registerEdgeIdentityExtension() throws InterruptedException { /** * Register's Identity Direct and Edge Identity Extension. And then starts the MobileCore */ - static void registerBothIdentityExtensions() throws Exception { + public static void registerBothIdentityExtensions() throws Exception { HashMap config = new HashMap() { { put("global.privacy", "optedin"); @@ -72,7 +88,7 @@ static void registerBothIdentityExtensions() throws Exception { /** * Updates configuration shared state with an orgId */ - static void setupConfiguration() throws Exception { + public static void setupConfiguration() throws Exception { HashMap config = new HashMap() { { put("experienceCloud.org", "testOrg@AdobeOrg"); @@ -85,7 +101,7 @@ static void setupConfiguration() throws Exception { /** * Set the ECID in persistence for Identity Direct extension. */ - static void setIdentityDirectPersistedECID(final String legacyECID) { + public static void setIdentityDirectPersistedECID(final String legacyECID) { TestPersistenceHelper.updatePersistence( IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, @@ -96,7 +112,7 @@ static void setIdentityDirectPersistedECID(final String legacyECID) { /** * Set the persistence data for Edge Identity extension. */ - static void setEdgeIdentityPersistence(final Map persistedData) { + public static void setEdgeIdentityPersistence(final Map persistedData) { if (persistedData != null) { final JSONObject persistedJSON = new JSONObject(persistedData); TestPersistenceHelper.updatePersistence( @@ -110,7 +126,7 @@ static void setEdgeIdentityPersistence(final Map persistedData) /** * Method to get the ECID from Identity Direct extension synchronously. */ - static String getIdentityDirectECIDSync() { + public static String getIdentityDirectECIDSync() { try { final HashMap getExperienceCloudIdResponse = new HashMap<>(); final ADBCountDownLatch latch = new ADBCountDownLatch(1); @@ -131,6 +147,263 @@ public void call(final String ecid) { } } + /** + * Helper method to create IdentityXDM Map using {@link TestItem}s + */ + public static Map createXDMIdentityMap(TestItem... items) { + final Map>> allItems = new HashMap<>(); + + for (TestItem item : items) { + final Map itemMap = new HashMap<>(); + itemMap.put(IdentityConstants.XDMKeys.ID, item.id); + itemMap.put(IdentityConstants.XDMKeys.AUTHENTICATED_STATE, "ambiguous"); + itemMap.put(IdentityConstants.XDMKeys.PRIMARY, item.isPrimary); + List> nameSpaceItems = allItems.get(item.namespace); + + if (nameSpaceItems == null) { + nameSpaceItems = new ArrayList<>(); + } + + nameSpaceItems.add(itemMap); + allItems.put(item.namespace, nameSpaceItems); + } + + final Map identityMapDict = new HashMap<>(); + identityMapDict.put(IdentityConstants.XDMKeys.IDENTITY_MAP, allItems); + return identityMapDict; + } + + /** + * Helper method to build remove identity request event with XDM formatted Identity jsonString + */ + public static Event buildRemoveIdentityRequestWithJSONString(final String jsonStr) throws Exception { + final JSONObject jsonObject = new JSONObject(jsonStr); + final Map xdmData = JSONUtils.toMap(jsonObject); + return buildRemoveIdentityRequest(xdmData); + } + + /** + * Helper method to build remove identity request event with XDM formatted Identity map + */ + public static Event buildRemoveIdentityRequest(final Map map) { + return new Event.Builder( + "Remove Identity Event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.REMOVE_IDENTITY + ) + .setEventData(map) + .build(); + } + + /** + * Helper method to build update identity request event with XDM formatted Identity jsonString + */ + public static Event buildUpdateIdentityRequestJSONString(final String jsonStr) throws Exception { + final JSONObject jsonObject = new JSONObject(jsonStr); + final Map xdmData = JSONUtils.toMap(jsonObject); + return buildUpdateIdentityRequest(xdmData); + } + + /** + * Helper method to build update identity request event with XDM formatted Identity map + */ + public static Event buildUpdateIdentityRequest(final Map map) { + return new Event.Builder( + "Update Identity Event", + IdentityConstants.EventType.EDGE_IDENTITY, + IdentityConstants.EventSource.UPDATE_IDENTITY + ) + .setEventData(map) + .build(); + } + + /** + * Serialize the given {@code jsonString} to a JSON Object, then flattens to {@code Map}. + * If the provided string is not in JSON structure an {@link JSONException} is thrown. + * + * @param jsonString the string in JSON structure to flatten + * @return new map with flattened structure + */ + public static Map flattenJSONString(final String jsonString) throws JSONException { + JSONObject jsonObject = new JSONObject(jsonString); + Map persistenceValueMap = JSONUtils.toMap(jsonObject); + return flattenMap(persistenceValueMap); + } + + /** + * Serialize the given {@code map} to a JSON Object, then flattens to {@code Map}. + * For example, a JSON such as "{xdm: {stitchId: myID, eventType: myType}}" is flattened + * to two map elements "xdm.stitchId" = "myID" and "xdm.eventType" = "myType". + * + * @param map map with JSON structure to flatten + * @return new map with flattened structure + */ + public static Map flattenMap(final Map map) { + if (map == null || map.isEmpty()) { + return Collections.emptyMap(); + } + + try { + JSONObject jsonObject = new JSONObject(map); + Map payloadMap = new HashMap<>(); + addKeys("", new ObjectMapper().readTree(jsonObject.toString()), payloadMap); + return payloadMap; + } catch (IOException e) { + Log.error(LOG_TAG, LOG_SOURCE, "Failed to parse JSON object to tree structure."); + } + + return Collections.emptyMap(); + } + + /** + * Deserialize {@code JsonNode} and flatten to provided {@code map}. + * For example, a JSON such as "{xdm: {stitchId: myID, eventType: myType}}" is flattened + * to two map elements "xdm.stitchId" = "myID" and "xdm.eventType" = "myType". + *

+ * Method is called recursively. To use, call with an empty path such as + * {@code addKeys("", new ObjectMapper().readTree(JsonNodeAsString), map);} + * + * @param currentPath the path in {@code JsonNode} to process + * @param jsonNode {@link JsonNode} to deserialize + * @param map {@code Map} instance to store flattened JSON result + * @see Stack Overflow post + */ + public static void addKeys(String currentPath, JsonNode jsonNode, Map map) { + if (jsonNode.isObject()) { + ObjectNode objectNode = (ObjectNode) jsonNode; + Iterator> iter = objectNode.fields(); + String pathPrefix = currentPath.isEmpty() ? "" : currentPath + "."; + + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + addKeys(pathPrefix + entry.getKey(), entry.getValue(), map); + } + } else if (jsonNode.isArray()) { + ArrayNode arrayNode = (ArrayNode) jsonNode; + + for (int i = 0; i < arrayNode.size(); i++) { + addKeys(currentPath + "[" + i + "]", arrayNode.get(i), map); + } + } else if (jsonNode.isValueNode()) { + ValueNode valueNode = (ValueNode) jsonNode; + map.put(currentPath, valueNode.asText()); + } + } + + /** + * Class similar to {@link IdentityItem} for a specific namespace used for easier testing. + * For simplicity this class does not involve authenticatedState and primary key + */ + public static class TestItem { + + private final String namespace; + private final String id; + private final boolean isPrimary = false; + + public TestItem(String namespace, String id) { + this.namespace = namespace; + this.id = id; + } + } + + /** + * Retrieves identities from Identity extension synchronously + * @return a {@code Map} of identities if retrieved successfully; + * null in case of a failure to retrieve it within timeout. + */ + public static Map getIdentitiesSync() { + try { + final HashMap getIdentityResponse = new HashMap<>(); + final ADBCountDownLatch latch = new ADBCountDownLatch(1); + Identity.getIdentities( + new AdobeCallbackWithError() { + @Override + public void call(final IdentityMap identities) { + getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, identities); + latch.countDown(); + } + + @Override + public void fail(final AdobeError adobeError) { + getIdentityResponse.put(IdentityTestConstants.GetIdentitiesHelper.ERROR, adobeError); + latch.countDown(); + } + } + ); + latch.await(2000, TimeUnit.MILLISECONDS); + + return getIdentityResponse; + } catch (Exception exp) { + return null; + } + } + + /** + * Retrieves Experience Cloud Id from Identity extension synchronously + * @return an ECID if retrieved successfully; + * null in case of a failure to retrieve it within timeout. + */ + public static String getExperienceCloudIdSync() { + try { + final HashMap getExperienceCloudIdResponse = new HashMap<>(); + final ADBCountDownLatch latch = new ADBCountDownLatch(1); + Identity.getExperienceCloudId( + new AdobeCallback() { + @Override + public void call(final String ecid) { + getExperienceCloudIdResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, ecid); + latch.countDown(); + } + } + ); + latch.await(2000, TimeUnit.MILLISECONDS); + return getExperienceCloudIdResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); + } catch (Exception exp) { + return null; + } + } + + /** + * Retrieves url variables from Identity extension synchronously + * @return a url variable string if retrieved successfully; + * null in case of a failure to retrieve it within timeout. + */ + public static String getUrlVariablesSync() { + try { + final HashMap getUrlVariablesResponse = new HashMap<>(); + final ADBCountDownLatch latch = new ADBCountDownLatch(1); + Identity.getUrlVariables( + new AdobeCallback() { + @Override + public void call(final String urlVariables) { + getUrlVariablesResponse.put(IdentityTestConstants.GetIdentitiesHelper.VALUE, urlVariables); + latch.countDown(); + } + } + ); + latch.await(2000, TimeUnit.MILLISECONDS); + return getUrlVariablesResponse.get(IdentityTestConstants.GetIdentitiesHelper.VALUE); + } catch (Exception exp) { + return null; + } + } + + public static IdentityMap createIdentityMap(final String namespace, final String id) { + return createIdentityMap(namespace, id, AuthenticatedState.AMBIGUOUS, false); + } + + public static IdentityMap createIdentityMap( + final String namespace, + final String id, + final AuthenticatedState state, + final boolean isPrimary + ) { + IdentityMap map = new IdentityMap(); + IdentityItem item = new IdentityItem(id, state, isPrimary); + map.addItem(item, namespace); + return map; + } + // -------------------------------------------------------------------------------------------- // Verifiers // -------------------------------------------------------------------------------------------- @@ -139,7 +412,7 @@ public void call(final String ecid) { * Verifies that primary ECID is not null for the Edge Identity extension. * This method checks for the data in shared state, persistence and through getExperienceCloudId API. */ - static void verifyPrimaryECIDNotNull() throws InterruptedException { + public static void verifyPrimaryECIDNotNull() throws InterruptedException { String ecid = getExperienceCloudIdSync(); assertNotNull(ecid); @@ -152,7 +425,7 @@ static void verifyPrimaryECIDNotNull() throws InterruptedException { * Verifies that primary ECID for the Edge Identity Extension is equal to the value provided. * This method checks for the data in shared state, persistence and through getExperienceCloudId API. */ - static void verifyPrimaryECID(final String primaryECID) throws Exception { + public static void verifyPrimaryECID(final String primaryECID) throws Exception { String ecid = getExperienceCloudIdSync(); assertEquals(primaryECID, ecid); @@ -173,7 +446,7 @@ static void verifyPrimaryECID(final String primaryECID) throws Exception { * Verifies that secondary ECID for the Edge Identity Extension is equal to the value provided * This method checks for the data in shared state and persistence. */ - static void verifySecondaryECID(final String secondaryECID) throws Exception { + public static void verifySecondaryECID(final String secondaryECID) throws Exception { // verify xdm shared state is has correct secondary ECID Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); assertEquals(secondaryECID, xdmSharedState.get("identityMap.ECID[1].id")); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java index 27df8d6e..bf584920 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java @@ -11,11 +11,11 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.createIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getIdentitiesSync; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getUrlVariablesSync; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.createIdentityMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getExperienceCloudIdSync; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getIdentitiesSync; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getUrlVariablesSync; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.setupConfiguration; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; @@ -153,7 +153,7 @@ public void testUpdateAPI_emptyData() throws Exception { @Test public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { // test - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("Email", "example@email.com")); + Identity.updateIdentities(createIdentityMap("Email", "example@email.com")); Identity.updateIdentities( createIdentityMap("Email", "example@email.com", AuthenticatedState.AUTHENTICATED, true) ); @@ -184,13 +184,13 @@ public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { @Test public void testUpdateAPI_withReservedNamespaces() throws Exception { // test - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("ECID", "newECID")); - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("GAID", "")); - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("IDFA", "")); - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("IDFa", "")); - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("gaid", "")); - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("ecid", "")); - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("idfa", "")); + Identity.updateIdentities(createIdentityMap("ECID", "newECID")); + Identity.updateIdentities(createIdentityMap("GAID", "")); + Identity.updateIdentities(createIdentityMap("IDFA", "")); + Identity.updateIdentities(createIdentityMap("IDFa", "")); + Identity.updateIdentities(createIdentityMap("gaid", "")); + Identity.updateIdentities(createIdentityMap("ecid", "")); + Identity.updateIdentities(createIdentityMap("idfa", "")); TestHelper.waitForThreads(2000); // verify xdm shared state does not get updated @@ -415,7 +415,7 @@ public void testRemoveIdentity_nonExistentNamespace() throws Exception { public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { // setup // update Identities through API - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("Email", "example@email.com")); + Identity.updateIdentities(createIdentityMap("Email", "example@email.com")); // test Identity.removeIdentity(new IdentityItem("example@email.com"), "email"); @@ -439,7 +439,7 @@ public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { public void testRemoveIdentity_nonExistentItem() throws Exception { // setup // update Identities through API - Identity.updateIdentities(IdentityAndroidTestUtil.createIdentityMap("Email", "example@email.com")); + Identity.updateIdentities(createIdentityMap("Email", "example@email.com")); // test Identity.removeIdentity(new IdentityItem("secondary@email.com"), "Email"); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java index a1810a70..d51825b5 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java @@ -11,8 +11,8 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.flattenMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityAndroidTestUtil.getExperienceCloudIdSync; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.flattenMap; +import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getExperienceCloudIdSync; import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.*; From 16200d789c7c3e67eb99cbf0b52d4b4c634864ee Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Mon, 12 Dec 2022 13:09:24 -0800 Subject: [PATCH 11/50] Discard Identity test constants and change copyright year --- .../identity/IdentityFunctionalTestUtil.java | 2 +- .../edge/identity/IdentityTestConstants.java | 40 ------------------- 2 files changed, 1 insertion(+), 41 deletions(-) delete mode 100644 code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestConstants.java diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java index e1527d86..66ac4ea2 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java @@ -1,5 +1,5 @@ /* - Copyright 2021 Adobe. All rights reserved. + 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 diff --git a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestConstants.java b/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestConstants.java deleted file mode 100644 index 2c217b7c..00000000 --- a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestConstants.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - 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; - -public class IdentityTestConstants { - - public final class DataStoreKey { - - public static final String CONFIG_DATASTORE = "AdobeMobile_ConfigState"; - public static final String IDENTITY_DATASTORE = "com.adobe.edge.identity"; - public static final String IDENTITY_DIRECT_DATASTORE = "visitorIDServiceDataStore"; - - private DataStoreKey() {} - } - - public final class SharedStateName { - - public static final String CONFIG = "com.adobe.module.configuration"; - public static final String EVENT_HUB = "com.adobe.module.eventhub"; - - private SharedStateName() {} - } - - public final class GetIdentitiesHelper { - - public static final String VALUE = "getConsentValue"; - public static final String ERROR = "getConsentError"; - - private GetIdentitiesHelper() {} - } -} From 50afa2ab8918e0231ac6aaa32596e9b0045ca75c Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 13 Dec 2022 09:10:16 -0800 Subject: [PATCH 12/50] Make imports consistent and unify test constants --- .../edge/identity/IdentityAdIdTest.java | 76 ++++++------------- .../edge/identity/IdentityBootUpTest.java | 2 +- .../identity/IdentityECIDHandlingTest.java | 2 +- .../edge/identity/IdentityPublicAPITest.java | 41 ++++------ .../identity/IdentityResetHandlingTest.java | 9 +-- .../IdentityFunctionalTestUtil.java | 61 ++++++++------- .../identity/util/IdentityTestConstants.java | 45 ++++++++++- .../edge/identity/util/MonitorExtension.java | 24 +++--- .../edge/identity/util/TestConstants.java | 44 ----------- .../mobile/edge/identity/util/TestHelper.java | 12 +-- .../edge/identity/IdentityStateTests.java | 32 ++++---- .../mobile/edge/identity/IdentityTests.java | 52 ++++++++++--- 12 files changed, 195 insertions(+), 205 deletions(-) rename code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/{ => util}/IdentityFunctionalTestUtil.java (89%) delete mode 100644 code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestConstants.java diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java index b9dc704d..332ce3d5 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java @@ -11,19 +11,14 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.createXDMIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.flattenMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.setEdgeIdentityPersistence; -import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getDispatchedEventsWith; -import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; +import static com.adobe.marketing.mobile.edge.identity.util.IdentityFunctionalTestUtil.*; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.assertEquals; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.MobileCore; -import com.adobe.marketing.mobile.edge.identity.util.TestHelper; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; import com.adobe.marketing.mobile.util.JSONUtils; import com.adobe.marketing.mobile.util.StringUtils; @@ -38,9 +33,7 @@ public class IdentityAdIdTest { @Rule - public RuleChain rule = RuleChain - .outerRule(new TestHelper.SetupCoreRule()) - .around(new TestHelper.RegisterMonitorExtensionRule()); + public RuleChain rule = RuleChain.outerRule(new SetupCoreRule()).around(new RegisterMonitorExtensionRule()); @Test public void testGenericIdentityRequest_whenValidAdId_thenNewValidAdId() throws Exception { @@ -49,17 +42,14 @@ public void testGenericIdentityRequest_whenValidAdId_thenNewValidAdId() throws E String initialAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; String newAdId = "8d9ca5ff-7e74-44ac-bbcd-7aee7baf4f6c"; setEdgeIdentityPersistence( - createXDMIdentityMap( - new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), - new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) - ) + createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); // After sending mobile core event, give a wait time to allow for processing - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should not be dispatched; valid -> valid does not signal change in consent verifyDispatchedEvents(true, null); @@ -82,16 +72,13 @@ public void testGenericIdentityRequest_whenValidAdId_thenNonAdId() throws Except // Test String initialAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; setEdgeIdentityPersistence( - createXDMIdentityMap( - new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), - new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) - ) + createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); registerEdgeIdentityExtension(); dispatchGenericIdentityNonAdIdEvent(); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should not be dispatched; valid -> (unchanged) valid does not signal change in consent verifyDispatchedEvents(false, null); @@ -115,15 +102,12 @@ public void testGenericIdentityRequest_whenValidAdId_thenSameValidAdId() throws String initialAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; String newAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; setEdgeIdentityPersistence( - createXDMIdentityMap( - new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), - new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) - ) + createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should not be dispatched; valid -> valid does not signal change in consent verifyDispatchedEvents(true, null); @@ -147,15 +131,12 @@ public void testGenericIdentityRequest_whenValidAdId_thenEmptyAdId() throws Exce String initialAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; String newAdId = ""; setEdgeIdentityPersistence( - createXDMIdentityMap( - new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), - new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) - ) + createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should be dispatched; valid -> invalid signals change in consent verifyDispatchedEvents(true, "n"); @@ -179,15 +160,12 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E String initialAdId = "fa181743-2520-4ebc-b125-626baf1e3db8"; String newAdId = "00000000-0000-0000-0000-000000000000"; setEdgeIdentityPersistence( - createXDMIdentityMap( - new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID"), - new IdentityFunctionalTestUtil.TestItem("GAID", initialAdId) - ) + createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should be dispatched; valid -> invalid signals change in consent verifyDispatchedEvents(true, "n"); @@ -209,13 +187,11 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exception { // Test String newAdId = "8d9ca5ff-7e74-44ac-bbcd-7aee7baf4f6c"; - setEdgeIdentityPersistence( - createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) - ); + setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Generic Identity event containing advertisingIdentifier should be dispatched // Edge Consent event should not be dispatched; valid -> valid does not signal change in consent @@ -237,14 +213,12 @@ public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exce @Test public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception { // Test - setEdgeIdentityPersistence( - createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) - ); + setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); dispatchGenericIdentityNonAdIdEvent(); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should not be dispatched; valid -> valid does not signal change in consent verifyDispatchedEvents(false, null); @@ -266,13 +240,11 @@ public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Exception { // Test String newAdId = ""; - setEdgeIdentityPersistence( - createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) - ); + setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should not be dispatched; invalid -> invalid does not signal change in consent verifyDispatchedEvents(true, null); @@ -294,13 +266,11 @@ public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Excepti public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws Exception { // Test String newAdId = "00000000-0000-0000-0000-000000000000"; - setEdgeIdentityPersistence( - createXDMIdentityMap(new IdentityFunctionalTestUtil.TestItem("ECID", "primaryECID")) - ); + setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); registerEdgeIdentityExtension(); MobileCore.setAdvertisingIdentifier(newAdId); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should not be dispatched; invalid -> invalid does not signal change in consent verifyDispatchedEvents(true, null); @@ -318,10 +288,10 @@ public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws verifyFlatIdentityMap(persistedMap, null); // Reset wildcard listener - TestHelper.resetTestExpectations(); + resetTestExpectations(); // Test all zeros sent again MobileCore.setAdvertisingIdentifier(newAdId); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // Verify dispatched events // Edge Consent event should not be dispatched; invalid -> invalid does not signal change in consent verifyDispatchedEvents(true, null); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java index ea4827ad..b30d4963 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java @@ -11,7 +11,7 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.*; +import static com.adobe.marketing.mobile.edge.identity.util.IdentityFunctionalTestUtil.*; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; import static org.junit.Assert.assertEquals; diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java index 7f57e2a9..9d70c65e 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java @@ -11,7 +11,7 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.*; +import static com.adobe.marketing.mobile.edge.identity.util.IdentityFunctionalTestUtil.*; import static org.junit.Assert.*; import androidx.test.ext.junit.runners.AndroidJUnit4; diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java index bf584920..9278a0c1 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java @@ -11,20 +11,13 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.createIdentityMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.flattenMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getExperienceCloudIdSync; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getIdentitiesSync; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getUrlVariablesSync; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.setupConfiguration; +import static com.adobe.marketing.mobile.edge.identity.util.IdentityFunctionalTestUtil.*; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.*; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; -import com.adobe.marketing.mobile.edge.identity.util.TestHelper; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; import com.adobe.marketing.mobile.util.JSONUtils; import java.util.List; @@ -40,9 +33,7 @@ public class IdentityPublicAPITest { @Rule - public RuleChain rule = RuleChain - .outerRule(new TestHelper.SetupCoreRule()) - .around(new TestHelper.RegisterMonitorExtensionRule()); + public RuleChain rule = RuleChain.outerRule(new SetupCoreRule()).around(new RegisterMonitorExtensionRule()); // -------------------------------------------------------------------------------------------- // Setup @@ -91,7 +82,7 @@ public void testUpdateIdentitiesAPI() throws Exception { Identity.updateIdentities( createIdentityMap("Email", "example@email.com", AuthenticatedState.AUTHENTICATED, true) ); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify xdm shared state Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); @@ -116,7 +107,7 @@ public void testUpdateIdentitiesAPI() throws Exception { public void testUpdateAPI_nullData() throws Exception { // test Identity.updateIdentities(null); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify no shares state change event dispatched List dispatchedEvents = getDispatchedEventsWith( @@ -135,7 +126,7 @@ public void testUpdateAPI_nullData() throws Exception { public void testUpdateAPI_emptyData() throws Exception { // test Identity.updateIdentities(new IdentityMap()); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify no shares state change event dispatched List dispatchedEvents = getDispatchedEventsWith( @@ -160,7 +151,7 @@ public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { Identity.updateIdentities( createIdentityMap("Email", "example@email.com", AuthenticatedState.LOGGED_OUT, false) ); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify the final xdm shared state Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); @@ -191,7 +182,7 @@ public void testUpdateAPI_withReservedNamespaces() throws Exception { Identity.updateIdentities(createIdentityMap("gaid", "")); Identity.updateIdentities(createIdentityMap("ecid", "")); Identity.updateIdentities(createIdentityMap("idfa", "")); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify xdm shared state does not get updated Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); @@ -217,7 +208,7 @@ public void testUpdateAPI_multipleNamespaceMap() throws Exception { map.addItem(new IdentityItem("zzzyyyxxx"), "UserId"); map.addItem(new IdentityItem("John Doe"), "UserName"); Identity.updateIdentities(map); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify xdm shared state Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); @@ -247,7 +238,7 @@ public void testUpdateAPI_caseSensitiveNamespacesForCustomIdentifiers() throws E map.addItem(new IdentityItem("primary@email.com"), "Email"); map.addItem(new IdentityItem("secondary@email.com"), "email"); Identity.updateIdentities(map); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify xdm shared state Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); @@ -329,7 +320,7 @@ public void testGetIdentities() { // test Map getIdentitiesResponse = getIdentitiesSync(); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify IdentityMap responseMap = (IdentityMap) getIdentitiesResponse.get( @@ -367,7 +358,7 @@ public void testRemoveIdentity() throws Exception { // test Identity.removeIdentity(new IdentityItem("primary@email.com"), "Email"); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify xdm shared state Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); @@ -376,7 +367,7 @@ public void testRemoveIdentity() throws Exception { // test again Identity.removeIdentity(new IdentityItem("secondary@email.com"), "Email"); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify xdm shared state xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); @@ -395,7 +386,7 @@ public void testRemoveIdentity() throws Exception { public void testRemoveIdentity_nonExistentNamespace() throws Exception { // test Identity.removeIdentity(new IdentityItem("primary@email.com"), "Email"); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify item is not removed // verify xdm shared state @@ -419,7 +410,7 @@ public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { // test Identity.removeIdentity(new IdentityItem("example@email.com"), "email"); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify item is not removed // verify xdm shared state @@ -443,7 +434,7 @@ public void testRemoveIdentity_nonExistentItem() throws Exception { // test Identity.removeIdentity(new IdentityItem("secondary@email.com"), "Email"); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // verify item is not removed // verify xdm shared state @@ -466,7 +457,7 @@ public void testRemoveIdentity_doesNotRemoveECID() throws Exception { // attempt to remove ECID Identity.removeIdentity(new IdentityItem(currentECID), "ECID"); - TestHelper.waitForThreads(2000); + waitForThreads(2000); // ECID is a reserved namespace and should not be removed // verify xdm shared state diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java index d51825b5..d223aac7 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java @@ -11,15 +11,12 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.flattenMap; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.getExperienceCloudIdSync; -import static com.adobe.marketing.mobile.edge.identity.IdentityFunctionalTestUtil.registerEdgeIdentityExtension; +import static com.adobe.marketing.mobile.edge.identity.util.IdentityFunctionalTestUtil.*; import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.*; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.MobileCore; -import com.adobe.marketing.mobile.edge.identity.util.TestHelper; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; import com.adobe.marketing.mobile.util.JSONUtils; import java.util.List; @@ -33,9 +30,7 @@ public class IdentityResetHandlingTest { @Rule - public RuleChain rule = RuleChain - .outerRule(new TestHelper.SetupCoreRule()) - .around(new TestHelper.RegisterMonitorExtensionRule()); + public RuleChain rule = RuleChain.outerRule(new SetupCoreRule()).around(new RegisterMonitorExtensionRule()); // -------------------------------------------------------------------------------------------- // Setup diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java similarity index 89% rename from code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java rename to code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java index 66ac4ea2..a0ddd758 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java @@ -9,11 +9,10 @@ governing permissions and limitations under the License. */ -package com.adobe.marketing.mobile.edge.identity; +package com.adobe.marketing.mobile.edge.identity.util; import static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; -import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; -import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.resetTestExpectations; +import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -21,11 +20,13 @@ import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.MobileCore; -import com.adobe.marketing.mobile.edge.identity.util.ADBCountDownLatch; -import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; -import com.adobe.marketing.mobile.edge.identity.util.TestHelper; -import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; +import com.adobe.marketing.mobile.edge.identity.AuthenticatedState; +import com.adobe.marketing.mobile.edge.identity.Identity; +import com.adobe.marketing.mobile.edge.identity.IdentityItem; +import com.adobe.marketing.mobile.edge.identity.IdentityMap; import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.JSONUtils; import com.fasterxml.jackson.databind.JsonNode; @@ -103,8 +104,8 @@ public static void setupConfiguration() throws Exception { */ public static void setIdentityDirectPersistedECID(final String legacyECID) { TestPersistenceHelper.updatePersistence( - IdentityConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE_NAME, - IdentityConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, + IdentityTestConstants.DataStoreKey.IDENTITY_DIRECT_DATASTORE, + IdentityTestConstants.DataStoreKey.IDENTITY_DIRECT_ECID_KEY, legacyECID ); } @@ -116,8 +117,8 @@ public static void setEdgeIdentityPersistence(final Map persiste if (persistedData != null) { final JSONObject persistedJSON = new JSONObject(persistedData); TestPersistenceHelper.updatePersistence( - IdentityConstants.DataStoreKey.DATASTORE_NAME, - IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, + IdentityTestConstants.DataStoreKey.IDENTITY_DATASTORE, + IdentityTestConstants.DataStoreKey.IDENTITY_PROPERTIES, persistedJSON.toString() ); } @@ -155,9 +156,9 @@ public static Map createXDMIdentityMap(TestItem... items) { for (TestItem item : items) { final Map itemMap = new HashMap<>(); - itemMap.put(IdentityConstants.XDMKeys.ID, item.id); - itemMap.put(IdentityConstants.XDMKeys.AUTHENTICATED_STATE, "ambiguous"); - itemMap.put(IdentityConstants.XDMKeys.PRIMARY, item.isPrimary); + itemMap.put(IdentityTestConstants.XDMKeys.ID, item.id); + itemMap.put(IdentityTestConstants.XDMKeys.AUTHENTICATED_STATE, "ambiguous"); + itemMap.put(IdentityTestConstants.XDMKeys.PRIMARY, item.isPrimary); List> nameSpaceItems = allItems.get(item.namespace); if (nameSpaceItems == null) { @@ -169,7 +170,7 @@ public static Map createXDMIdentityMap(TestItem... items) { } final Map identityMapDict = new HashMap<>(); - identityMapDict.put(IdentityConstants.XDMKeys.IDENTITY_MAP, allItems); + identityMapDict.put(IdentityTestConstants.XDMKeys.IDENTITY_MAP, allItems); return identityMapDict; } @@ -188,8 +189,8 @@ public static Event buildRemoveIdentityRequestWithJSONString(final String jsonSt public static Event buildRemoveIdentityRequest(final Map map) { return new Event.Builder( "Remove Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY + EventType.EDGE_IDENTITY, + "om.adobe.eventSource.updateIdentity" ) .setEventData(map) .build(); @@ -208,11 +209,7 @@ public static Event buildUpdateIdentityRequestJSONString(final String jsonStr) t * Helper method to build update identity request event with XDM formatted Identity map */ public static Event buildUpdateIdentityRequest(final Map map) { - return new Event.Builder( - "Update Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY - ) + return new Event.Builder("Update Identity Event", EventType.EDGE_IDENTITY, EventSource.UPDATE_IDENTITY) .setEventData(map) .build(); } @@ -417,7 +414,9 @@ public static void verifyPrimaryECIDNotNull() throws InterruptedException { assertNotNull(ecid); // verify xdm shared state is has ECID - Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); + Map xdmSharedState = flattenMap( + getXDMSharedStateFor(IdentityTestConstants.EXTENSION_NAME, 1000) + ); assertNotNull(xdmSharedState.get("identityMap.ECID[0].id")); } @@ -430,13 +429,15 @@ public static void verifyPrimaryECID(final String primaryECID) throws Exception assertEquals(primaryECID, ecid); // verify xdm shared state is has correct primary ECID - Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); + Map xdmSharedState = flattenMap( + getXDMSharedStateFor(IdentityTestConstants.EXTENSION_NAME, 1000) + ); assertEquals(primaryECID, xdmSharedState.get("identityMap.ECID[0].id")); // verify primary ECID in persistence final String persistedJson = TestPersistenceHelper.readPersistedData( - IdentityConstants.DataStoreKey.DATASTORE_NAME, - IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES + IdentityTestConstants.DataStoreKey.IDENTITY_DATASTORE, + IdentityTestConstants.DataStoreKey.IDENTITY_PROPERTIES ); Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(primaryECID, persistedMap.get("identityMap.ECID[0].id")); @@ -448,13 +449,15 @@ public static void verifyPrimaryECID(final String primaryECID) throws Exception */ public static void verifySecondaryECID(final String secondaryECID) throws Exception { // verify xdm shared state is has correct secondary ECID - Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); + Map xdmSharedState = flattenMap( + getXDMSharedStateFor(IdentityTestConstants.EXTENSION_NAME, 1000) + ); assertEquals(secondaryECID, xdmSharedState.get("identityMap.ECID[1].id")); // verify secondary ECID in persistence final String persistedJson = TestPersistenceHelper.readPersistedData( - IdentityConstants.DataStoreKey.DATASTORE_NAME, - IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES + IdentityTestConstants.DataStoreKey.IDENTITY_DATASTORE, + IdentityTestConstants.DataStoreKey.IDENTITY_PROPERTIES ); Map persistedMap = flattenMap(JSONUtils.toMap(new JSONObject(persistedJson))); assertEquals(secondaryECID, persistedMap.get("identityMap.ECID[1].id")); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java index 9e6cffc5..1a414924 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityTestConstants.java @@ -14,12 +14,15 @@ public class IdentityTestConstants { public static final String LOG_TAG = "EdgeIdentity"; + static final String EXTENSION_NAME = "com.adobe.edge.identity"; - public static final class DataStoreKey { + static final class DataStoreKey { - public static final String CONFIG_DATASTORE = "AdobeMobile_ConfigState"; - public static final String IDENTITY_DATASTORE = "com.adobe.edge.identity"; + static final String CONFIG_DATASTORE = "AdobeMobile_ConfigState"; + static final String IDENTITY_DATASTORE = "com.adobe.edge.identity"; public static final String IDENTITY_DIRECT_DATASTORE = "visitorIDServiceDataStore"; + public static final String IDENTITY_DIRECT_ECID_KEY = "ADOBEMOBILE_PERSISTED_MID"; + public static final String IDENTITY_PROPERTIES = "identity.properties"; private DataStoreKey() {} } @@ -39,4 +42,40 @@ public static final class GetIdentitiesHelper { private GetIdentitiesHelper() {} } + + public static class EventType { + + static final String MONITOR = "com.adobe.functional.eventType.monitor"; + + private EventType() {} + } + + public static class EventSource { + + // Used by Monitor Extension + static final String XDM_SHARED_STATE_REQUEST = "com.adobe.eventSource.xdmsharedStateRequest"; + static final String XDM_SHARED_STATE_RESPONSE = "com.adobe.eventSource.xdmsharedStateResponse"; + static final String SHARED_STATE_REQUEST = "com.adobe.eventSource.sharedStateRequest"; + static final String SHARED_STATE_RESPONSE = "com.adobe.eventSource.sharedStateResponse"; + static final String UNREGISTER = "com.adobe.eventSource.unregister"; + + private EventSource() {} + } + + public static class EventDataKey { + + static final String STATE_OWNER = "stateowner"; + + private EventDataKey() {} + } + + static final class XDMKeys { + + static final String IDENTITY_MAP = "identityMap"; + static final String ID = "id"; + static final String AUTHENTICATED_STATE = "authenticatedState"; + static final String PRIMARY = "primary"; + + private XDMKeys() {} + } } diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java index 9a4d95e9..372591db 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java @@ -83,8 +83,8 @@ public void error(ExtensionError extensionError) { public static void unregisterExtension() { Event event = new Event.Builder( "Unregister Monitor Extension Request", - TestConstants.EventType.MONITOR, - TestConstants.EventSource.UNREGISTER + IdentityTestConstants.EventType.MONITOR, + IdentityTestConstants.EventSource.UNREGISTER ) .build(); MobileCore.dispatchEvent(event); @@ -128,12 +128,12 @@ public static void reset() { * @param event the event to be processed */ public void wildcardProcessor(final Event event) { - if (TestConstants.EventType.MONITOR.equalsIgnoreCase(event.getType())) { - if (TestConstants.EventSource.SHARED_STATE_REQUEST.equalsIgnoreCase(event.getSource())) { + if (IdentityTestConstants.EventType.MONITOR.equalsIgnoreCase(event.getType())) { + if (IdentityTestConstants.EventSource.SHARED_STATE_REQUEST.equalsIgnoreCase(event.getSource())) { processSharedStateRequest(event); - } else if (TestConstants.EventSource.XDM_SHARED_STATE_REQUEST.equalsIgnoreCase(event.getSource())) { + } else if (IdentityTestConstants.EventSource.XDM_SHARED_STATE_REQUEST.equalsIgnoreCase(event.getSource())) { processXDMSharedStateRequest(event); - } else if (TestConstants.EventSource.UNREGISTER.equalsIgnoreCase(event.getSource())) { + } else if (IdentityTestConstants.EventSource.UNREGISTER.equalsIgnoreCase(event.getSource())) { processUnregisterRequest(event); } @@ -176,7 +176,7 @@ private void processXDMSharedStateRequest(final Event event) { return; } - final String stateOwner = DataReader.optString(eventData, TestConstants.EventDataKey.STATE_OWNER, null); + final String stateOwner = DataReader.optString(eventData, IdentityTestConstants.EventDataKey.STATE_OWNER, null); if (stateOwner == null) { return; @@ -187,8 +187,8 @@ private void processXDMSharedStateRequest(final Event event) { Event responseEvent = new Event.Builder( "Get Shared State Response", - TestConstants.EventType.MONITOR, - TestConstants.EventSource.XDM_SHARED_STATE_RESPONSE + IdentityTestConstants.EventType.MONITOR, + IdentityTestConstants.EventSource.XDM_SHARED_STATE_RESPONSE ) .setEventData(sharedStateResult == null ? null : sharedStateResult.getValue()) .inResponseToEvent(event) @@ -209,7 +209,7 @@ private void processSharedStateRequest(final Event event) { return; } - final String stateOwner = DataReader.optString(eventData, TestConstants.EventDataKey.STATE_OWNER, null); + final String stateOwner = DataReader.optString(eventData, IdentityTestConstants.EventDataKey.STATE_OWNER, null); if (stateOwner == null) { return; @@ -220,8 +220,8 @@ private void processSharedStateRequest(final Event event) { Event responseEvent = new Event.Builder( "Get Shared State Response", - TestConstants.EventType.MONITOR, - TestConstants.EventSource.SHARED_STATE_RESPONSE + IdentityTestConstants.EventType.MONITOR, + IdentityTestConstants.EventSource.SHARED_STATE_RESPONSE ) .setEventData(sharedStateResult == null ? null : sharedStateResult.getValue()) .inResponseToEvent(event) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestConstants.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestConstants.java deleted file mode 100644 index 69045499..00000000 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestConstants.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - 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.util; - -/** - * Class to maintain test constants. - */ -public class TestConstants { - - public class EventType { - - static final String MONITOR = "com.adobe.functional.eventType.monitor"; - - private EventType() {} - } - - public class EventSource { - - // Used by Monitor Extension - static final String XDM_SHARED_STATE_REQUEST = "com.adobe.eventSource.xdmsharedStateRequest"; - static final String XDM_SHARED_STATE_RESPONSE = "com.adobe.eventSource.xdmsharedStateResponse"; - static final String SHARED_STATE_REQUEST = "com.adobe.eventSource.sharedStateRequest"; - static final String SHARED_STATE_RESPONSE = "com.adobe.eventSource.sharedStateResponse"; - static final String UNREGISTER = "com.adobe.eventSource.unregister"; - - private EventSource() {} - } - - public class EventDataKey { - - static final String STATE_OWNER = "stateowner"; - - private EventDataKey() {} - } -} diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java index 1e8c5b5f..d7f6ca4d 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java @@ -454,13 +454,13 @@ public static Map getSharedStateFor(final String stateOwner, int throws InterruptedException { Event event = new Event.Builder( "Get Shared State Request", - TestConstants.EventType.MONITOR, - TestConstants.EventSource.SHARED_STATE_REQUEST + IdentityTestConstants.EventType.MONITOR, + IdentityTestConstants.EventSource.SHARED_STATE_REQUEST ) .setEventData( new HashMap() { { - put(TestConstants.EventDataKey.STATE_OWNER, stateOwner); + put(IdentityTestConstants.EventDataKey.STATE_OWNER, stateOwner); } } ) @@ -505,13 +505,13 @@ public static Map getXDMSharedStateFor(final String stateOwner, throws InterruptedException { Event event = new Event.Builder( "Get Shared State Request", - TestConstants.EventType.MONITOR, - TestConstants.EventSource.XDM_SHARED_STATE_REQUEST + IdentityTestConstants.EventType.MONITOR, + IdentityTestConstants.EventSource.XDM_SHARED_STATE_REQUEST ) .setEventData( new HashMap() { { - put(TestConstants.EventDataKey.STATE_OWNER, stateOwner); + put(IdentityTestConstants.EventDataKey.STATE_OWNER, stateOwner); } } ) diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java index 59d19029..2b0b3ff8 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java @@ -399,19 +399,25 @@ public void testResetIdentifiers() { state.getIdentityProperties().setAdId("adID"); final ECID existingEcid = state.getIdentityProperties().getECID(); - // test - state.resetIdentifiers(); - - // verify - assertNotEquals(existingEcid, state.getIdentityProperties().getECID()); // ECID should be regenerated - assertFalse(state.getIdentityProperties().getECID().toString().isEmpty()); // ECID should not be empty - assertNull(state.getIdentityProperties().getECIDSecondary()); // should be cleared - assertNull(state.getIdentityProperties().getAdId()); // should be cleared - verify(mockIdentityStorageManager, times(1)).savePropertiesToPersistence(state.getIdentityProperties()); // should save to data store - // Verify consent event not sent (or any event). Consent should not be dispatched by resetIdentifiers - - // PowerMockito.verifyStatic(MobileCore.class, Mockito.never()); - // MobileCore.dispatchEvent(any(Event.class), any(ExtensionErrorCallback.class)); + try (MockedStatic mockedStaticCore = Mockito.mockStatic(MobileCore.class)) { + // test + state.resetIdentifiers(); + + // verify + assertNotEquals(existingEcid, state.getIdentityProperties().getECID()); // ECID should be regenerated + assertFalse(state.getIdentityProperties().getECID().toString().isEmpty()); // ECID should not be empty + assertNull(state.getIdentityProperties().getECIDSecondary()); // should be cleared + assertNull(state.getIdentityProperties().getAdId()); // should be cleared + verify(mockIdentityStorageManager, times(1)).savePropertiesToPersistence(state.getIdentityProperties()); // should save to data store + + // Verify consent event not sent (or any event). Consent should not be dispatched by resetIdentifiers + mockedStaticCore.verify( + () -> { + MobileCore.dispatchEvent(any()); + }, + never() + ); + } } // ====================================================================================================================== 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 130c1050..bd2aa37f 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 @@ -87,7 +87,7 @@ public void testRegistration() { // verify that the callback invocation does not throw an exception extensionErrorCallback.error(ExtensionError.UNEXPECTED_ERROR); } catch (final Exception e) { - fail(); + fail(e.getMessage()); } } @@ -139,7 +139,7 @@ public void testGetExperienceCloudId() { adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(xdmData)); assertEquals(ecid.toString(), callbackReturnValues.get(0)); } catch (Exception e) { - fail(); + fail(e.getMessage()); } } @@ -177,7 +177,7 @@ public void call(String ecid) {} ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } // set response event to null @@ -204,6 +204,8 @@ public void testGetExperienceCloudId_nullCallback() { ), times(0) ); + } catch (Exception e) { + fail(e.getMessage()); } } @@ -241,7 +243,7 @@ public void call(String ecid) {} ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } // set response event to null @@ -284,7 +286,7 @@ public void call(String ecid) {} ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } // set response event to null @@ -330,6 +332,8 @@ public void call(Object o) {} adobeCallbackCaptor.capture() ) ); + } catch (Exception e) { + fail(e.getMessage()); } // set response event to map missing ECID @@ -372,7 +376,7 @@ public void call(String s) { ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } // verify the dispatched event details @@ -406,6 +410,8 @@ public void testGetUrlVariables_nullCallback() { ), never() ); + } catch (Exception e) { + fail(e.getMessage()); } } @@ -442,7 +448,7 @@ public void call(String o) {} ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } final Event dispatchedEvent = eventCaptor.getValue(); @@ -493,7 +499,7 @@ public void call(String o) {} ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } // set response event data to not have urlvariables key @@ -540,7 +546,7 @@ public void call(String o) {} ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } // set response event to have urlvariables map to null value @@ -585,8 +591,9 @@ public void call(String o) {} ) ); } catch (Exception e) { - fail(); + fail(e.getMessage()); } + adobeCallbackCaptor.getValue().fail(AdobeError.UNEXPECTED_ERROR); // verify @@ -611,7 +618,7 @@ public void testUpdateIdentities() { mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(eventCaptor.capture())); } catch (Exception e) { - fail(); + fail(e.getMessage()); } // verify the dispatched event details @@ -631,6 +638,8 @@ public void testUpdateIdentitiesNullMap() { // verify that no event is dispatched mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(any(Event.class)), never()); + } catch (Exception e) { + fail(e.getMessage()); } } @@ -643,6 +652,8 @@ public void testUpdateIdentities_EmptyMap() { // verify that no event is dispatched mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(any(Event.class)), never()); + } catch (Exception e) { + fail(e.getMessage()); } } @@ -661,6 +672,8 @@ public void testRemoveIdentity() { Identity.removeIdentity(sampleItem, "namespace"); mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(eventCaptor.capture())); + } catch (Exception e) { + fail(e.getMessage()); } final Event dispatchedEvent = eventCaptor.getValue(); @@ -686,6 +699,8 @@ public void testRemoveIdentity_WithInvalidInputs() { // verify that no event is dispatched mockedStaticMobileCore.verify(() -> MobileCore.dispatchEvent(any(Event.class)), never()); + } catch (Exception e) { + fail(e.getMessage()); } } @@ -719,6 +734,8 @@ public void call(IdentityMap map) { adobeCallbackCaptor.capture() ) ); + } catch (Exception e) { + fail(e.getMessage()); } // verify the dispatched event details @@ -787,6 +804,8 @@ public void testGetIdentities_nullCallback() { ), never() ); + } catch (Exception e) { + fail(e.getMessage()); } } @@ -822,6 +841,8 @@ public void call(Object o) {} adobeCallbackCaptor.capture() ) ); + } catch (Exception e) { + fail(e.getMessage()); } // set response event to null @@ -864,7 +885,10 @@ public void call(Object o) {} adobeCallbackCaptor.capture() ) ); + } catch (Exception e) { + fail(e.getMessage()); } + // set response event Map eventData = new HashMap<>(); eventData.put("someKey", "someValue"); @@ -907,7 +931,10 @@ public void call(Object o) {} adobeCallbackCaptor.capture() ) ); + } catch (Exception e) { + fail(e.getMessage()); } + // set response event with empty data Map eventData = new HashMap<>(); adobeCallbackCaptor.getValue().call(buildIdentityResponseEvent(eventData)); @@ -949,7 +976,10 @@ public void call(Object o) {} adobeCallbackCaptor.capture() ) ); + } catch (Exception e) { + fail(e.getMessage()); } + // set response event with empty data adobeCallbackCaptor.getValue().fail(AdobeError.UNEXPECTED_ERROR); From adca97d9e74c2f9185121435d48f54c9f1353816 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 13 Dec 2022 10:56:22 -0800 Subject: [PATCH 13/50] Fix unit test imports --- .../marketing/mobile/edge/identity/ECIDTests.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java index 2dbee4f4..d4002cb0 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java @@ -111,12 +111,12 @@ public void testECID_equals() { ECID a = new ECID(); ECID b = new ECID(a.toString()); - Assert.assertEquals(a, b); - Assert.assertEquals(b, a); - Assert.assertEquals(a, a); - Assert.assertEquals(b, b); + assertEquals(a, b); + assertEquals(b, a); + assertEquals(a, a); + assertEquals(b, b); - Assert.assertNotNull(a); + assertNotNull(a); assertNotEquals(a, new ECID()); assertNotEquals(a, new NotECID(a.toString())); From fb12862b36e82607212b0bdfed954d957adc54c8 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 13 Dec 2022 11:14:22 -0800 Subject: [PATCH 14/50] Fix incorrect constant --- .../mobile/edge/identity/util/IdentityFunctionalTestUtil.java | 3 ++- .../com/adobe/marketing/mobile/edge/identity/ECIDTests.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java index a0ddd758..462ddf3b 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java @@ -190,7 +190,8 @@ public static Event buildRemoveIdentityRequest(final Map map) { return new Event.Builder( "Remove Identity Event", EventType.EDGE_IDENTITY, - "om.adobe.eventSource.updateIdentity" + // TODO: Use event source from Core when available. + "com.adobe.eventSource.removeIdentity" ) .setEventData(map) .build(); diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java index d4002cb0..37b31b23 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/ECIDTests.java @@ -20,7 +20,6 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.junit.Assert; import org.junit.Test; public class ECIDTests { From c6a4e73980a8cfaf87e3259722403b6f2949e90a Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 13 Dec 2022 15:30:25 -0800 Subject: [PATCH 15/50] Rename state based unit tests --- code/build.gradle | 2 +- code/edgeidentity/build.gradle | 2 +- .../com/adobe/marketing/mobile/MobileCoreHelper.java | 4 ++-- .../mobile/edge/identity/util/TestHelper.java | 5 +++-- .../mobile/edge/identity/IdentityStateTests.java | 12 ++++++------ 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/code/build.gradle b/code/build.gradle index 56c90130..7c3df82e 100644 --- a/code/build.gradle +++ b/code/build.gradle @@ -24,7 +24,7 @@ ext { // test dependencies junitVersion = "1.1.3" - mockitoCoreVersion = "4.5.1" + mockitoVersion = "4.5.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // spotless diff --git a/code/edgeidentity/build.gradle b/code/edgeidentity/build.gradle index 4e8529fc..a1de1d16 100644 --- a/code/edgeidentity/build.gradle +++ b/code/edgeidentity/build.gradle @@ -227,7 +227,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.3.0' testImplementation "androidx.test.ext:junit:${rootProject.ext.junitVersion}" - testImplementation "org.mockito:mockito-core:${rootProject.ext.mockitoCoreVersion}" + testImplementation "org.mockito:mockito-core:${rootProject.ext.mockitoVersion}" testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' testImplementation 'org.json:json:20180813' diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java index c34a117c..e333522f 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/MobileCoreHelper.java @@ -12,8 +12,8 @@ package com.adobe.marketing.mobile; /** - * Helper class that exists as a to access test helper methods provided in core - * within the package com.adobe.marketing.mobile + * Helper class that exists as a way to access test helper methods provided in core + * within the package com.adobe.marketing.mobile */ public class MobileCoreHelper { diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java index d7f6ca4d..52fd466e 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java @@ -49,6 +49,7 @@ public class TestHelper { private static final String LOG_SOURCE = "TestHelper"; static final int WAIT_TIMEOUT_MS = 1000; static final int WAIT_EVENT_TIMEOUT_MS = 2000; + static final long WAIT_SHARED_STATE_MS = 5000; static Application defaultApplication; // List of threads to wait for after test execution @@ -470,7 +471,7 @@ public static Map getSharedStateFor(final String stateOwner, int final Map sharedState = new HashMap<>(); MobileCore.dispatchEventWithResponseCallback( event, - 5000L, + WAIT_SHARED_STATE_MS, new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { @@ -521,7 +522,7 @@ public static Map getXDMSharedStateFor(final String stateOwner, final Map sharedState = new HashMap<>(); MobileCore.dispatchEventWithResponseCallback( event, - 5000L, + WAIT_SHARED_STATE_MS, new AdobeCallbackWithError() { @Override public void fail(AdobeError adobeError) { diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java index 2b0b3ff8..ea5b44e4 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java @@ -96,7 +96,7 @@ public void testBootUpIfReady_waitsForHubSharedState_hubStateIsNull() { } @Test - public void testBootUpIfReady_waitsForHubSharedState_hubStateIsPending() { + public void testBootUpIfReady_waitsForHubSharedState_hubStateStatusIsPending() { final IdentityState identityState = new IdentityState(mockIdentityStorageManager); when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) .thenReturn(new SharedStateResult(SharedStateStatus.PENDING, Collections.EMPTY_MAP)); @@ -105,7 +105,7 @@ public void testBootUpIfReady_waitsForHubSharedState_hubStateIsPending() { } @Test - public void testBootUpIfReady_waitsForHubSharedState_hubStateIsSet() { + public void testBootUpIfReady_waitsForHubSharedState_hubStateStatusIsSet() { final IdentityState identityState = new IdentityState(mockIdentityStorageManager); when(mockSharedStateCallback.getSharedState(IdentityConstants.SharedState.Hub.NAME, null)) .thenReturn(new SharedStateResult(SharedStateStatus.SET, Collections.EMPTY_MAP)); @@ -209,7 +209,7 @@ public void testBootUpIfReady_prefersPersistedIdentityDirectEcidOverFetchingFrom } @Test - public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered_stateIsNone() { + public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered_stateStatusIsNone() { final IdentityState identityState = new IdentityState(mockIdentityStorageManager); final Map hubSharedState = new HashMap<>(); hubSharedState.put( @@ -241,7 +241,7 @@ public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered_stateIsNon } @Test - public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered_stateIsPending() { + public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered() { final IdentityState identityState = new IdentityState(mockIdentityStorageManager); final Map hubSharedState = new HashMap<>(); hubSharedState.put( @@ -273,7 +273,7 @@ public void testBootUpIfReady_waitsForIdentityDirectStateIfRegistered_stateIsPen } @Test - public void testBootUpIfReady_waitsForPendingIdentityDirectStateIfRegistered_stateIsNull() { + public void testBootUpIfReady_waitsForPendingIdentityDirectStateIfRegistered() { final IdentityState identityState = new IdentityState(mockIdentityStorageManager); final Map hubSharedState = new HashMap<>(); hubSharedState.put( @@ -338,7 +338,7 @@ public void testBootUpIfReady_regeneratesECIDWhenIdentityDirectStateECIDIsNull() } @Test - public void testBootUpIfReady_generatesNewECIDWhenDirectStateAndPersistedStateUnavailable() { + public void testBootUpIfReady_generatesNewECIDWhenDirectStateAndPersistedStateAreUnavailable() { // No persisted properties final IdentityProperties persistedProperties = new IdentityProperties(); when(mockIdentityStorageManager.loadPropertiesFromPersistence()).thenReturn(persistedProperties); From 35b4dbdeaa567465489eb43cbc1fdc7a29c98b67 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Tue, 13 Dec 2022 17:27:32 -0800 Subject: [PATCH 16/50] Move IdentityTestUtil out of sharedutils --- .../adobe/marketing/mobile/edge/identity/IdentityTestUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename code/edgeidentity/src/{sharedTestUtils => test}/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java (99%) diff --git a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java similarity index 99% rename from code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java rename to code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java index 59a97a99..dddc94b8 100644 --- a/code/edgeidentity/src/sharedTestUtils/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java @@ -1,5 +1,5 @@ /* - Copyright 2021 Adobe. All rights reserved. + 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 From 5a8d17aaceb73eda83033a58b010d77de80ce079 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Wed, 14 Dec 2022 09:29:33 -0800 Subject: [PATCH 17/50] Switch to new registration for MoitorExtension and fix test rules --- .../edge/identity/IdentityAdIdTest.java | 24 +++--- .../edge/identity/IdentityBootUpTest.java | 11 +-- .../identity/IdentityECIDHandlingTest.java | 75 +++++++++++++++---- .../edge/identity/IdentityPublicAPITest.java | 8 +- .../identity/IdentityResetHandlingTest.java | 8 +- .../util/IdentityFunctionalTestUtil.java | 44 ++++------- .../edge/identity/util/MonitorExtension.java | 18 ----- .../mobile/edge/identity/util/TestHelper.java | 31 -------- 8 files changed, 106 insertions(+), 113 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java index 332ce3d5..2a0a0367 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java @@ -19,21 +19,23 @@ import androidx.annotation.Nullable; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; import com.adobe.marketing.mobile.util.JSONUtils; import com.adobe.marketing.mobile.util.StringUtils; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.json.JSONObject; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; public class IdentityAdIdTest { @Rule - public RuleChain rule = RuleChain.outerRule(new SetupCoreRule()).around(new RegisterMonitorExtensionRule()); + public TestRule rule = new SetupCoreRule(); @Test public void testGenericIdentityRequest_whenValidAdId_thenNewValidAdId() throws Exception { @@ -45,7 +47,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenNewValidAdId() throws E createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); MobileCore.setAdvertisingIdentifier(newAdId); // After sending mobile core event, give a wait time to allow for processing @@ -74,7 +76,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenNonAdId() throws Except setEdgeIdentityPersistence( createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); dispatchGenericIdentityNonAdIdEvent(); @@ -104,7 +106,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenSameValidAdId() throws setEdgeIdentityPersistence( createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); MobileCore.setAdvertisingIdentifier(newAdId); waitForThreads(2000); @@ -133,7 +135,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenEmptyAdId() throws Exce setEdgeIdentityPersistence( createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); MobileCore.setAdvertisingIdentifier(newAdId); waitForThreads(2000); @@ -162,7 +164,7 @@ public void testGenericIdentityRequest_whenValidAdId_thenAllZerosAdId() throws E setEdgeIdentityPersistence( createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("GAID", initialAdId)) ); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); MobileCore.setAdvertisingIdentifier(newAdId); waitForThreads(2000); @@ -188,7 +190,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exce // Test String newAdId = "8d9ca5ff-7e74-44ac-bbcd-7aee7baf4f6c"; setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); MobileCore.setAdvertisingIdentifier(newAdId); waitForThreads(2000); @@ -214,7 +216,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenNewValidAdId() throws Exce public void testGenericIdentityRequest_whenNoAdId_thenNonAdId() throws Exception { // Test setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); dispatchGenericIdentityNonAdIdEvent(); @@ -241,7 +243,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenEmptyAdId() throws Excepti // Test String newAdId = ""; setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); MobileCore.setAdvertisingIdentifier(newAdId); waitForThreads(2000); @@ -267,7 +269,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws // Test String newAdId = "00000000-0000-0000-0000-000000000000"; setEdgeIdentityPersistence(createXDMIdentityMap(new TestItem("ECID", "primaryECID"))); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); MobileCore.setAdvertisingIdentifier(newAdId); waitForThreads(2000); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java index b30d4963..eaee7d20 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityBootUpTest.java @@ -15,21 +15,21 @@ import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.getXDMSharedStateFor; import static org.junit.Assert.assertEquals; +import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestHelper; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; import com.adobe.marketing.mobile.util.JSONUtils; +import java.util.Arrays; import java.util.Map; import org.json.JSONObject; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; public class IdentityBootUpTest { @Rule - public RuleChain rule = RuleChain - .outerRule(new TestHelper.SetupCoreRule()) - .around(new TestHelper.RegisterMonitorExtensionRule()); + public TestRule rule = new TestHelper.SetupCoreRule(); // -------------------------------------------------------------------------------------------- // OnBootUp @@ -46,7 +46,8 @@ public void testOnBootUp_LoadsAllIdentitiesFromPreference() throws Exception { new TestItem("UserId", "JohnDoe") ) ); - registerEdgeIdentityExtension(); + + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // verify xdm shared state Map xdmSharedState = flattenMap(getXDMSharedStateFor(IdentityConstants.EXTENSION_NAME, 1000)); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java index 9d70c65e..d17e1f9e 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java @@ -17,24 +17,33 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.MobilePrivacyStatus; +import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestHelper; +import java.util.Arrays; +import java.util.HashMap; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class IdentityECIDHandlingTest { @Rule - public RuleChain rule = RuleChain - .outerRule(new TestHelper.SetupCoreRule()) - .around(new TestHelper.RegisterMonitorExtensionRule()); + public TestRule rule = new TestHelper.SetupCoreRule(); + + private static final HashMap TEST_CONFIG = new HashMap() { + { + put("global.privacy", "optedin"); + put("experienceCloud.org", "testOrg@AdobeOrg"); + put("experienceCloud.server", "notaserver"); + } + }; @Test public void testECID_autoGeneratedWhenBooted() throws InterruptedException { // setup - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // verify ECID is not null verifyPrimaryECIDNotNull(); @@ -46,7 +55,7 @@ public void testECID_loadedFromPersistence() throws Exception { setEdgeIdentityPersistence( createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("ECID", "secondaryECID")) ); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // verify verifyPrimaryECID("primaryECID"); @@ -58,7 +67,7 @@ public void testECID_edgePersistenceTakesPreferenceOverDirectExtension() throws // setup setIdentityDirectPersistedECID("legacyECID"); setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap()); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // verify verifyPrimaryECID("edgeECID"); @@ -70,7 +79,7 @@ public void testECID_loadsIdentityDirectECID() throws Exception { // This will happen when EdgeIdentity extension is installed after Identity direct extension // setup setIdentityDirectPersistedECID("legacyECID"); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // verify verifyPrimaryECID("legacyECID"); @@ -79,7 +88,14 @@ public void testECID_loadsIdentityDirectECID() throws Exception { @Test public void testECID_whenBothExtensionRegistered_install() throws Exception { // setup - registerBothIdentityExtensions(); // no ECID exists before this step + registerExtensions( + Arrays.asList( + MonitorExtension.EXTENSION, + Identity.EXTENSION, + com.adobe.marketing.mobile.Identity.EXTENSION + ), + TEST_CONFIG + ); // no ECID exists before this step String directECID = getIdentityDirectECIDSync(); String edgeECID = getExperienceCloudIdSync(); @@ -95,7 +111,14 @@ public void testECID_whenBothExtensionRegistered_migrationPath() throws Exceptio // setup String existingECID = "legacyECID"; setIdentityDirectPersistedECID(existingECID); - registerBothIdentityExtensions(); + registerExtensions( + Arrays.asList( + MonitorExtension.EXTENSION, + Identity.EXTENSION, + com.adobe.marketing.mobile.Identity.EXTENSION + ), + TEST_CONFIG + ); String directECID = getIdentityDirectECIDSync(); String edgeECID = getExperienceCloudIdSync(); @@ -113,7 +136,7 @@ public void testECID_onResetClearsOldECID() throws Exception { setEdgeIdentityPersistence( createXDMIdentityMap(new TestItem("ECID", "primaryECID"), new TestItem("ECID", "secondaryECID")) ); - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // test MobileCore.resetIdentities(); @@ -130,7 +153,14 @@ public void testECID_onResetClearsOldECID() throws Exception { public void testECID_AreDifferentAfterPrivacyChange() throws Exception { /// Test Edge Identity and IdentityDirect have same ECID on bootup, and after privacy change ECIDs are different setIdentityDirectPersistedECID("legacyECID"); - registerBothIdentityExtensions(); + registerExtensions( + Arrays.asList( + MonitorExtension.EXTENSION, + Identity.EXTENSION, + com.adobe.marketing.mobile.Identity.EXTENSION + ), + TEST_CONFIG + ); TestHelper.waitForThreads(2000); // verify ECID for both extensions are same @@ -154,7 +184,16 @@ public void testECID_AreDifferentAfterResetIdentitiesAndPrivacyChange() throws E // 1) Register Identity then Edge Identity and verify both have same ECID setIdentityDirectPersistedECID("legacyECID"); - registerBothIdentityExtensions(); + + registerExtensions( + Arrays.asList( + MonitorExtension.EXTENSION, + Identity.EXTENSION, + com.adobe.marketing.mobile.Identity.EXTENSION + ), + TEST_CONFIG + ); + TestHelper.waitForThreads(2000); // verify ECID for both extensions are same @@ -179,7 +218,15 @@ public void testECID_DirectEcidIsRemovedOnPrivacyOptOut() throws Exception { // setup setIdentityDirectPersistedECID("legacyECID"); setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap()); - registerBothIdentityExtensions(); + + registerExtensions( + Arrays.asList( + MonitorExtension.EXTENSION, + Identity.EXTENSION, + com.adobe.marketing.mobile.Identity.EXTENSION + ), + TEST_CONFIG + ); // verify ECID verifyPrimaryECID("edgeECID"); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java index 9278a0c1..6af5dce6 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java @@ -18,22 +18,24 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; +import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; import com.adobe.marketing.mobile.util.JSONUtils; +import java.util.Arrays; import java.util.List; import java.util.Map; import org.json.JSONObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class IdentityPublicAPITest { @Rule - public RuleChain rule = RuleChain.outerRule(new SetupCoreRule()).around(new RegisterMonitorExtensionRule()); + public TestRule rule = new SetupCoreRule(); // -------------------------------------------------------------------------------------------- // Setup @@ -41,7 +43,7 @@ public class IdentityPublicAPITest { @Before public void setup() throws Exception { - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); } // -------------------------------------------------------------------------------------------- diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java index d223aac7..6068fe47 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java @@ -17,20 +17,22 @@ import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.MobileCore; +import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; import com.adobe.marketing.mobile.util.JSONUtils; +import java.util.Arrays; import java.util.List; import java.util.Map; import org.json.JSONObject; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; public class IdentityResetHandlingTest { @Rule - public RuleChain rule = RuleChain.outerRule(new SetupCoreRule()).around(new RegisterMonitorExtensionRule()); + public TestRule rule = new SetupCoreRule(); // -------------------------------------------------------------------------------------------- // Setup @@ -38,7 +40,7 @@ public class IdentityResetHandlingTest { @Before public void setup() throws Exception { - registerEdgeIdentityExtension(); + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); } // -------------------------------------------------------------------------------------------- diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java index 462ddf3b..3bb81637 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java @@ -22,6 +22,7 @@ import com.adobe.marketing.mobile.Event; import com.adobe.marketing.mobile.EventSource; import com.adobe.marketing.mobile.EventType; +import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.edge.identity.AuthenticatedState; import com.adobe.marketing.mobile.edge.identity.Identity; @@ -36,13 +37,13 @@ import com.fasterxml.jackson.databind.node.ValueNode; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; @@ -51,37 +52,24 @@ public class IdentityFunctionalTestUtil { private static final String LOG_SOURCE = "IdentityFunctionalTestUtil"; /** - * Register's Edge Identity Extension and start the Core + * Applies the configuration provided, registers the extensions and then starts + * core. + * @param extensions the extensions that need to be registered + * @param configuration the initial configuration update that needs to be applied + * @throws InterruptedException if the wait time for extension registration has elapsed */ - public static void registerEdgeIdentityExtension() throws InterruptedException { - final ADBCountDownLatch latch = new ADBCountDownLatch(1); - MobileCore.registerExtensions(Arrays.asList(Identity.EXTENSION), o -> latch.countDown()); - - latch.await(1000, TimeUnit.MILLISECONDS); - TestHelper.waitForThreads(2000); - resetTestExpectations(); - } - - /** - * Register's Identity Direct and Edge Identity Extension. And then starts the MobileCore - */ - public static void registerBothIdentityExtensions() throws Exception { - HashMap config = new HashMap() { - { - put("global.privacy", "optedin"); - put("experienceCloud.org", "testOrg@AdobeOrg"); - put("experienceCloud.server", "notaserver"); - } - }; - MobileCore.updateConfiguration(config); + public static void registerExtensions( + final List> extensions, + @Nullable final Map configuration + ) throws InterruptedException { + if (configuration != null) { + MobileCore.updateConfiguration(configuration); + } final ADBCountDownLatch latch = new ADBCountDownLatch(1); - MobileCore.registerExtensions( - Arrays.asList(Identity.EXTENSION, com.adobe.marketing.mobile.Identity.EXTENSION), - o -> latch.countDown() - ); + MobileCore.registerExtensions(extensions, o -> latch.countDown()); - latch.await(); + latch.await(1000, TimeUnit.MILLISECONDS); TestHelper.waitForThreads(2000); resetTestExpectations(); } diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java index 372591db..8aeeeda7 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java @@ -19,8 +19,6 @@ import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionApi; -import com.adobe.marketing.mobile.ExtensionError; -import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.SharedStateResolution; import com.adobe.marketing.mobile.SharedStateResult; @@ -61,22 +59,6 @@ protected void onRegistered() { getApi().registerEventListener(EventType.WILDCARD, EventSource.WILDCARD, this::wildcardProcessor); } - public static void registerExtension() { - MobileCore.registerExtension( - MonitorExtension.class, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - Log.error( - LOG_TAG, - LOG_SOURCE, - "There was an error registering the Monitor extension: " + extensionError.getErrorName() - ); - } - } - ); - } - /** * Unregister the Monitor Extension from the EventHub. */ diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java index 52fd466e..f0ce70f2 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java @@ -105,37 +105,6 @@ public void evaluate() throws Throwable { } } - /** - * {@code TestRule} which registers the {@code MonitorExtension}, allowing test cases to assert - * events passing through the {@code EventHub}. This {@code TestRule} must be applied after - * the {@link SetupCoreRule} to ensure the {@code MobileCore} is setup for testing first. - *

- * To use, add the following to your test class: - *

-	 *  @Rule
-	 *    public RuleChain rule = RuleChain.outerRule(new SetupCoreRule())
-	 * 							.around(new RegisterMonitorExtensionRule());
-	 * 
- */ - public static class RegisterMonitorExtensionRule implements TestRule { - - @Override - public Statement apply(final Statement base, final Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - MonitorExtension.registerExtension(); - - try { - base.evaluate(); - } finally { - MonitorExtension.reset(); - } - } - }; - } - } - /** * Waits for all the {@code #knownThreads} to finish or fails the test after timeoutMillis if some of them are still running * when the timer expires. If timeoutMillis is 0, a default timeout will be set = 1000ms From 943366a6465353f0883dce39c6c80ae6d583ef84 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Wed, 14 Dec 2022 09:44:13 -0800 Subject: [PATCH 18/50] Fix constant and import correct Nullable --- .../edge/identity/util/IdentityFunctionalTestUtil.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java index 3bb81637..a995242a 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java @@ -16,6 +16,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import androidx.annotation.Nullable; import com.adobe.marketing.mobile.AdobeCallback; import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; @@ -43,13 +44,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; public class IdentityFunctionalTestUtil { private static final String LOG_SOURCE = "IdentityFunctionalTestUtil"; + private static final long REGISTRATION_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(2); /** * Applies the configuration provided, registers the extensions and then starts @@ -69,7 +70,7 @@ public static void registerExtensions( final ADBCountDownLatch latch = new ADBCountDownLatch(1); MobileCore.registerExtensions(extensions, o -> latch.countDown()); - latch.await(1000, TimeUnit.MILLISECONDS); + latch.await(REGISTRATION_TIMEOUT_MS, TimeUnit.MILLISECONDS); TestHelper.waitForThreads(2000); resetTestExpectations(); } From 6c064f427d3bcc257d88e28118056066ee3733b8 Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Wed, 14 Dec 2022 11:15:07 -0800 Subject: [PATCH 19/50] Remove unused helper methods --- .../util/IdentityFunctionalTestUtil.java | 58 ------- .../edge/identity/util/MonitorExtension.java | 11 -- .../mobile/edge/identity/util/TestHelper.java | 141 +----------------- 3 files changed, 1 insertion(+), 209 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java index a995242a..43435db2 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java @@ -20,9 +20,6 @@ import com.adobe.marketing.mobile.AdobeCallback; import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; -import com.adobe.marketing.mobile.Event; -import com.adobe.marketing.mobile.EventSource; -import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.edge.identity.AuthenticatedState; @@ -44,7 +41,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import org.json.JSONException; import org.json.JSONObject; public class IdentityFunctionalTestUtil { @@ -163,60 +159,6 @@ public static Map createXDMIdentityMap(TestItem... items) { return identityMapDict; } - /** - * Helper method to build remove identity request event with XDM formatted Identity jsonString - */ - public static Event buildRemoveIdentityRequestWithJSONString(final String jsonStr) throws Exception { - final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = JSONUtils.toMap(jsonObject); - return buildRemoveIdentityRequest(xdmData); - } - - /** - * Helper method to build remove identity request event with XDM formatted Identity map - */ - public static Event buildRemoveIdentityRequest(final Map map) { - return new Event.Builder( - "Remove Identity Event", - EventType.EDGE_IDENTITY, - // TODO: Use event source from Core when available. - "com.adobe.eventSource.removeIdentity" - ) - .setEventData(map) - .build(); - } - - /** - * Helper method to build update identity request event with XDM formatted Identity jsonString - */ - public static Event buildUpdateIdentityRequestJSONString(final String jsonStr) throws Exception { - final JSONObject jsonObject = new JSONObject(jsonStr); - final Map xdmData = JSONUtils.toMap(jsonObject); - return buildUpdateIdentityRequest(xdmData); - } - - /** - * Helper method to build update identity request event with XDM formatted Identity map - */ - public static Event buildUpdateIdentityRequest(final Map map) { - return new Event.Builder("Update Identity Event", EventType.EDGE_IDENTITY, EventSource.UPDATE_IDENTITY) - .setEventData(map) - .build(); - } - - /** - * Serialize the given {@code jsonString} to a JSON Object, then flattens to {@code Map}. - * If the provided string is not in JSON structure an {@link JSONException} is thrown. - * - * @param jsonString the string in JSON structure to flatten - * @return new map with flattened structure - */ - public static Map flattenJSONString(final String jsonString) throws JSONException { - JSONObject jsonObject = new JSONObject(jsonString); - Map persistenceValueMap = JSONUtils.toMap(jsonObject); - return flattenMap(persistenceValueMap); - } - /** * Serialize the given {@code map} to a JSON Object, then flattens to {@code Map}. * For example, a JSON such as "{xdm: {stitchId: myID, eventType: myType}}" is flattened diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java index 8aeeeda7..1e458885 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/MonitorExtension.java @@ -72,17 +72,6 @@ public static void unregisterExtension() { MobileCore.dispatchEvent(event); } - /** - * Add an event to the list of expected events. - * @param type the type of the event. - * @param source the source of the event. - * @param count the number of events expected to be received. - */ - public static void setExpectedEvent(final String type, final String source, final int count) { - EventSpec eventSpec = new EventSpec(source, type); - expectedEvents.put(eventSpec, new ADBCountDownLatch(count)); - } - public static Map getExpectedEvents() { return expectedEvents; } diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java index f0ce70f2..98ff6537 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestHelper.java @@ -12,9 +12,7 @@ package com.adobe.marketing.mobile.edge.identity.util; import static com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants.LOG_TAG; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import android.app.Application; import android.app.Instrumentation; @@ -227,145 +225,9 @@ public static void resetTestExpectations() { // --------------------------------------------------------------------------------------------- // Event Test Helpers // --------------------------------------------------------------------------------------------- - - /** - * Sets an expectation for a specific event type and source and how many times the event should be dispatched. - * - * @param type the event type - * @param source the event source - * @param count the expected number of times the event is dispatched - * @throws IllegalArgumentException if {@code count} is less than 1 - */ - public static void setExpectationEvent(final String type, final String source, final int count) { - if (count < 1) { - throw new IllegalArgumentException("Cannot set expectation event count less than 1!"); - } - - MonitorExtension.setExpectedEvent(type, source, count); - } - - /** - * Asserts if all the expected events were received and fails if an unexpected event was seen. - * - * @param ignoreUnexpectedEvents if set on false, an assertion is made on unexpected events, otherwise the unexpected events are ignored - * @throws InterruptedException - * @see #setExpectationEvent(String, String, int) - * @see #assertUnexpectedEvents() - */ - public static void assertExpectedEvents(final boolean ignoreUnexpectedEvents) throws InterruptedException { - Map expectedEvents = MonitorExtension.getExpectedEvents(); - - if (expectedEvents.isEmpty()) { - fail("There are no event expectations set, use this API after calling setExpectationEvent"); - return; - } - - for (Map.Entry expected : expectedEvents.entrySet()) { - boolean awaitResult = expected.getValue().await(WAIT_EVENT_TIMEOUT_MS, TimeUnit.MILLISECONDS); - String failMessage = String.format( - "Timed out waiting for event type %s and source %s.", - expected.getKey().type, - expected.getKey().source - ); - assertTrue(failMessage, awaitResult); - int expectedCount = expected.getValue().getInitialCount(); - int receivedCount = expected.getValue().getCurrentCount(); - failMessage = - String.format( - "Expected %d events for '%s', but received %d", - expectedCount, - expected.getKey(), - receivedCount - ); - assertEquals(failMessage, expectedCount, receivedCount); - } - - if (!ignoreUnexpectedEvents) { - assertUnexpectedEvents(false); - } - } - - /** - * Asserts if any unexpected event was received. Use this method to verify the received events - * are correct when setting event expectations. Waits a short time before evaluating received - * events to allow all events to come in. - * - * @see #setExpectationEvent - */ - public static void assertUnexpectedEvents() throws InterruptedException { - assertUnexpectedEvents(true); - } - - /** - * Asserts if any unexpected event was received. Use this method to verify the received events - * are correct when setting event expectations. - * - * @param shouldWait waits a short time to allow events to be received when true - * @see #setExpectationEvent - */ - public static void assertUnexpectedEvents(final boolean shouldWait) throws InterruptedException { - // Short wait to allow events to come in - if (shouldWait) { - sleep(WAIT_TIMEOUT_MS); - } - - int unexpectedEventsReceivedCount = 0; - StringBuilder unexpectedEventsErrorString = new StringBuilder(); - - Map> receivedEvents = MonitorExtension.getReceivedEvents(); - Map expectedEvents = MonitorExtension.getExpectedEvents(); - - for (Map.Entry> receivedEvent : receivedEvents.entrySet()) { - ADBCountDownLatch expectedEventLatch = expectedEvents.get(receivedEvent.getKey()); - - if (expectedEventLatch != null) { - expectedEventLatch.await(WAIT_EVENT_TIMEOUT_MS, TimeUnit.MILLISECONDS); - int expectedCount = expectedEventLatch.getInitialCount(); - int receivedCount = receivedEvent.getValue().size(); - String failMessage = String.format( - "Expected %d events for '%s', but received %d", - expectedCount, - receivedEvent.getKey(), - receivedCount - ); - assertEquals(failMessage, expectedCount, receivedCount); - } else { - unexpectedEventsReceivedCount += receivedEvent.getValue().size(); - unexpectedEventsErrorString.append( - String.format( - "(%s,%s,%d)", - receivedEvent.getKey().type, - receivedEvent.getKey().source, - receivedEvent.getValue().size() - ) - ); - Log.debug( - LOG_TAG, - LOG_SOURCE, - String.format( - "Received unexpected event with type: %s source: %s", - receivedEvent.getKey().type, - receivedEvent.getKey().source - ) - ); - } - } - - assertEquals( - String.format( - "Received %d unexpected event(s): %s", - unexpectedEventsReceivedCount, - unexpectedEventsErrorString.toString() - ), - 0, - unexpectedEventsReceivedCount - ); - } - /** * Returns the {@code Event}(s) dispatched through the Event Hub, or empty if none was found. - * Use this API after calling {@link #setExpectationEvent(String, String, int)} to wait for - * the expected events. The wait time for each event is {@link #WAIT_EVENT_TIMEOUT_MS}ms. + * The wait time for each event is {@link #WAIT_EVENT_TIMEOUT_MS}ms. * * @param type the event type as in the expectation * @param source the event source as in the expectation @@ -380,7 +242,6 @@ public static List getDispatchedEventsWith(final String type, final Strin /** * Returns the {@code Event}(s) dispatched through the Event Hub, or empty if none was found. - * Use this API after calling {@link #setExpectationEvent(String, String, int)} to wait for the right amount of time * * @param type the event type as in the expectation * @param source the event source as in the expectation From c66775e3c310ed34885f01a79714e01878c9ab1b Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Thu, 15 Dec 2022 12:49:58 -0800 Subject: [PATCH 20/50] Removed unused constants, utility methods and tasks --- code/edgeidentity/build.gradle | 12 -- .../edge/identity/IdentityConstants.java | 1 - .../marketing/mobile/edge/identity/Utils.java | 81 ++--------- .../mobile/edge/identity/UtilsTests.java | 133 ++---------------- 4 files changed, 25 insertions(+), 202 deletions(-) diff --git a/code/edgeidentity/build.gradle b/code/edgeidentity/build.gradle index a1de1d16..60f72702 100644 --- a/code/edgeidentity/build.gradle +++ b/code/edgeidentity/build.gradle @@ -64,18 +64,6 @@ android { sourceCompatibility rootProject.ext.sourceCompatibility targetCompatibility rootProject.ext.targetCompatibility } - - sourceSets { - String sharedTestUtilJavaDir = 'src/sharedTestUtils/java' - - test { - java.srcDirs += [sharedTestUtilJavaDir] - } - - androidTest { - java.srcDirs += [sharedTestUtilJavaDir] - } - } } afterEvaluate { 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 bb1edfb1..ffa3df8b 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 @@ -26,7 +26,6 @@ private Default() {} static final class EventSource { - static final String BOOTED = "com.adobe.eventSource.booted"; static final String REMOVE_IDENTITY = "com.adobe.eventSource.removeIdentity"; static final String REQUEST_CONTENT = "com.adobe.eventSource.requestContent"; static final String REQUEST_IDENTITY = "com.adobe.eventSource.requestIdentity"; 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 9d93020d..ebdcfc95 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 @@ -1,5 +1,5 @@ /* - Copyright 2021 Adobe. All rights reserved. + 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 @@ -11,89 +11,30 @@ package com.adobe.marketing.mobile.edge.identity; -import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; - -import com.adobe.marketing.mobile.services.Log; -import com.adobe.marketing.mobile.util.JSONUtils; -import java.util.ArrayList; import java.util.List; import java.util.Map; -import org.json.JSONException; -import org.json.JSONObject; class Utils { - private static final String LOG_SOURCE = "Utils"; - private Utils() {} - static boolean isNullOrEmpty(final Map map) { - return map == null || map.isEmpty(); - } - - static boolean isNullOrEmpty(final List list) { - return list == null || list.isEmpty(); - } - /** - * Adds {@code key}/{@code value} to {@code map} if {@code value} is not null or an - * empty collection. + * Checks if the {@code Map} provided is null or empty. * - * @param map collection to put {@code value} mapped to {@code key} if {@code value} is - * non-null and contains at least one entry - * @param key key used to map {@code value} in {@code map} - * @param value a Object to add to {@code map} if not null + * @param map the {@code Map} to verify + * @return true if the {@code Map} provided is null or empty; false otherwise */ - static void putIfNotNull(final Map map, final String key, final Object value) { - boolean addValues = map != null && key != null && value != null; - - if (addValues) { - map.put(key, value); - } - } - - /** - * Creates a deep copy of the provided {@link Map}. - * - * @param map to be copied - * @return {@link Map} containing a deep copy of all the elements in {@code map} - */ - static Map deepCopy(final Map map) { - if (map == null) { - return null; - } - - try { - // Core's JSONUtils retains null in resulting Map but, EdgeIdentity 1.0 implementaion - // filtered out the null value keys. One issue this may cause is sending empty objects to - // Edge Network. - // TODO: Add/verify tests to check side effects of retaining nulls in the resulting Map - return JSONUtils.toMap(new JSONObject(map)); - } catch (final JSONException | NullPointerException e) { - Log.debug(LOG_TAG, LOG_SOURCE, "Unable to deep copy map, json string is invalid."); - } - - return null; + static boolean isNullOrEmpty(final Map map) { + return map == null || map.isEmpty(); } /** - * Creates a deep copy of the provided {@code listOfMaps}. + * Checks if the {@code List} provided is null or empty. * - * @param listOfMaps to be copied - * @return {@link List} containing a deep copy of all the elements in {@code listOfMaps} - * @see #deepCopy(Map) + * @param list the {@code List} to verify + * @return true if the {@code List} provided is null or empty; false otherwise */ - static List> deepCopy(final List> listOfMaps) { - if (listOfMaps == null) { - return null; - } - - List> deepCopy = new ArrayList<>(); - - for (Map map : listOfMaps) { - deepCopy.add(deepCopy(map)); - } - - return deepCopy; + static boolean isNullOrEmpty(final List list) { + return list == null || list.isEmpty(); } } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java index 957d6b37..3a5072b4 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java @@ -11,14 +11,11 @@ package com.adobe.marketing.mobile.edge.identity; -import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; -import static junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertNull; import static junit.framework.TestCase.assertTrue; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import org.junit.Test; @@ -27,134 +24,32 @@ public class UtilsTests { @Test - public void testUtils_deepCopyNull() { - assertNull(Utils.deepCopy((Map) null)); + public void test_isNullOrEmpty_nullMap() { + assertTrue(Utils.isNullOrEmpty((Map) null)); } @Test - public void testUtils_deepCopyEmpty() { - Map emptyMap = new HashMap<>(); - assertEquals(0, Utils.deepCopy(emptyMap).size()); + public void test_isNullOrEmpty_emptyMap() { + assertTrue(Utils.isNullOrEmpty(Collections.EMPTY_MAP)); } @Test - public void testUtils_deepCopyValidSimpleNull() { - Map map = new HashMap<>(); - map.put("key1", "value1"); - Map deepCopy = Utils.deepCopy(map); - - map = null; - assertNotNull(deepCopy); - } - - @Test - public void testUtils_deepCopyValidSimple() { - Map map = new HashMap<>(); - map.put("key1", "value1"); - - Map deepCopy = Utils.deepCopy(map); - deepCopy.remove("key1"); - - assertTrue(map.containsKey("key1")); + public void test_isNullOrEmpty_nonEmptyNonNullMap() { + assertFalse(Utils.isNullOrEmpty(Collections.singletonMap("someKey", "someValue"))); } @Test - public void testUtils_deepCopyValidNested() { - Map map = new HashMap<>(); - map.put("key1", "value1"); - - Map nested = new HashMap<>(); - nested.put("nestedKey", "nestedValue"); - map.put("nestedMap", nested); - - Map deepCopy = Utils.deepCopy(map); - Map nestedDeepCopy = (Map) deepCopy.get("nestedMap"); - nestedDeepCopy.put("newKey", "newValue"); - - assertFalse(nested.size() == nestedDeepCopy.size()); + public void test_isNullOrEmpty_nullList() { + assertTrue(Utils.isNullOrEmpty((List) null)); } @Test - public void testUtils_deepCopyNullKey_returnsNull() { - Map map = new HashMap<>(); - map.put("key1", "value1"); - map.put(null, "null"); - - Map nested = new HashMap<>(); - nested.put("nestedKey", "nestedValue"); - map.put(null, "nestednull"); - - assertNull(Utils.deepCopy(map)); - } - - @Test - public void testPutIfNotNull_NonNull_InsertsCorrectly() { - Map map = new HashMap<>(); - Utils.putIfNotNull(map, "testKey", new Integer(2)); - - assertEquals(1, map.size()); - assertEquals(2, map.get("testKey")); - } - - @Test - public void testPutIfNotNull_Null_NoInsert() { - Map map = new HashMap<>(); - Utils.putIfNotNull(map, "testKey", null); - - assertEquals(0, map.size()); + public void test_isNullOrEmpty_emptyList() { + assertTrue(Utils.isNullOrEmpty(Collections.EMPTY_LIST)); } @Test - public void testUtils_deepCopyListOfMaps_Null() { - assertNull(Utils.deepCopy((List) null)); - } - - @Test - public void testUtils_deepCopyListOfMaps_Empty() { - assertEquals(0, Utils.deepCopy(new ArrayList>()).size()); - } - - @Test - public void testUtils_deepCopyListOfMaps_ValidSimpleNull() { - List> list = new ArrayList<>(); - list.add(new HashMap()); - assertNotNull(Utils.deepCopy(list)); - } - - @Test - public void testUtils_deepCopyListOfMaps_ValidSimple() { - Map map = new HashMap<>(); - map.put("key1", "value1"); - List> list = new ArrayList<>(); - list.add(map); - - List> deepCopy = Utils.deepCopy(list); - deepCopy.remove(0); - - assertEquals(1, list.size()); - } - - @Test - public void testUtils_deepCopyListOfMaps_ValidNested() { - Map map = new HashMap<>(); - map.put("key1", "value1"); - - Map nested = new HashMap<>(); - nested.put("nestedKey", "nestedValue"); - map.put("nestedMap", nested); - - List> list = new ArrayList>(); - list.add(map); - - Map deepCopy = Utils.deepCopy(map); - List> nestedDeepCopy = Utils.deepCopy(list); - nestedDeepCopy.get(0).put("newKey", "newValue"); - ((Map) nestedDeepCopy.get(0).get("nestedMap")).put("nestedKey2", 2222); - - assertEquals(3, nestedDeepCopy.get(0).size()); - assertEquals(2, list.get(0).size()); - assertTrue(list.get(0).containsKey("key1")); - assertEquals(1, ((Map) list.get(0).get("nestedMap")).size()); - assertEquals(2, ((Map) nestedDeepCopy.get(0).get("nestedMap")).size()); + public void test_isNullOrEmpty_nonEmptyNonNullList() { + assertFalse(Utils.isNullOrEmpty(Arrays.asList("A", 1, true))); } } From 734d4b68e9a0a84c84be9ab4b49559b1f83840cd Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat Date: Thu, 15 Dec 2022 14:20:20 -0800 Subject: [PATCH 21/50] Preserve copyright date for moved files --- .../marketing/mobile/edge/identity/util/ADBCountDownLatch.java | 2 +- .../mobile/edge/identity/util/IdentityFunctionalTestUtil.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/ADBCountDownLatch.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/ADBCountDownLatch.java index 3d36853c..b635d3c8 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/ADBCountDownLatch.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/ADBCountDownLatch.java @@ -1,5 +1,5 @@ /* - Copyright 2022 Adobe. All rights reserved. + 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 diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java index 43435db2..793c369a 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java @@ -1,5 +1,5 @@ /* - Copyright 2022 Adobe. All rights reserved. + 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 From fd3214d2ae1f28d8bb370e11f170da68d9db62eb Mon Sep 17 00:00:00 2001 From: Prashanth Rudrabhat <96199823+prudrabhat@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:47:43 -0800 Subject: [PATCH 22/50] Use Constants from Core and add friendly name (#79) --- .../edge/identity/IdentityAdIdTest.java | 21 ++- .../edge/identity/IdentityPublicAPITest.java | 12 +- .../identity/IdentityResetHandlingTest.java | 7 +- .../mobile/edge/identity/Identity.java | 22 +-- .../edge/identity/IdentityConstants.java | 27 +--- .../edge/identity/IdentityExtension.java | 71 +++----- .../mobile/edge/identity/IdentityState.java | 6 +- .../mobile/edge/identity/EventUtilsTests.java | 50 +++--- .../edge/identity/IdentityExtensionTests.java | 151 ++++++------------ .../edge/identity/IdentityStateTests.java | 8 +- .../edge/identity/IdentityTestUtil.java | 14 +- .../mobile/edge/identity/IdentityTests.java | 34 ++-- 12 files changed, 156 insertions(+), 267 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java index 2a0a0367..f914dbc3 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityAdIdTest.java @@ -18,6 +18,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; @@ -316,7 +318,7 @@ public void testGenericIdentityRequest_whenNoAdId_thenAllZerosAdIdTwice() throws * APIs are properly dispatched. Verifies: * 1. Event type and source * 2. Event data/properties as required for proper ad ID functionality - * @param isGenericIdentityEventAdIdEvent true if the expected {@link com.adobe.marketing.mobile.edge.identity.IdentityConstants.EventType#GENERIC_IDENTITY} + * @param isGenericIdentityEventAdIdEvent true if the expected {@link EventType#GENERIC_IDENTITY} * event should be an ad ID event, false otherwise * @param expectedConsentValue the expected consent value in the format {@link IdentityConstants.XDMKeys.Consent#YES} * or {@link IdentityConstants.XDMKeys.Consent#NO}; however, if consent event should not be dispatched, use null @@ -326,18 +328,15 @@ private void verifyDispatchedEvents(boolean isGenericIdentityEventAdIdEvent, Str throws Exception { // Check the event type and source List dispatchedGenericIdentityEvents = getDispatchedEventsWith( - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_CONTENT + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_CONTENT ); // Verify Generic Identity event assertEquals(1, dispatchedGenericIdentityEvents.size()); Event genericIdentityEvent = dispatchedGenericIdentityEvents.get(0); assertEquals(isGenericIdentityEventAdIdEvent, EventUtils.isAdIdEvent(genericIdentityEvent)); // Verify Edge Consent event - List dispatchedConsentEvents = getDispatchedEventsWith( - IdentityConstants.EventType.EDGE_CONSENT, - IdentityConstants.EventSource.UPDATE_CONSENT - ); + List dispatchedConsentEvents = getDispatchedEventsWith(EventType.CONSENT, EventSource.UPDATE_CONSENT); assertEquals(StringUtils.isNullOrEmpty(expectedConsentValue) ? 0 : 1, dispatchedConsentEvents.size()); if (!StringUtils.isNullOrEmpty(expectedConsentValue)) { Map consentDataMap = flattenMap(dispatchedConsentEvents.get(0).getEventData()); @@ -372,8 +371,8 @@ private void verifyFlatIdentityMap( } /** - * Dispatches an event using the MobileCore dispatchEvent API. Event has type {@link com.adobe.marketing.mobile.edge.identity.IdentityConstants.EventType#GENERIC_IDENTITY} - * and source {@link com.adobe.marketing.mobile.edge.identity.IdentityConstants.EventSource#REQUEST_CONTENT}. + * Dispatches an event using the MobileCore dispatchEvent API. Event has type {@link EventType#GENERIC_IDENTITY} + * and source {@link EventSource#REQUEST_CONTENT}. * This is the combination of event type and source that the ad ID listener will capture, and this * method helps set up test cases that verify the ad ID is not modified if the advertisingIdentifier * property is not present in the correct format @@ -381,8 +380,8 @@ private void verifyFlatIdentityMap( private void dispatchGenericIdentityNonAdIdEvent() { Event genericIdentityNonAdIdEvent = new Event.Builder( "Test event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_CONTENT + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_CONTENT ) .setEventData( new HashMap() { diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java index 6af5dce6..590296fa 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java @@ -17,6 +17,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.edge.identity.util.IdentityTestConstants; import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; @@ -112,10 +114,7 @@ public void testUpdateAPI_nullData() throws Exception { waitForThreads(2000); // verify no shares state change event dispatched - List dispatchedEvents = getDispatchedEventsWith( - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ); + List dispatchedEvents = getDispatchedEventsWith(EventType.HUB, EventSource.SHARED_STATE); assertEquals(0, dispatchedEvents.size()); // verify xdm shared state is not disturbed @@ -131,10 +130,7 @@ public void testUpdateAPI_emptyData() throws Exception { waitForThreads(2000); // verify no shares state change event dispatched - List dispatchedEvents = getDispatchedEventsWith( - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ); + List dispatchedEvents = getDispatchedEventsWith(EventType.HUB, EventSource.SHARED_STATE); assertEquals(0, dispatchedEvents.size()); // verify xdm shared state is not disturbed diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java index 6068fe47..4107aba0 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityResetHandlingTest.java @@ -16,6 +16,8 @@ import static org.junit.Assert.*; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.edge.identity.util.MonitorExtension; import com.adobe.marketing.mobile.edge.identity.util.TestPersistenceHelper; @@ -67,10 +69,7 @@ public void testReset_ClearsAllIDAndDispatchesResetComplete() throws Exception { assertNotEquals(beforeResetECID, newECID); // verify edge reset complete event dispatched - List resetCompleteEvent = getDispatchedEventsWith( - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.RESET_COMPLETE - ); + List resetCompleteEvent = getDispatchedEventsWith(EventType.EDGE_IDENTITY, EventSource.RESET_COMPLETE); assertEquals(1, resetCompleteEvent.size()); // verify shared state is updated 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 d22a4d3b..7e87f61b 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 @@ -17,6 +17,8 @@ import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionError; import com.adobe.marketing.mobile.ExtensionErrorCallback; @@ -86,8 +88,8 @@ public static void getExperienceCloudId(final AdobeCallback callback) { final Event event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_IDENTITY_ECID, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REQUEST_IDENTITY ) .build(); @@ -169,8 +171,8 @@ public static void getUrlVariables(final AdobeCallback callback) { final Event event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData( new HashMap() { @@ -237,8 +239,8 @@ public static void updateIdentities(final IdentityMap identityMap) { final Event updateIdentitiesEvent = new Event.Builder( IdentityConstants.EventNames.UPDATE_IDENTITIES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.UPDATE_IDENTITY ) .setEventData(identityMap.asXDMMap(false)) .build(); @@ -269,8 +271,8 @@ public static void removeIdentity(final IdentityItem item, final String namespac final Event removeIdentitiesEvent = new Event.Builder( IdentityConstants.EventNames.REMOVE_IDENTITIES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REMOVE_IDENTITY ) .setEventData(identityMap.asXDMMap(false)) .build(); @@ -296,8 +298,8 @@ public static void getIdentities(final AdobeCallback callback) { final Event event = new Event.Builder( IdentityConstants.EventNames.REQUEST_IDENTITIES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REQUEST_IDENTITY ) .build(); 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 ffa3df8b..08d9e696 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 @@ -15,6 +15,7 @@ final class IdentityConstants { static final String LOG_TAG = "EdgeIdentity"; static final String EXTENSION_NAME = "com.adobe.edge.identity"; + static final String EXTENSION_FRIENDLY_NAME = "Edge Identity"; static final String EXTENSION_VERSION = "2.0.0"; static final class Default { @@ -24,32 +25,6 @@ static final class Default { private Default() {} } - static final class EventSource { - - static final String REMOVE_IDENTITY = "com.adobe.eventSource.removeIdentity"; - static final String REQUEST_CONTENT = "com.adobe.eventSource.requestContent"; - static final String REQUEST_IDENTITY = "com.adobe.eventSource.requestIdentity"; - static final String REQUEST_RESET = "com.adobe.eventSource.requestReset"; - static final String RESET_COMPLETE = "com.adobe.eventSource.resetComplete"; - static final String RESPONSE_IDENTITY = "com.adobe.eventSource.responseIdentity"; - static final String SHARED_STATE = "com.adobe.eventSource.sharedState"; - static final String UPDATE_CONSENT = "com.adobe.eventSource.updateConsent"; - static final String UPDATE_IDENTITY = "com.adobe.eventSource.updateIdentity"; - - private EventSource() {} - } - - static final class EventType { - - static final String EDGE_CONSENT = "com.adobe.eventType.edgeConsent"; - static final String EDGE_IDENTITY = "com.adobe.eventType.edgeIdentity"; - static final String GENERIC_IDENTITY = "com.adobe.eventType.generic.identity"; - static final String HUB = "com.adobe.eventType.hub"; - static final String IDENTITY = "com.adobe.eventType.identity"; - - private EventType() {} - } - static final class EventNames { static final String CONSENT_UPDATE_REQUEST_AD_ID = "Consent Update Request for Ad ID"; 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 ef6f2c77..81c7243f 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 @@ -16,6 +16,8 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionApi; import com.adobe.marketing.mobile.SharedStateResolution; @@ -71,6 +73,11 @@ protected String getName() { return IdentityConstants.EXTENSION_NAME; } + @Override + protected String getFriendlyName() { + return IdentityConstants.EXTENSION_FRIENDLY_NAME; + } + @Override protected String getVersion() { return IdentityConstants.EXTENSION_VERSION; @@ -82,13 +89,13 @@ protected String getVersion() { *

* The following listeners are registered during this extension's registration. *

    - *
  • EventType {@link IdentityConstants.EventType#GENERIC_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_CONTENT}
  • - *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_IDENTITY}
  • - *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#UPDATE_IDENTITY}
  • - *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REMOVE_IDENTITY}
  • - *
  • EventType {@link IdentityConstants.EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_CONTENT}
  • - *
  • EventType {@link IdentityConstants.EventType#GENERIC_IDENTITY} and EventSource {@link IdentityConstants.EventSource#REQUEST_RESET}
  • - *
  • EventType {@link IdentityConstants.EventType#HUB} and EventSource {@link IdentityConstants.EventSource#SHARED_STATE}
  • + *
  • EventType {@link EventType#GENERIC_IDENTITY} and EventSource {@link EventSource#REQUEST_CONTENT}
  • + *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#REQUEST_IDENTITY}
  • + *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#UPDATE_IDENTITY}
  • + *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#REMOVE_IDENTITY}
  • + *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#REQUEST_CONTENT}
  • + *
  • EventType {@link EventType#GENERIC_IDENTITY} and EventSource {@link EventSource#REQUEST_RESET}
  • + *
  • EventType {@link EventType#HUB} and EventSource {@link EventSource#SHARED_STATE}
  • *
*

*/ @@ -98,48 +105,22 @@ protected void onRegistered() { // GENERIC_IDENTITY event listeners getApi() - .registerEventListener( - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_CONTENT, - this::handleRequestContent - ); + .registerEventListener(EventType.GENERIC_IDENTITY, EventSource.REQUEST_CONTENT, this::handleRequestContent); - getApi() - .registerEventListener( - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_RESET, - this::handleRequestReset - ); + getApi().registerEventListener(EventType.GENERIC_IDENTITY, EventSource.REQUEST_RESET, this::handleRequestReset); // EDGE_IDENTITY event listeners getApi() - .registerEventListener( - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY, - this::handleRequestIdentity - ); + .registerEventListener(EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY, this::handleRequestIdentity); getApi() - .registerEventListener( - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY, - this::handleUpdateIdentities - ); + .registerEventListener(EventType.EDGE_IDENTITY, EventSource.UPDATE_IDENTITY, this::handleUpdateIdentities); getApi() - .registerEventListener( - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY, - this::handleRemoveIdentity - ); + .registerEventListener(EventType.EDGE_IDENTITY, EventSource.REMOVE_IDENTITY, this::handleRemoveIdentity); // HUB shared state event listener - getApi() - .registerEventListener( - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE, - this::handleIdentityDirectECIDUpdate - ); + getApi().registerEventListener(EventType.HUB, EventSource.SHARED_STATE, this::handleIdentityDirectECIDUpdate); } @Override @@ -243,8 +224,8 @@ private void handleUrlVariableResponse( ) { Event responseEvent = new Event.Builder( IdentityConstants.EventNames.IDENTITY_RESPONSE_URL_VARIABLES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.RESPONSE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.RESPONSE_IDENTITY ) .setEventData( new HashMap() { @@ -328,8 +309,8 @@ private void handleGetIdentifiersRequest(@NonNull final Event event) { final Map xdmData = state.getIdentityProperties().toXDMData(false); final Event responseEvent = new Event.Builder( IdentityConstants.EventNames.IDENTITY_RESPONSE_CONTENT_ONE_TIME, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.RESPONSE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.RESPONSE_IDENTITY ) .setEventData(xdmData) .inResponseToEvent(event) @@ -350,8 +331,8 @@ void handleRequestReset(@NonNull final Event event) { // dispatch reset complete event final Event responseEvent = new Event.Builder( IdentityConstants.EventNames.RESET_IDENTITIES_COMPLETE, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.RESET_COMPLETE + EventType.EDGE_IDENTITY, + EventSource.RESET_COMPLETE ) .inResponseToEvent(event) .build(); diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index 080d4302..cfecc251 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -16,6 +16,8 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; @@ -326,8 +328,8 @@ private void dispatchAdIdConsentRequestEvent(final String consentVal) { final Event consentEvent = new Event.Builder( IdentityConstants.EventNames.CONSENT_UPDATE_REQUEST_AD_ID, - IdentityConstants.EventType.EDGE_CONSENT, - IdentityConstants.EventSource.UPDATE_CONSENT + EventType.CONSENT, + EventSource.UPDATE_CONSENT ) .setEventData(consentData) .build(); diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java index 95136a19..49f4c331 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/EventUtilsTests.java @@ -17,6 +17,8 @@ import static org.junit.Assert.assertTrue; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -37,8 +39,8 @@ public void test_isRequestIdentityEventForGetUrlVariable_nullEvent_returnsFalse( public void test_isRequestIdentityEventForGetUrlVariable_eventContainsUrlVariablesKey_validBooleanValue_returnsSetBooleanValue() { Event event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData( new HashMap() { @@ -55,8 +57,8 @@ public void test_isRequestIdentityEventForGetUrlVariable_eventContainsUrlVariabl event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData( new HashMap() { @@ -75,8 +77,8 @@ public void test_isRequestIdentityEventForGetUrlVariable_eventTypeNotEdgeIdentit // eventType is not edgeIdentity and eventSource is not requestIdentity Event event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_RESET + EventType.IDENTITY, + EventSource.REQUEST_RESET ) .setEventData( new HashMap() { @@ -93,8 +95,8 @@ public void test_isRequestIdentityEventForGetUrlVariable_eventTypeNotEdgeIdentit event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData( new HashMap() { @@ -112,8 +114,8 @@ public void test_isRequestIdentityEventForGetUrlVariable_eventTypeNotEdgeIdentit public void test_RequestIdentityEventForGetUrlVariable_eventContainsUrlVariablesKey_invalidValue_returnsFalse() { Event event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData( new HashMap() { @@ -130,8 +132,8 @@ public void test_RequestIdentityEventForGetUrlVariable_eventContainsUrlVariables event = new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData( new HashMap() { @@ -299,8 +301,8 @@ public void test_getAdId_whenValid_thenValid() { public void test_isSharedStateUpdateFor_stateOwnerIsNull() { final Event event = new Event.Builder( "Shared state event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData(Collections.EMPTY_MAP) .build(); @@ -317,21 +319,21 @@ public void test_isSharedStateUpdateFor_eventIsNull() { public void test_isSharedStateUpdateFor_stateOwnerValueIsNull() { final Event event = new Event.Builder( "Shared state event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData(Collections.singletonMap(IdentityConstants.EventDataKeys.STATE_OWNER, null)) .build(); - assertFalse(EventUtils.isSharedStateUpdateFor(IdentityConstants.EventType.GENERIC_IDENTITY, event)); + assertFalse(EventUtils.isSharedStateUpdateFor(EventType.GENERIC_IDENTITY, event)); } @Test public void test_isSharedStateUpdateFor_stateOwnerValueIsEmpty() { final Event event = new Event.Builder( "Shared state event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData(Collections.singletonMap(IdentityConstants.EventDataKeys.STATE_OWNER, "")) .build(); @@ -343,8 +345,8 @@ public void test_isSharedStateUpdateFor_stateOwnerValueIsEmpty() { public void test_isSharedStateUpdateFor_stateOwnerValueExists() { final Event event = new Event.Builder( "Shared state event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_IDENTITY ) .setEventData( Collections.singletonMap(IdentityConstants.EventDataKeys.STATE_OWNER, "com.adobe.module.configuration") @@ -389,11 +391,7 @@ public void test_getECID_valueIsNull() { * @return {@link Event} with the given event data set */ private Event createGenericIdentityEvent(final HashMap data) { - return new Event.Builder( - "Test event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + return new Event.Builder("Test event", EventType.GENERIC_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData(data) .build(); } 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 2e313f0b..9545273e 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 @@ -27,6 +27,8 @@ import static org.mockito.Mockito.when; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.ExtensionApi; import com.adobe.marketing.mobile.SharedStateResolution; import com.adobe.marketing.mobile.SharedStateResult; @@ -68,41 +70,16 @@ public void test_onRegistered_registersListeners() { // verify verify(mockExtensionApi) - .registerEventListener( - eq(IdentityConstants.EventType.GENERIC_IDENTITY), - eq(IdentityConstants.EventSource.REQUEST_CONTENT), - any() - ); + .registerEventListener(eq(EventType.GENERIC_IDENTITY), eq(EventSource.REQUEST_CONTENT), any()); verify(mockExtensionApi) - .registerEventListener( - eq(IdentityConstants.EventType.GENERIC_IDENTITY), - eq(IdentityConstants.EventSource.REQUEST_RESET), - any() - ); + .registerEventListener(eq(EventType.GENERIC_IDENTITY), eq(EventSource.REQUEST_RESET), any()); verify(mockExtensionApi) - .registerEventListener( - eq(IdentityConstants.EventType.EDGE_IDENTITY), - eq(IdentityConstants.EventSource.REQUEST_IDENTITY), - any() - ); + .registerEventListener(eq(EventType.EDGE_IDENTITY), eq(EventSource.REQUEST_IDENTITY), any()); verify(mockExtensionApi) - .registerEventListener( - eq(IdentityConstants.EventType.EDGE_IDENTITY), - eq(IdentityConstants.EventSource.UPDATE_IDENTITY), - any() - ); + .registerEventListener(eq(EventType.EDGE_IDENTITY), eq(EventSource.UPDATE_IDENTITY), any()); verify(mockExtensionApi) - .registerEventListener( - eq(IdentityConstants.EventType.EDGE_IDENTITY), - eq(IdentityConstants.EventSource.REMOVE_IDENTITY), - any() - ); - verify(mockExtensionApi) - .registerEventListener( - eq(IdentityConstants.EventType.HUB), - eq(IdentityConstants.EventSource.SHARED_STATE), - any() - ); + .registerEventListener(eq(EventType.EDGE_IDENTITY), eq(EventSource.REMOVE_IDENTITY), any()); + verify(mockExtensionApi).registerEventListener(eq(EventType.HUB), eq(EventSource.SHARED_STATE), any()); verifyNoMoreInteractions(mockExtensionApi); } @@ -118,6 +95,21 @@ public void test_getName() { assertEquals("getName should return the correct module name", IdentityConstants.EXTENSION_NAME, moduleName); } + // ======================================================================================== + // getFriendlyName + // ======================================================================================== + @Test + public void test_getFriendlyName() { + extension = new IdentityExtension(mockExtensionApi); + + final String extensionFriendlyName = extension.getFriendlyName(); + assertEquals( + "getFriendlyName should return the correct friendly name", + IdentityConstants.EXTENSION_FRIENDLY_NAME, + extensionFriendlyName + ); + } + // ======================================================================================== // getVersion // ======================================================================================== @@ -150,11 +142,7 @@ public void test_readyForEvent_cannotBoot() { public void test_readyForEvent_GetUrlVariablesRequestButConfigurationStateUnavailable() { // setup when(mockIdentityState.bootupIfReady(any())).thenReturn(true); - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + Event event = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData( new HashMap() { { @@ -183,11 +171,7 @@ public void test_readyForEvent_GetUrlVariablesRequestButConfigurationStateUnavai public void test_readyForEvent_GetUrlVariablesRequest_ConfigurationStatePending() { // setup when(mockIdentityState.bootupIfReady(any())).thenReturn(true); - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + Event event = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData( new HashMap() { { @@ -216,11 +200,7 @@ public void test_readyForEvent_GetUrlVariablesRequest_ConfigurationStatePending( public void test_readyForEvent_GetUrlVariablesRequest_ConfigurationStateSet() { // setup when(mockIdentityState.bootupIfReady(any())).thenReturn(true); - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + Event event = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData( new HashMap() { { @@ -268,12 +248,7 @@ public void test_handleRequestIdentity_DispatchesResponseEvent_PersistedECIDExis extension = new IdentityExtension(mockExtensionApi, mockIdentityState); - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) - .build(); + Event event = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY).build(); final ArgumentCaptor responseEventCaptor = ArgumentCaptor.forClass(Event.class); // test @@ -302,8 +277,8 @@ public void test_handleRequestIdentity_DispatchesResponseEvent_PersistedECIDExis public void test_handleIdentityDirectECIDUpdate_notAnIdentityDirectStateUpdate() { final Event event = new Event.Builder( "Not an IdentityDirect State event", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE + EventType.HUB, + EventSource.SHARED_STATE ) .setEventData( new HashMap() { @@ -324,11 +299,7 @@ public void test_handleIdentityDirectECIDUpdate_notAnIdentityDirectStateUpdate() @Test public void test_handleIdentityDirectECIDUpdate_identityDirectStateResultIsNull() { - final Event event = new Event.Builder( - "IdentityDirect State event", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ) + final Event event = new Event.Builder("IdentityDirect State event", EventType.HUB, EventSource.SHARED_STATE) .setEventData( new HashMap() { { @@ -366,11 +337,7 @@ public void test_handleIdentityDirectECIDUpdate_identityDirectStateResultIsNull( @Test public void test_handleIdentityDirectECIDUpdate_identityDirectStateIsNull() { - final Event event = new Event.Builder( - "IdentityDirect State event", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ) + final Event event = new Event.Builder("IdentityDirect State event", EventType.HUB, EventSource.SHARED_STATE) .setEventData( new HashMap() { { @@ -408,11 +375,7 @@ public void test_handleIdentityDirectECIDUpdate_identityDirectStateIsNull() { @Test public void test_handleIdentityDirectECIDUpdate_legacyEcidUpdateFailed() { - final Event event = new Event.Builder( - "IdentityDirect State event", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ) + final Event event = new Event.Builder("IdentityDirect State event", EventType.HUB, EventSource.SHARED_STATE) .setEventData( new HashMap() { { @@ -457,11 +420,7 @@ public void test_handleIdentityDirectECIDUpdate_legacyEcidUpdateFailed() { @Test public void test_handleIdentityDirectECIDUpdate_identityDirectStateValidECID() { - final Event event = new Event.Builder( - "IdentityDirect State event", - IdentityConstants.EventType.HUB, - IdentityConstants.EventSource.SHARED_STATE - ) + final Event event = new Event.Builder("IdentityDirect State event", EventType.HUB, EventSource.SHARED_STATE) .setEventData( new HashMap() { { @@ -522,11 +481,7 @@ public void test_handleIdentityDirectECIDUpdate_identityDirectStateValidECID() { @Test public void test_handleUrlVariablesRequest_whenOrgIdAndECIDNotPresent_returnsNull() { // setup - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + Event event = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData( new HashMap() { { @@ -571,11 +526,7 @@ public void test_handleUrlVariablesRequest_whenOrgIdAndECIDNotPresent_returnsNul @Test public void test_handleUrlVariablesRequest_whenOrgIdPresentAndECIDNotPresent_returnsNull() { // setup - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + Event event = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData( new HashMap() { { @@ -631,11 +582,7 @@ public void test_handleUrlVariablesRequest_whenOrgIdPresentAndECIDNotPresent_ret @Test public void test_handleUrlVariablesRequest_whenOrgIdAndECIDPresent_returnsValidUrlVariables() { // setup - Event event = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + Event event = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData( new HashMap() { { @@ -723,8 +670,8 @@ public void test_handleUpdateIdentities_whenValidData_updatesCustomerIdentifiers ); final Event updateIdentityEvent = new Event.Builder( "Update Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.UPDATE_IDENTITY ) .setEventData(identityXDM) .build(); @@ -750,8 +697,8 @@ public void test_handleUpdateIdentities_nullEventData_returns() { // test final Event updateIdentityEvent = new Event.Builder( "Update Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.UPDATE_IDENTITY ) // no event data .build(); extension.handleUpdateIdentities(updateIdentityEvent); @@ -774,8 +721,8 @@ public void test_handleUpdateIdentities_EmptyEventData_returns() { // test final Event updateIdentityEvent = new Event.Builder( "Update Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.UPDATE_IDENTITY ) .setEventData(new HashMap<>()) .build(); @@ -883,8 +830,8 @@ public void test_handleRequestContent() { // Setup final Event event = new Event.Builder( "Test Ad ID event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_CONTENT + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_CONTENT ) .setEventData( new HashMap() { @@ -908,8 +855,8 @@ public void test_handleRequestContent_EventIsNotAdIdEvent() { // Setup final Event event = new Event.Builder( "Not an Ad ID event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_CONTENT + EventType.GENERIC_IDENTITY, + EventSource.REQUEST_CONTENT ) .setEventData( new HashMap() { @@ -939,11 +886,7 @@ public void test_handleRequestReset() { extension = new IdentityExtension(mockExtensionApi, mockIdentityState); - Event resetEvent = new Event.Builder( - "Test event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + Event resetEvent = new Event.Builder("Test event", EventType.EDGE_IDENTITY, EventSource.REQUEST_IDENTITY) .build(); extension.handleRequestReset(resetEvent); diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java index ea5b44e4..796f74c9 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityStateTests.java @@ -28,6 +28,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; @@ -788,11 +790,7 @@ private void assertUpdateAdvertisingIdentifier( * @return */ private Event fakeGenericIdentityEvent(final String adId) { - return new Event.Builder( - "Test event", - IdentityConstants.EventType.GENERIC_IDENTITY, - IdentityConstants.EventSource.REQUEST_IDENTITY - ) + return new Event.Builder("Test event", EventType.GENERIC_IDENTITY, EventSource.REQUEST_IDENTITY) .setEventData( new HashMap() { { diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java index dddc94b8..1abe2035 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityTestUtil.java @@ -12,6 +12,8 @@ package com.adobe.marketing.mobile.edge.identity; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.LoggingMode; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.util.JSONUtils; @@ -74,11 +76,7 @@ static Event buildRemoveIdentityRequestWithJSONString(final String jsonStr) thro * Helper method to build remove identity request event with XDM formatted Identity map */ static Event buildRemoveIdentityRequest(final Map map) { - return new Event.Builder( - "Remove Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.REMOVE_IDENTITY - ) + return new Event.Builder("Remove Identity Event", EventType.EDGE_IDENTITY, EventSource.REMOVE_IDENTITY) .setEventData(map) .build(); } @@ -96,11 +94,7 @@ static Event buildUpdateIdentityRequestJSONString(final String jsonStr) throws E * Helper method to build update identity request event with XDM formatted Identity map */ static Event buildUpdateIdentityRequest(final Map map) { - return new Event.Builder( - "Update Identity Event", - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.UPDATE_IDENTITY - ) + return new Event.Builder("Update Identity Event", EventType.EDGE_IDENTITY, EventSource.UPDATE_IDENTITY) .setEventData(map) .build(); } 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 bd2aa37f..af87cc32 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 @@ -26,6 +26,8 @@ import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; import com.adobe.marketing.mobile.Event; +import com.adobe.marketing.mobile.EventSource; +import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.ExtensionError; import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.MobileCore; @@ -120,8 +122,8 @@ public void testGetExperienceCloudId() { // verify the dispatched event details final Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.IDENTITY_REQUEST_IDENTITY_ECID, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); + assertEquals(EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); assertNull(dispatchedEvent.getEventData()); // verify callback responses @@ -382,8 +384,8 @@ public void call(String s) { // verify the dispatched event details final Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); + assertEquals(EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); assertTrue(dispatchedEvent.getEventData().containsKey("urlvariables")); assertTrue((boolean) dispatchedEvent.getEventData().get("urlvariables")); @@ -453,8 +455,8 @@ public void call(String o) {} final Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); + assertEquals(EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); assertTrue(dispatchedEvent.getEventData().containsKey("urlvariables")); assertTrue((boolean) dispatchedEvent.getEventData().get("urlvariables")); @@ -624,8 +626,8 @@ public void testUpdateIdentities() { // verify the dispatched event details Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.UPDATE_IDENTITIES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.UPDATE_IDENTITY, dispatchedEvent.getSource()); + assertEquals(EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(EventSource.UPDATE_IDENTITY, dispatchedEvent.getSource()); assertEquals(map.asXDMMap(), dispatchedEvent.getEventData()); } @@ -678,8 +680,8 @@ public void testRemoveIdentity() { final Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.REMOVE_IDENTITIES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REMOVE_IDENTITY, dispatchedEvent.getSource()); + assertEquals(EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(EventSource.REMOVE_IDENTITY, dispatchedEvent.getSource()); final IdentityMap expectedIdentityMap = new IdentityMap(); expectedIdentityMap.addItem(sampleItem, "namespace"); @@ -741,8 +743,8 @@ public void call(IdentityMap map) { // verify the dispatched event details final Event dispatchedEvent = eventCaptor.getValue(); assertEquals(IdentityConstants.EventNames.REQUEST_IDENTITIES, dispatchedEvent.getName()); - assertEquals(IdentityConstants.EventType.EDGE_IDENTITY, dispatchedEvent.getType()); - assertEquals(IdentityConstants.EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); + assertEquals(EventType.EDGE_IDENTITY, dispatchedEvent.getType()); + assertEquals(EventSource.REQUEST_IDENTITY, dispatchedEvent.getSource()); assertNull(dispatchedEvent.getEventData()); // verify callback responses @@ -994,8 +996,8 @@ public void call(Object o) {} private Event buildIdentityResponseEvent(final Map eventData) { return new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_IDENTITY_ECID, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.RESPONSE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.RESPONSE_IDENTITY ) .setEventData(eventData) .build(); @@ -1004,8 +1006,8 @@ private Event buildIdentityResponseEvent(final Map eventData) { private Event buildUrlVariablesResponseEvent(final Map eventData) { return new Event.Builder( IdentityConstants.EventNames.IDENTITY_REQUEST_URL_VARIABLES, - IdentityConstants.EventType.EDGE_IDENTITY, - IdentityConstants.EventSource.RESPONSE_IDENTITY + EventType.EDGE_IDENTITY, + EventSource.RESPONSE_IDENTITY ) .setEventData(eventData) .build(); From be93222f62ee9d56146624f2f61cddc7ea855678 Mon Sep 17 00:00:00 2001 From: Emilia Dobrin <33132425+emdobrin@users.noreply.github.com> Date: Thu, 22 Dec 2022 14:02:41 -0800 Subject: [PATCH 23/50] Update dependencies, remove jetifier (#80) --- .gitignore | 3 +-- code/app/build.gradle | 21 +++++++++--------- code/app/libs/edge-release-2.0.0.aar | Bin 0 -> 102427 bytes code/app/libs/edgeconsent-release-2.0.0.aar | Bin 0 -> 27489 bytes .../identity/app/EdgeIdentityApplication.kt | 10 +++------ code/edgeidentity/build.gradle | 4 ++-- code/gradle.properties | 1 - 7 files changed, 16 insertions(+), 23 deletions(-) create mode 100644 code/app/libs/edge-release-2.0.0.aar create mode 100644 code/app/libs/edgeconsent-release-2.0.0.aar diff --git a/.gitignore b/.gitignore index 74484c94..bbccea0c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,7 @@ jacoco.exec # Application files *.apk -*.aar *.aab # Dex files for ART/Dalvik VM -*.dex \ No newline at end of file +*.dex diff --git a/code/app/build.gradle b/code/app/build.gradle index 6fe46a51..6bfbb5c2 100644 --- a/code/app/build.gradle +++ b/code/app/build.gradle @@ -44,6 +44,10 @@ android { } } +configurations.all { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' +} + dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:${rootProject.ext.kotlinVersion}" implementation 'androidx.core:core-ktx:1.3.2' @@ -59,20 +63,15 @@ dependencies { implementation project(':edgeidentity') - // TODO: Uncomment next line when core 2.0 artifacts are published to maven + // TODO: Update these dependencies when all 2.0 artifacts are published to maven // implementation "com.adobe.marketing.mobile:core:${rootProject.mavenCoreVersion}" - implementation 'com.github.adobe.aepsdk-core-android:core:dev-v2.0.0-SNAPSHOT' - - implementation ('com.adobe.marketing.mobile:identity:1+') { - transitive = false - } - implementation('com.adobe.marketing.mobile:edgeconsent:1.+') { - transitive = false - } - implementation('com.adobe.marketing.mobile:edge:1.+') { + implementation 'com.adobe.marketing.mobile:core:2.0.0-SNAPSHOT' + implementation ('com.adobe.marketing.mobile:identity:2.0.0-SNAPSHOT') { transitive = false } - implementation ('com.adobe.marketing.mobile:assurance:1+') { + + implementation fileTree(dir: 'libs', include: ['edge-release-2.0.0.aar', 'edgeconsent-release-2.0.0.aar']) + implementation ('com.adobe.marketing.mobile:assurance:2.0.0-SNAPSHOT') { transitive = false } diff --git a/code/app/libs/edge-release-2.0.0.aar b/code/app/libs/edge-release-2.0.0.aar new file mode 100644 index 0000000000000000000000000000000000000000..53d50d1d8621238677a207c1129fa9957e3f2b96 GIT binary patch literal 102427 zcmV)HK)t_EO9KQ7000OG0000%0Kpm}DTpcn08beK00jU508%b=cy#T3TXWnvvgZ5# z3jct4Q0ZptQI^_mNBE*+soOKLvB6*!QB^!#0;ICCmqeH-o9`nyDt+IVW2! zou=VcxtZ#&eOnIq^$%XcK%{<{*x_xt`Of?O*j5DS0ebUk&kl?LJl6jrllrEf9;fGq z1O(Ea%5@wONq%C#g5`(W^{uXUY@!iIKBg_1`TfxK_uWq#@UeWhCc%>OiM2+Bg3a* zATcE9Vd#XaxNFLB^a1{?*-s+zM?F2??K>_Y1E73mG;Eme>$2%~va-MPuIaFIw+eS$$;fZrXbixy)=Q zZ|ZUC?z%(Yu!+G;ruvSw8$CzC#G_{vltSnErCC|lM%awXtfn%3d^h=j)j zpIp9F&+yI9@z8DdZ1a8H{qhthl2{d(EgM*;pqakM+}`oEZPPVP*^hkr;u@!&?%B8* z>K>GyALq#)FT&!{?PkN=ri+ASd3e`!T!vg#BtfE+K6eA}S5Gojcj%g1(7@&7P978T zzTWK{{(oOh6of40H?G5cO?w#XLpeOtDfi{@-IMzGvz|8lr~UD;_Lq4$)RV7=V`C%I zKeSxK{pKnmC-GZ7?Qg}RHqy9{Y>T>UN&V{U%_W(uvmurYwfM|uDyR@&Yxtm0M#QG$ zYOHR{sT-s``zFG1^#0H5GjY+D|i=|7Hacz%!OXeF27ZfM!?wXEvn=u78|%9afS zs~*a#LULSD%WzeXeN#Sv={>_x`F1Vp;rni=|HXU9l^c~zR6?Zv$<2a#gNVw&!pkP4 zu;Es$;kbV*W#E6TM@km~q*9Q%n?MP@XIOk#RZpPTs4>^+9(!LWd8)t-+aDhJV!!&b zt!|a=X4{c|i*s8KBd^=Yn!06A-BZ{58nNwMA%Z9G%0t~eh1HrR61vx4vR_wEQ-NnXQF zA=Yfq6A60NRXZkEKmG&NTwrRQ>`hgTcGCD0ZyUM3((=bo?8 z2Ud>9fr0rV8VX}wH_mEx;4`|GGC&PoJDVmkyL&-mwYQ{UCDR@cY#?{;9bfghIe+V! zb-nCJIg3AcULSVxTAv;@(mmP#6V_O&H~#52SO;#TcI2Wl zh;5d?G&_yZRW`^obw}Z`gL+{(m1)#-LQRc~efL**6dp@3(?U*s3iWpOf;jkn_w#m? zTO*vJ^t6Wq#;4~gmhq8yV8gC6QniGUD=PGgqb83p$Dzsf=FdZq+lKy zpQ;sda1dau3vA$AJ@6E}J6jtn-Tq(M$XC!uuQ1Sf&x~`xP|-kN3Ar&0F}lYDQkMP4 z;I{1~tJe1HG_6Y@Lhr=F~g6kl!_@5kY8hetf; zUEPuiZ=F+NCmC55Mw-XEFjAk)!(=DT9e|it38|EjK7qsObvZ2lCvsg@NFrsb`*+>I zq^k7QL1BjSK%RCg3|^%sydH>09zoHMJ)=KuxWT}F*-wA)75ziIZTK?cJ5&21RzPG8 z{1n#Ktv-SdCVmn2>objkss=#s>R+sq2L7k=Kiz)Cn7R#M#G(++Mnb4u^uj{sF$1U^ zT%&r;M_t58$n(pB%UHn!j^AjzD{(JQaTB!GOW@XPr<3?!wM~B7mf-o zKehv^9KWNJ3{G{T11C8F+4n-2A|yg?M#yAg02lTeq%h*xFx)o|#Md8&=GdxJ#$cX7 ze58QHe8brdLVFmqUCpXU_6fAK;_8+!mm|w84YAS+%PSJ*IXVzuHdTGd?))ex zp`eWXgV{1Ts#c3pC{HiWmtkTZQ|mV`knWP|%ajKDvYr#y&5;$mfjt**&d%bXnpIJ3UKj^QPzQ;(Z0^czna$ z@nq{k*_l-$KF`^>s^}qkN{jbb2^`*hy<-0h&up<8~@S=7c69YEB!qH{wwKDx3h1Abu*>g7eez)%z*R$E;pb;WTg) zrq)lBr>@UnDqEC;)8%a8h(?`4nTI081?P7m!`2KZP@wlcgvtuBU9c)yPRRWn$rh@Q zMVX~HPSDbpRf;C1l(Q+vO(~}kYmcMo<`Iq;*f^1Lr#xr~t0 zp^#C{sDTMHUyY_Jd=RmZ3HU9zITw*O9hniV<_DRnTy*5I!<@hMN3sr$k{) zB>MyeQOsEztiAyZS3Z3Z&6>v1Qk$uO(J)?_G2#so!jOo=sij|sjzScgT)C(z zWg(@TPPMo(BZg8415p`pg-N8GWb;Zv_KwR)(?TvQGGw!lO8Wnce|a!MSVc?o6rzZy z5t>bALG`{4Vlmc;P`1iX3Wq~N35w7ydTsH;sk(qi5~?9Qil%*;QZJJ_H!6h)j$U`J zMNJ%KTldbX?*fosHhDwLUnJx|2i4zVD1Q}!`Ka{;T!_S8CScw)6R{2~v2 znFimpvVy42G;{-z7irJ~pWDZv$}q8f*6l^0LPR*f?x_Ze_1B|K%M-)M?9?%0YPJO2m%Y#1Dn@TKU+$r?^Lz_o*L(xBsfUf2k5+(> z2dsTCYHvJRaXTKoA#q+QrNBPXw6KS=7M|zhp*2n7AcgDfvS`Ii%n41EM^;JQEukY^ zevw!gsko&_d@*eN_5(!}inJVMw*@&|o2N(u*r&V-2@o|@)_Jl-Yj>cv*Ca58e)YFp z1HFT#6Zw4F)T0h-EqIlNvgo#3uJ_Xz)1J@Beb=}R*KpUzM!=p`p5knIPiztL-+|(5?@1XW) z#!*rNdUX{*t0`)Ai3P??WVei9b#$RT0>L zU)l%n;p&^5x~SJcVCR!qBdJRh&^i4G#*Z5M)pcO#4ctW{?($6xw=bJ*a^VIzUIPOh ztcD2r&FC;^Vd4vJ=h(TXtTJL*OSerqRNvXN_Md{V9Lyj~ ziy<%SGncY3UcP`QtTz3IUPN>kMkJFxCShn& zFT!Dcsz5u~Sk7R_ZH~hj&v%L1=5x5M8YYg9WC^r}2+{(&!oxe%mG~#SK$DnPq%=nJ zOEh?q0;PnN`i7>Y0h1hSE0)C2mc9d6w`iN<1W9%tdSFaZDj{j+)@2u3`Spo*BWo^E}VW$sWM^W8ZKb%zewO%`!N(>e~hAbzts7JPyK5^iS&^EO^>lT`6xF)7jK) z^kpP4uv(cfr1J!Lsp5y-7ohHXpH#vG**ZfX>#3ed?V|8)cwaot8T42Nzb7{e=gbOiFhEgzo zH6(=yN!lDq0taSrtE@M1kMH#3$-)(EJ#ZEMa^i%a!r_wnl<#a9Yn>-u)693hz^x*I z0L8-vWmnNp4SpGbTyN+OSc}KmalEjklc|Hys zq-=FmlJIAtlFWD@5cD5zC-KpqHb*U(JV=OWiGae;iWKk06>p>;~?|XyyUl7U3TmjSoK>SH64oULVM1u_)< zw3LhtRT^q7EHo^+XnXlLaMEjR1_?$oHV6@!2A@sA z50KpGJ`H4uaw=U?!9BRVYOLj&d_c9QD zm4^4&{6TyVA$dH%n#VE(V=c}6*NbLr<1Zn1s z(*RprXC;AMR#AB_>GZ>4tCj?|&%hHouyjtu|GBZCf)Mx{+~RN!W_PcWF}5!Vj1V-X2>y zixcr&AD5}vabItf`$OzIlflo21ZY379$pTPBh|7DG0t?dP(_AHlyOlRc-u!8$IHh# z1hG}lAPB#K7Y{*4Qor}=rhOHN1n;irGh(`1nOTMYtV?L=(H;~K1ba`c0ihk8Ufc{4 zTy|(&v{~Hty>kz308@OlTww~yH=DA|1?OC+9ssI)bqzNc4SsU$K#7Fg?~9Zs(Lisz zBs{foG@5E5$p|!4zq}0Z9SR@Ha`j}m*@zg1+c$HbhArN5TVa*WIL~9)Ud$BAUFpuW zIOkq2>3OYDq`U?x(#0vH%Pi7+WprebKFB-AvN-l<36dG7l_i|;@)MfHeBtmZRq$tI z-?*HT(I@c+;t7K)K})gFlOx*9MM%{(>$N*__;}6Bb*GSuEdw zDDIs1N%J#%c@8fa74v%#r|_2*#kraPq&)JMx4&RvQ(4IGDxSiGbU4Z`9;H(-u`jhi z&)NN^&KN{HPR-;=ZOSs{7?}q>!QcZ0>T2|eQSC)8dL@HneGfAYep)%?wkSxDJF*NK zxmOeTgl;wW0`SjfB3Ca59%&*%e%rH_o||4uq&qNP+;MMxL^94nUsDNCQoCDO%pg8- z>A|$*o;C4V{c94_d)Z=T^|i$e_4uFXtNb}NlfnY;f#qdNk3R_x7I20tiNZRY9Y9Lfrmhmd*NZD)L(YKzD-luYZQ{TBw z#Rmoz*DIteX>{?}HMQ6F4RD^yb$&sdM72H4qI#HOk(-Mkp}2!M9xFO7nfk@=9uG0I z3KHLbl}EejoslS~h?voT5}GOsGrI77DK*Y4JYW6GVuf~kB>Sb6KA}u<3xx>qkN6&< zDhr~W-Wt(P8QibSws?|JF{H5(_`~we z=Onybe)1Hz!^u$JE;B=kd#K5sLU2PqlaxG?)tThtPX=ai^7Q*h*9ll}U&F(3BIrQ9$mQ zFu=P5nqf(v!wA~v35-#Kl>!eyv@RAXmfuxMepjjaC5qwKsnHEj50fxRp=}W4;gQ4; zTh>9H8*Pwig{ZS`&Mea6alIxl9Q)l0V$RLw(S6@4I|KQSl>H^^+)O<7P6pZI#}Ba$ z{rjohWsaRBQ)_(&QwWV4sglcggI03*|KWuG}<|!-O>7+=f%pj)|Fa0%f zOD_*uIw6Ob*X8sMS?DL$B5$$I44QgTywrPP(FW-7dOL#F-|mz>@FHj#WzK0bUz0;M1&Ga``jgWK~l@} z8l2J2tmY+%T|%H+M!?D`;ceNGZKs5$`8nyMP9u|JU^2sBawA`|gI==ZU2%85ZLr^lz96q=G9m2yf< zNoqJrMkGmQ5XnjLBWWaJD|cGlNh0e7Wu|m{UaZQAVJfM#lS<7OnapLu!%bZrOsa5% zWH&H#!BE5#mJMasoG`;BGaS+rAcewoGDDkAjcOuzWC#g`SJ4;qXZB}$3o2w0e?d?#T}CiRLR{9Nl;hJed%Sl ztmwhix7M%dQDhDKA#1pzhfjSKctsD}o|p4Q%uC)0kK&^8S1WqJ;8w@m@sEC4J680N z0a$g)iXJR+73_*0AiloB4Vwp;+)OWyk3t8m=)r*-Dj(DrWmoi&@V$EX>YN^kb{<_} zubYA8UyN7Wk`>U%8ecvH>)?a zx>ZsDN_Ta(Iy+lm(CSWNi?XeTuCCPmyTxuOVMFqY(&;+BRfXvs20XYe;E>xZ59oP|F@1R1;{?mM8HZ)a@fnPQSRA~#pJPHIBAiqEgu!Fqv zc9^@%{I75?IdFATwNBB7yEIeo^%sDA9qDFNxAjz)&78#hR&}dgWy8leOY`>pr1jmn z)6N!lTwwz$FK2~l&wKIRYc%u-D=yA|lCT=Lc6W0wJ z86zEKFvh(Kv zw?%x7xV$WvpX=r4O?uOo0$$KSPbS-;JAA0ph1ipY_yCj~@MH=0WHWUi)HcWNa=jN8 zrvZ~8;1?bwU_Q{N4HixVdlPt zkD=17)o}Re>nDhUA)D$VpMnkcp<~SkyX8G=dSvrX#M4Y!O=qaJSfWI_eem? zpbN>Nl@;Ph06qCN2Cc4-@S!;`$_h+cWA(`1EHAH`?0_{zH!nnCvXv zJK-DCA4R>0NzOu4CB?*LAxLd`5t9?ONBAk^dHx8ADlH}|7Wu>YuIWTr$E%B&%y>e& zRFM&hS0N6umzehw4n2M4&2Ix8>37R4e;_HY2XtZihWbko-$#rDx z>m__&?S&$v4hNU=+;D00Qrzo^8~|HwY0bmn*AZc8j5u3W&4@TOcC@V`MWmr2q~LW# z3J9KjkL`6t9tg5rcV0)t1CiC_6%Odv5ow{w(h2=KA~`)Aj2m+QCL%Zj4}%DEO=^ud z5oxg~GPvYTL{fSkhpaZ~5i#jq5qT34kzskaM-d{jG3fAL1&>IB4PPeKn}{ITJSjYH zBI7^K0mQ0qkr5x|Nst8JM21^_HyKhKm()#U&?%zvQrb;K2-Fvs&6grnr6b~?hzcAL zg;A{L3LB9E`(Q#Sh9$KACL$9KSp}KAx`~K{pZ3VOO;Lf3a}yB{f-gktCL$^}cW$b> z*J5=R5gwavULZ8zPOv(Q$Pt~#lgc0>MNS6L?q$qYXAx1F^b&8ZbMw%x&LXlkvt_;i z7#hxr$(Rf?UqWgvN7z|R$Rt~%W3nah8+z9bOwLZ_-aCuQl;REL!Se8(MdS&-Z2O_z z!gt_@yQbtiU={O@s(t<(v%ZQsM;(H?pMmota$@qpIkXTXA|i?611oaP|5W}bbYjd& zig|S_co6|Hcw#Zt#YH3o(Nt*>5s@_groApJA|DP(l@t*UN&4KWd3}zL`R3P*jZj*A z2IHXM3-(|Za507L5YAOjP#qOB2S3fnau=&5xS_m4#bG)xvEP^Itj7mb{wPUs1v@|x z`%rnt`pFom;Sr4vCV<5*pzb1u zq0_$Jd~ad@fWw^MEzv8PZyzj75d!ltbke(VocP4>%|wE5$F0N>4H5x<$HdNqZ~E8{ zPrDEa?j!TXrW2e8uI9=e9ytOouR+EU!wVdK&1&%KVvh5>x|r9>t0fTd>SB%*ypoZE zxq||~mN;1`oE{H^RmN~S;ZKOY7N<&Vg8ldy7#l?X=6`{NU`*5ILtC+5fuRw2(PnoZ zc`(Ys7zg4ux`v>K^vxgK^k$BXx>_y5 zwpsw?<$|piz#@XH)!*jQRts%~gwCqsVsMFLP+4tP^g@ikBe@Hq|A7R}k7jzO=1Lg} z|2q;e68aA$XMX5TJypjOuFUyKBgD7Ly1VigAp9MPTmbwJ6s6+_Pjw?NAVFJD0uRBL zD{LlOX#3rX@@kPk9QBNdj(H!fmWA)OTW$$?Ojx+?Qg4LhUTdk``SPmI`D^|2teh42 z*=Ctj=Wmh}HZE@2O{h50+os-$_xMky-tFtAk_yJ1TY*e=gmD|7@P(iEw*NJr0PUxA z{2vc!%6UloC1-ca(a`x^FU~lyay$+UUgj3QN;vZ2eTQbAhYIhXsS}>@6@IDJ!|?Bo zlw%Rx?&CW0@HLGZg5?)5VB5?Ri#;GLJ1r{gS| z@p6Floi8W0U!a$&Ws3p{vyx(j8ylo+_P~r)bcYo$R9t0VYsB9l^in-k!CG9Y^whk z*+wzQGZgHA+fkm1;E>}PrVfO}6-DRRvuoTLp$*DKahxx2lwT~W=VDPO=bPo{qOs)5 zG65}Oy;8OpN0x8tizP5)UNZH|8@|?Cl-fQb_sJ^4%S#MKF=cPXF@R5C5zPtW_X)4X z`JvYDd&1sC1aEw;Pml$xhnL}o9}HUa*q0kHPnqzlZyGjVJs3DeaiB#nHy83%zR#Tw`*ypGpgnc{e0+y$W!#82?-8nUF)9}O;(XBp zE81Il@;h3_g?o$NZgUh<5Vh0;t`BU!y($6S_DA{NrmGG-;6wTA+p;n6W93~ouynhV z-UM}rLp=$UJJStR#kS?KnS{IdJ)qqAZD4nu$jzP91_BlzSWl|A&-XH8WLy~N#(z%Ze_DU_}#(A3z?Gyx{Lp^e@_ z6Yu`Og)??hBHj-zu_AayDs_|6BX%#%`eQtx&r!5H@?H;7e$Vh-j07n%uSJoe@V@#_4`ogV~e4hcn* zBu4OBXfe{qy8Vu3On@ZTEO*X?@M25l&K`=u&ERdh@k}9yU`Pj=Ne>^xe+$ zmSZ{JT3!Ad+#o?r8gDd;G~?P%Mv_DxbF*Hjo`tJ_(B8=<#aQ; z0Re$!AA4>(rhGg6e=&uZ=#8;&36p~6gD8L0Z=QXonJzoq9Tj=u6tT60#AO%jT#k2E zT}ImIQjHh~(lhPlEqHI}bpnS7!j|um9oNnV$v71}+ZreP=iX*68JNdNzJ~db@&%E~ za}_4RX!M=F{|``00|W{H00;;G002P%$H-DZ6#@VNcLM+b6951JL2hJnZ)s#rVQy(= zWpi{ccx`NDQ%#SXFciG!SG@YvN&)uc(oK`B=$5LeD^dk^S9>(@AXdPpwpo&YzYmAB zsp<=|8OHNwhUNXCZQu>H5lXJuwSUC`WuZzT>lN#5<$Y%N>)G;y=golDgX)f2)H@3| zSJ!tCmMTXd!lnUtYCxkw{f4FQ;MqN6&qfp|4VGXP990j!EK!5qA%q<-=<#?0 z%;EW@2X6c;_&5UC^oo7@;&{i;TYdzoEc6D6gFEhj7|ba!;rs=7o1x3IIeTLi%{3_f>4eWmMQn#Kf=@k5lPkw;=@j^Rl*XhtV~ z_d?US`4M}b-;3eE{!z7EN6gFGfM!&XE8YNkn#hWB`%$x5JCa>@_ zPQJ|n$u3F;51k$dA+=((l0tRijEsL)uTib}Jf&$JTdicNVKN}}b)pU!>CJK)_uAkD#a z#+(YuY)^_-j1WF|jcvK!<7m0xs+?-$V_74E^(nw5Rmym#+`HYlCcZ^0WLoZ@|NJg@ z>_3R*x<-46y1iu(5cM)RPqcmqP)h>@3IG5I2mk;8K>+qZ!o4(M0RTyi0RRgC003ib zVRLh3b1rIOa-_Rsbf(*~HXIur+qSLl*tTuk>DWofwr%@~&5n(ZZFKTwy?gERp0&n! z_dff4_rG~Rv&J(X8+2X;9pz28~Z(M zq6Ctj#?KWdFc1*mzcv=Kv$41Sd}C)TWMFM=XkcXJBx7J}U}oY-%0zEuZQ$gT6(dy+ z@(oVJ?yV^Y@&Unlds$Fk`|BPkP85+8!oZvCx;3 zCnY?Ydu&Z{XZ9J@OW*WHWs6tvzq-RL;KDZh(-k40KtNLeYj-GpZu|Vn&Q{#S+TO(R zPvLrnaoJ$CSZH2F^cfK{v{3sN7cWmKwF)tJM?|!}cgB50-oRtP@zU|; z4n_F{`le9iW<*K0JwDazoylW1mBn*-J-q??UFfWuIJDD0AQ}x7+D0!lhVy5(vmjFJ z0c+S)ro5Mbd`v}uxS_CEwfq>$JLfYQu><*@W=^bwxR^ju`bet2tLN{&mh3fhTsM}D zl}3HTpWjW4;Mwz-ky8K)0c|~9ad#y_hBRRT$7+Jzo0!<{-#EueP4a>ysc++F%FU!& zTxYD|puyF0Bn8`WgzMdqJel#Kp&4_U>W!^DjsC@vs`!9a|Aj}M&d4d&`$NmE^p8Ja(|cU1feF^-J1+} zv&9F7;mECM8~M;i(fTm9q|IP|#V^T53vSsPE*Y**|b$0}Q*Al|Q8SqNRVlLgs9j+<*5NlW8n z@w&WR^O}b9`+a+Q2^6!30}t%qdc2P;x+*a$JY|0=F4cDK-J%=hRJ44LZahO*OhGJ;RC{Uy?6{oENP~k z`(DjtMp_S%6jgE|IX_>kk~^4!&PS8Mh_^=DUfsx7u6Krj6TG58Lf{`GPxrfGsSX|< zB>I;oB3ODtjl7r&pX8(~9#3MrrvymIFK;1NZko|P+0JT%l$ecW62?X=Qlwc?-yeGI zZKhyIE55IU?VCEf&_6WFT8TrQX?Hz%&yE!r;e5joG{8mSBH8jhxVp=RZ+ z?OaixZN9)Dn;l9F*yEPnCWRdsIV{RTKdMBJV#}|G<&zw=z%#Q~B|PIQlJGyjZqo8Z zZR_HmqB0fAwn}A=Xt`<*@x?o2BDCNB84LiK!r=3tzix6?A+oO$Ps5#a0LBPkN#Zh- zt>3ONuc%k+A9>;0$Ny;1Njvtek5xp_SLUFGOU>yB!;T~EdDzbRlKGc~WrjMIP z8NyVyM_sI5G4Z1KKEE;K?pTCxDv7xSHYH2>6-NR@jTsVVQ}NeuV#kupy1YYJ{BB&@ zyRyHNLey?@%%gneN_KYuddhepzXzm#Vw;F#3AqlD?{U?-BXn3R~C>w(TsSyN&x7-xlbeoHwA>AIdYQMgsua^24J#J1K1M+}xI zIZD3?DDvL=xEa)mAP4ptXvTV}RG=(vU99qKFn>ySsgtmK;)TB1fn2!4a)G=I3`GzF zW-a*EwZ$p>Y?SaDZ+ytYdM9OCPLh;Nt@K;%#{2_jlg)Ilo?q0G?>C*IwbpIB%$*j0 zQUAa<_5y@Z^LB77$-8)%34kHakj{hD$>R&l*DL6BUqc#iJ}vKKhxZF2E!}65ro~#k zlP8qE;0KE3SG;C6Tyt|<^Rj37IEpZa(peVg*(wrvF@n5~tR68=mGldXS1gxvJh@r5 zKr2{&mazrkcF;SeXOAF8Fc+;0fJnP9bcq92VCMpAkou@QR=~-Dx2no?h!DsP3sam!n z@%Mv~;tC=w+Oj);a36iT(gU}MyiD?eQ*(e!KzlG}O^Z|OWZZr`7J)X-`I7ASl__N8Lq19(B3A~t1{UxJrDK0zYr zn+;d*Mnu_V-m}9)9v#y)y--U4UC6Dez6WOprtr)(W!K#C7C$k!Xbuh0nJ!?l3l(7N z9TrPX6qS#^f4*05WVQ^k9HI`wmSoI8c5JKXqGZt*fAWRDujIXX`t3ev54MLF0-K`#(3g0T};9O0nz`vbYbgcV(YBrY~bwT^k-q9s-cCVhUqJZ zZVDB+n`@0GlVk}cC7jz_uMDqHMpOruZ;lo+`aRAVf&yj1*0s*-dM5g8Qfq{!6s)brw_)~~nc-A4)_&nqP|-Ydx~s;hzkqC&0bu;jYp zXmQhgf#U~e=Z;|_~@zSz-9L}Nc6?H-YS#Y-H!P-56kW`I{1V~uTYI9OEOiv-J z!UC=;`6Df-ACA^HC3aQd)nAWlRBCa&FsWY}Oc$|?OJTccxz^KvasH$+|4PC|cg9@G z-~*gRMn9aV9{VnwLNLh9atT|kpg&F3MNqG?otPVaqW6?&BkR#I~iv2K? zTS;1nk)s03#VYL>!6c^M?V{Bk{_c`(Fj;2h_{pD%)#$GuIO<-cL}N2uzd4OsoQ*a| zHH(9m7oGCpJh2jO2?XMqCggRJwXqH{WwRH>sZpFOb7hhPY?r@ru=QcBjX_3(cTfJ{ zIPaT~^p6vvM@hGX*9U`!mH{ocgjoRrAZjzJPztmt>^%s|zEN)M%={eoI@{xVNT{`0=v2xQ)I{R8<8G zeXh_DY9K{iB9qLjv~QdO;~<0icS+ldu~yNR+qZ}I04#oJkn8=lQDsMOR&1x+QIvC8 zGQEYdnR=FNwrF7{0h?u`RcF}=QK=QDdLdsd%hc(-uV&=fy3--fyr5$>6b%D12K802SzZP*TQ3tyA7dSy=EwULA944m}x`{tx|JG<8{x+Qc5D=^@n zqcD4<)p=_2`2K7KO=R0oW4!>qLcg#}$+aaj<4P1{RP@EN4h7UW0dfmL3`j{E`Wf6W zax99CmjaI&-F_bYl*_^C9T;7Wr~hQu%i%(2jad|fiWjkK24M%4YOmTAxtVY*5c7O0&d34aoOnVk;;k0W65Zb_ zzS1p-m_UUjh}gFHb-l$t1Ef0NhvS@gpaqs-$0)7<*jvyH_m1D2y8MbI4zGsBqlDC> zWWaAYI1hWX$3Y&@r-Uqf?wIh(m3Ix^?R?8Z@daY)4R|^z{h=I#U}p|g8)?5#?k89o z?A%pFL%@V8FZpUm5KS5M7V_OfkRSBQ0%+}Pk)#^<$bFCy=uce|va)JrJv{}Dt9vVN zlJ;>a#04U6sm!g}7tks(^G6cw_rIyh=?~1}#ZNV<^rs*ZRB+3b!-)CZxcF=e!u!i_sqWIJZNw|F zaJK?MLPPD;E|yTQDZUi1o{y&&_+BAaK4p2AohQyf@Q|NI{J#_oiXtA|`PUCi;9%Dj zr8@fM;Q`r^A|YaBSR$Qv$Vk!duEdoKf#$}@Wx@-`h{9_}>y9?JqGq&>K0|bVRGahw zqpKu|wj|L@2^+pA&O^h|VP@5LRJF_j*Jyua9?ahu8qqbh zu6Z%qBEJ(oZRrr-+_CSV!*>VV7&)7JQR(Kd(U+qb7d>NDYSeJ)**~DD>iLmY28`e7 z<-={`;dYh`zJ8~8W`CK!>{?Dkv{10j*soE&vsG$1NgC)~yiBpLTXe#E~0JJFSx`CPnq0lpeBq`}~ zcPD=#H^X-vUxYNjLzyjLiM{qAS`Ith1#(6#VBrZINA-+4@VX_)>#mZL^9G-hj zwH;mveEfcZ{4Uj6lN*z#)xu7~Yg8m+oUrB=WRJrY zQN1Tdf(kB!{U(n!rCfK4V=~k1UM+Xnrk#H3hWWCGW?fv!sX3P=pSP15fDT^;#Rz75 zONT$|pqL}Kh2B0s!)cKk;+2-ikE(l908fG1XvGX98b2pHLRU*G zQ)=Ls9W8TaO?rWu6g0E5r8|-i9QBgR4yO+Z2?<7oy`~UH09>Es zx*c@F(QG~7KAC0x0B%@lr*+t%kIgX-Y+8=;$N;mV>B+~dyE+_F=BNOOOX{%NAWaQ^ z-e!m^Q|-g2%!I?z1KbeD1GHUnqs|ebi+z=v-p5AEVv&DOLXD0Rr zbS?E%J$aDb8;ZDccwPLjZ_oLD^P0|Xvf?DV$a}M6V9HvZOw|jU& zOw@X#VGFiO!al|q@2c$mjDZVYp%iZ{?JsVy0Ea*m-N1PDB+Umrc-VMhyPnYvYIri4 zNNij27;I)@osn`@Uc0gPHj(+V-Jq*{uZ=o@ecqyKheaz+Q6BppE+}QEa5QQYv-0I) zt3iaZ0Sn!kNcL1*;iOdrnJvyu8|hsePDuZZ7E%DmJ4InAiHWiPzDZ^LbHi%3b?gdY zR4Gr`V(eboEM8KHojL}vC`9r-Z;x;W@|k(LF0ecS?X__sF~uaEDrK20lG);PA&<4W zT&L9QFGWVt=s&aHAKxQhcfXRyfNjF1Ul|mLoK}of5aj=p|;mJfN)Wn{c$@GM4 zoTwnko})4f0;7VIpVc$$jcXp&m30_TOn%G!bsFiozH{kIAzuD$UOOB*)QBA1%a7|> zKN?kum+D6iB{6}o=LApS@V|TVJu` z_^voKU;IS-)n#2WB!8 zL%JA87Q-&tiAn|`Ve7Td-uO-Odw4{A3s>S% zFIP738%;tiE-@jx+_(fh6v83EG@cNotB zVMb;7XGWE=oB#U7XDx_ZRwg3^d(5my(LNvu@jIC1PbnzW9RLw!=F$}PO8rXJnG2~e zIx+z=r0-X7KWL_APT?D(%KfaB>x{G(yN8?U*c_m2Li@Ua^uYE|8g-LpsR`PBBa}${ zq4`*VBp|<3SIA8fL9k&7b%Zxhx<*-6@yIT#~xkp~r zeEGoAQY1MOhj)z1wXz9Jg#4m0MJYE*iu5(!qJcvNk@h|6JnCzd@_F2JUYP+Y4MzLO zn^+j!IQ%2wv?%83pse(|K)#SHvISn;-ja3cW%F;=bMH-y#OE~GCd5wmIiH#Ho8rwb zWt~)l)l7nMmFhJzY-~5%lW|SLBMh4$kniP=TJ5GAXV-)1CG0YXweOR(fU14qgG&H# ziv2aL)>E_%w}%N80f$4RHC~I<9o{)(bKd}H&q zr_ew^$XGx?WdEKGf68rj4{ux(EZ<@YmSfY^;_+GbuPe!o`LtF&_KsNS>0v_7H|oTWDW^-edDdc6W@SZCD^TqRL|55V*>L9< z*rV+INREiE!SP=Z!mr zA26#$X=O94^J>BKV0#vypG&4?tD&|kQ7*KWU{|mzOL|ox&CeV{^Q`#DFZ)(HElsHd z8f}JSizY#df60ZV(%6o`Oxr0WT=Zk@XaZ!zzV~L>*OwVfBS(d&X2}$3lBfqpIxpa; zN4j9s)92?=tOu$Jx?kg)X$Sg_INMxU9#OG6_qw$$D@=`TX0Kf53Kj!LbEm_x7KjKb zDGZpZm-Jy`yo)pQ{Yd>{ksxaTMhUI4B{}Fd3D>n2(Hg@Jic_l~R0(xUH0O?uuBA(0 zaN^c6kM$82V%|@iJSUK1-kMKaGk~D+9t7Z{C+%4tdDv8+yPH(CcqoF>qN3mFvnsXPJ+hrU~;jd#ql*kSA*@ zDo_&gq1p))V>X^CG60urE>EZ;ZkeT~R1j+kaVdI&Geds`zaIQl1%M%uR4}poRalC4 zj~lY8Ff8r4DfN=E0tLWLX&7F4I>sx@1+8R=lTmA}mvsIc0%0V*i;-%mevf+&TQ#M& z!dD24YC)N#AWxQlpQVXn_?^1GCIn~Qr(`O-m*yP_Z@@rkrOB{k+00a_yXDKtWn}?3 zLQ3Al5ER0WXj&16g}NVhmE?9$27D*5U128Hdq>Ei$p$i2?Q!{)5gVs|CCzBhC{L|H#zbgt5C#xqVvXPG-?@gck;X+nlbRqPEK7dLtSv5ziMlZH^g%#*Rn~3uk(2TKhI>Ja#3Gh>A1Ec--U%_2#4T z;^813^O&NbLeqhw5SgX;Tl>1XoZ4lUDtZ+is#5mLCW9S_-;Tx|Beh3NyvL169Su=4 zjhM~6k)1py!v(~dZ_{)$-sz$sNtX$%QK)izX;pj2>yW}i6SllsvSR3aL~%9jgEcpT zqd^>HFX2TEtI*FQD5Z79&Ij>FlUKMI6*FELW;Yv(jn~kxE?6p&klrU^FoR;lM%w9p z;nXZFj%1Rml#7u!QmvkfDsHkfayE1s&iE*WfUJWpf?G#|Sb7lP#<>RYg-zT8qVLkC|$_`58^( z71-0p-Dcs5@{I$PS=>b=mcB#%n(nG-00yqA9!~0#7IN8@izsQUT}`90DP4T{I`xj> z&e|`|?!yk#Vznm}**PuE;4R@RtvA`>OZYcnKVy735~~%+EKuUjf+&jMwrT07m~o2P zbmyJ3GJq64DGZpl$?9n0)rlAG;a7I0HwROQJX(0OW~?w;*)>i#wV|Q!2az$FyaE;X zqdoY9ntpM1?eCV`7Q9N4X71+~jW!RjH_WJ=cz0LiDYb|A(#PrAdu4k$);8k+$>AQh zN^0LkA5nYEiZYcTx^s}q#3gQ4Z{5iYQM~T&436()Y(3vg&_^B(wA(^y9AdDCgJmdc zk6O8JOnlYK234M5dZM?1x+7{Fw1d1HY{b2@SAnVXr7jP0(SB6#_1#hy3F283YCW3? zr|^Am_)bOJ^UFP(INx0;uxAXQnzf#n!wH>wk*7}-at&@iBA?d1EfmXppZR5FvnaJ*iK7TI-TvepkmQfenbBtwxY#COxM_gX z5}9OvFz=~))@RXlZ^p&7t2gqhc6AM+^FEU2m{dEXd*E$#CKS7PqDUGI7 zvXe{jr*k}0OA0m6X7UYY_`mgS_1hREZpfBM{ldr^pi>;>fHdt4HeJ^>7d@(mDe}VW z1Ii{XD5=90IDEr^J2YDX7EsH*6nWxz5*kw1DZQkI$yihiAUKntBe~gdg?iw_B@6ut z)^X!LU%=^#K`axyy}VJn;HiZ#R#^1xyIu!0Z(mKsbj54tSNsEG;8|-?Bo8@}S1a97+nA%J+MT=~pVb-#?6M9}5@-lXD$+cGZqKAL_k7d}xL5hntWz zngo@_N<*a=*q9Y%cV&l$I|?q-23}Jc33eI7W5H){M~}Whpecv3 zJwf}zEU508HM0Jgla(wpQ&GA_r8ZqkL5tw7EsT(bIjhRADD3+AWYR5N1w$jF;=JPX zbmx2w^|XLgXNGopcvT^vqvv!kC;S4QPkTS}+!Jcf06@QZ53E<}lLB(j>=}8^v-g$a z>=yQWgQWRD{M%4pde*Mva|QXSAL#ylwfN6*{y(b5;F!7yz*kg}ODDC0I)yi=CWrKM zZ7`%kVtu4qKM8yTXXXZD3g66Nf^9K6o?>JiI~N9A`onj(F5o8sqhKiGq)t(*eOhV6 zX@k3sluDa4j>1}gB0NrlgKx~01ZLI~V9fq2!it2WTv*md>&b8!2EaJEQFyOOA^iP* z{5g(M+{I#r!-a6MX29)6RNo8f^wAoYWL)JHqolgbX$3rh(KlI}x7!O00EwTsy)MFV zMp=pQk_W$v{sH<|FR-^%zy8sa)cI6Utp5Wq{9|6FVsC8V{9g^qB*it`PdUYx5t7f0%|d-49n~514Kfm}kC?ebUhH?}LY> zLC`d3sr((&O%3zTlt|`}?UN?eHC?-K9&KITb)pa7GV)6TP`jChG0Ew`9ZpWX`0bLf zfBeF*@*{5%hiL(l8Y!e{V({7;JA5}$49i1R2<_H-0M_D6%4|a$qyIwCW~1wS9#^8p z)O}?xwZT)4)X|4b^~dmlkdFNC?GwDS`C)y;pOs;QRu-79)s0(BjOkZ|u`DWzc_ewm z)OA;323~PoxV--3+gCTW%Fme~{h}=x<;3?C`?rXp6vHX&l-DxZk%m96>ru(@=PI}x z8=G9$D&fDzyC(BS$-UBUj9CrIvYytkig zO;{ix=Kqi<|D0=TsiCN$z9Y0?8Fs>>l{Z%?D1Zv@7O84dT2fJCh9QR&Zpy|3LnKIN zrsdulegxFK!|GI^X{x<0hrSnASNocowvwe962v!5^B;P9U0$AbO~2hw+xY=K;R=Y% z1ewCqDKzHGZ1=`%Cy(aSGLX)JIWByUE7)X`2qbisuHt@Qt*Bsh(zSjHk6lJ@z zwpS&{Mq?*hJ9?i1?yNm)gxpox*Gk|`)&c`-FFLn+P1@qDS;fHuMA1(271y;tKMx}R;RMn zMx3)zOU{0Ij_?km4UrFSWNL%y8_SQtZigj$w^(`>L%hS$oUw8!r}z92hWyqbVE@ex zGmHd41L)xqIA7%B!g{%6Vog5gz!#-l#rvUO6gCXf{;*9{SNxjnPNWqDBmv`N8#K`j zMpoEXPomos)oI7sREf^c7MN(9Ew4%jH^;40mMUDDKV-iRV=C>GgDeMTh+CwF86sQM z*U_=MBG;2JeyQnsz(dZ{E!u1T-fYcHPqG+CwEg_vz+G&SjV5oxwSEx7xWP67bLS^s zaKew#2lq=^s&l$Ng2R%?CG_-Cp}Ip{1iKud0a;z>+^%F(l|1%jo+c=LK}sd zU4M>dEYY$ZR~j|yDF_M~9n>F3p%$H{pMh_|W-g1UG}ykin-bL#%tU^{X6g=?d6PFE zMB4um`qjUI9QQC*u8HBc?Ou#OQ=7xb13%>D>oT+5y#nb0xFicMnr)#QT7JtVhPfT5 z)e!N=qXU?1K6uH@6vpUoCChR&885Im)mH{ZRioUrty;BRKP>jcu~mEXDjAK{*P*St zqa1}*(%;-2gOY0@N$dim!Mc6(S+xj@hC0NHyoU&&douK6NCBRM zr_|$@(hDvPZtVOtX?H&&ZDd8F@BS0rFb1yKTSedBP7z|iI46vKrrJXgARylVSqODD zuyuAK6*h1-P;$0&G?6m#_~#*!KRcKi#S&1*kaTqZAmBm5JKwnogTMsq<3*b6jc9a5 z_Kj_JRaOjZXuJYPrgwt|{0Vwv1*71@ly|?kPMl@h@|HZ`-f#nNPGnted)U!Cojz`+YKyXAEW}#lW2guJ>Csnv&ErQrAAF)8 zC}^TWnmEpy0*$tca~l@9jB^ur6*OGnegw1G$KG$7Cu!0pj8##kO4gK%`Oxq1<)$um z$d}PR7$a)e$c~hI+2p9`Sz9wJ!^p;b6Vk6_=*{LRx}@$()}3@cp*GMbB=D#9`?WTP zR33}Sx!EDy?u7bK$hk!G7Qw4_U=4*{>fhx~tk8V;FDLFwuAc{y7uZ^gc`MR4w8J>QGv^zr{phJ{&vK08KjMgqsey~N^PlvHR??DN_=@^&+UBegNeESmJP1oo zEBWmX+EB3rR9UK{*#sp!eZ^$JutmnTga7qtO95Zno00#uAj*uI@QaXjLfZ2b*H+tP zT24>559AfxG!3P}*Z$lXA~=>Pf(S%_xUi?bdzH}26jlhwF6)#YBOEO)yO{h?SV%++ z%?uX$s>bk_R}Cp_yjdTH+tjeEN;HbO+WJO{dEOC`%}XS&SV%awLqDujlGX?&_&x6G zGWm*>FVFA(%KO@d9(LXITM@qTN)r;}PPn$gaR+U{ZvA17@@Ud0t<~O^@E40VZc@SN zi%bQ^EErtF>~IaH{V}xj4!Y@!r<9Futt~jBJk5K{%2-~5jGpnrN~AtqLAx1m1B{2a z938xrnYOE1anp3uHDbVasohwf{QW_(eXpINAPc<)Ym<RCB+YHSfq}iBVV|jc{633Vp$BbN8YRC(K^XtIQIbSx$?M#t zi$xIo0l7f@syiTz<0jQ0$*bo1j?eGWKXi8C5pz+@?@mxjKI;%}-0T@8Zn7xaB&0~t zH*I|GiK@w6^}C$Ad{SNAP_5%wDH@ID5{_v9kDM>`^5KcAlqPr$8xCI-9nJTc^MdXu zD~ykx@!NhM?K#^$Ee@%KI8hh^3`Wocr^?&u#OfSlJ62(w<#O{A3zQfS*$NpJq? zdaFEFN>XIPm@`-(CY(8Hsl01~qEHHx!@c(FYuwrVte;Jy>rePwkGK)F^5mz;ms(95 zw)n+*dlrS{`o`Kx#wRrA1f~A0?C><>(k}P}`EDciQ+@)gQkKr_qPo7rV`WUHX?|EN zWIovv15!9EU+M|gQ8GDWUu&H(VQyY@IIZk^2jl0y0q3k->klfg5WgmJb zWVG`n3LjU}N;M><8`xnjyH4sd+Gk1l$7fBo!p#d`hEi*KnK;O_PTQ+P z)j&zcbQJ8o>e>sr#X`_H7u<5C?6&Luls-K9ST!Kd6yZhF=q<6_O(qsK$_|G4Sg9P7 zGnleB{P@6{J7#@zj+-0^tv&J!epJLG7jpVRO3Xr%eMr1?m6-QhBMXN^!_XNZ%7Idv z7YNVVYab~OE(2(ha*w={g>HemOuoY3=^$IrbCCyx-l;0M<(&AeGL)1mBMNOS+@f8W}%VePz&M_Ieg;+MTMf=xW8O!?ZE2=kI zey+Vhf?mV}cg0frlfXUa_O-3-kBhdm72ntQ0~#Rf@7zE-Gg$%ItapM0{&7Z+6DruY z_N9qd!xMX8FRpLBhaF(;DQe|ZKcKAL4U`;C4Wg-JIJgbg#XMLlpW%yVUWn!aguiTi z_ll7B2mnXMv01}lbIHjiA}9McYqd~OXui+&?m=eOD41twRqfGAdsokf&j zA@!^!sxZw7bQmgKqIw^#oTHH`g(p zhUl5pY;;{wVsZw6u@!EAiCOE^gG!@PSKWXW;a*Jtl?o8*h+k0JLUHR6@pf6gf@0RC z#hQieY*~Emm*xkGvVP}B8qnyBCJb;0Z$fzYk$y{VstNEd(05{Izo{ZU>Wzrxex4Oc z7lkOPzpTBX|mxH?KX>=4!_4UI2tnSm*W zFizQr(utFBeWb05$QcQBXKsME0f+JSJxMl$4SmJ7sxMv@p07p0g+yRyd=TGv-$>04 z{r!+lzXDNx$6~)ObL+ugBuzpWqPPYUII=MWXP8#DPYPuG;{;X-rLzya@G1)wt{?8M zA1D{A40dir2!!H6!GHJ%1z5IlHo7~6z!G@3KQF_T%)W@f9nlt&JM=UD`p4+_2u;s* zd9L4gTG-5Qx+v~=&vm^xPZc!JX8j&0YN(kKb1fA){@#{S*}Kb2x^Ga^_R@M#+g#R7 z=WFK(9?e_&@n{l*|nrO^p9IZf-*Q&B@8d z(ZJT|&zM`JFfI%7?Mo$uu?NiVtHXXSzBb}It8jq1I25U1Yx>FtGy;9Y=-IZP2lMqd z0SgwB;h@v>pNMdaeZ5}t88vs5yyA{-q+DB?6o4U$6 z;m4)`CeH{`x-YAf$QO^fENCa1s@(^T{WAIXEPdj&xR|OL_G(1Cpt!yn7@E^6&TO1K z9|`q~2eGX2ip5f%BR#ii=%&q-%NguN9->46=ONS=a_dIPV(Qc?=K&i-YS<|!V2xzmekMrpYw2;~LryD^sT>;Y=bOxftc1@{y z1QFcvZyvd9rzrqIvRSq^Zae%OT8M6n=7aTTbNnMLS+)2$Oxfh+v^kjCi7qP4|ZE6P*2tpflth+$|iky)J-)`Y2az~%Y ziT`hr`+RF^VfG2$ANS1uL{Cxb4?+0OF0uDz4sr)=6Y+lLLEh;IJ_syaOG6S*AvE(x zeiL7z)|rk)*YU@nYOtK!^&30~SW4RO?q^d|Ggm)d?+@T@AchX}+~d5T{n)D@T4p!e zFKr93e({+KCt#~~iK-V>U!Ba{e&vx|@v{Xpuq6D@6MqfPH1se)PD&s!hQ-F#;V#o@ z!^@GB(l{$DurU>=7@U(Vi9%n75*6$7<_t&ZFYw?*K8mgJ8lG~TTmm`mv6?jQE<{*N zBet!}S^No*-Zs@SXXWz65XG}*Qh;Sv5+C$|aCstqx?&0|xg|^wrbsyA3@gT4|K(vt zckE;mO)q}iS(P^`uJ2UIDl$$?#FHc;`x9J-`Zlex9MCh8%%f7&YyS4Zv*XZFr?Hxd zpt5lp+xgp)2c%2hs=i-4nJfuE8w&SL$_#ND%=Mi;!L2>s>z;b)afrtKE%G}#M5{l& z`c$bu{$4a%=jhWS%*Ab)!f&1jx76zu*as`GEyC`__zvqewrA+a<%epp*6E`1r@&SI z0g?wFe!d|k-ow=45)4f3zcFEM%-qE8lL>i$q`?0lO!%YE`cDqX$N|27@saXZ1^e~} zxQw;`ins{M0Ud6^$YilC5Ae}qmtSv|=(y_PO>@73d@6{V&quNoWnj5AJ?u(b*=Tu8 zQ3JBkPW%yAOrj?WEE2LB#n4LS!N~-9A?+TKw|p(6=YrL^cpx6yH*=leyT3PrX4<4LfDRYy546DO4qi2$0F$#Im z^E}}+G5?)BTH6hfsj=r`vdx{p1;xfz7lKw6rwnD@iAK{V>ud`+MLn296!OhrCif8S zw!epaNK88WL1!uvu2X+t0{kFO&9^mu28=MeM8yq&o;s2)Il_(p+c*h4mwjE~6Hcv9 zI0gQzXD3+i{6uZy*$PuI8GoFQ#3?81Ui}ZWTUixZI;Ap--n~g zE&%bWAgV>B-~ukP?T~+p?{RZt?RA{yJARB6Wv+RiaX}P}kku$b8;S#)+fbikP)MF5 zSrY3K++f0dSul0KNvTojv@>C5D#fR=&mZRg5od2}=Cos(gk3GYdXK@2#Ij{i(ykGl zw-ddQF>24-P5>u{64Lz0GRpx?bsv6L#dt6)sXVJ+OM>S}(u6H8B9)}gN77VGk%U>b z`;`<0Uzm8FM3?m-d}6o8c$C(7Z02!MH#52X*)u6~T$KhIM(vRia0mjsJV~XJ@^=|sqE}0R%ot}-RIT^JCtOPD z#xx#tVv&`LNL zT))ZKp`~!J*qOaOjCy&w`GD%<;2=n*Hpo{S;-mS$v}HL__C;O4J!TXTFH+9Pi0uS@x zyX$;x*Pg<%a{HA$i{7erL(%dVf?~;y3=xMOmE@5sc@ye`>k3S-l{TctPrXjWym)nG zFSXPS?1m=qFFVG5C4j)PXuNlL zYk7wZUIJ4Vgj3k}lB<#_YY1e`vMo-VItSk0Vs%YaCiYMcVXQk~f$zPQ(1eWx=JqwI z(v>P5tKONm$#l^UYa(AD?67bq7OxNpI8B(W@}6=23ih7HXb!_C&_iJVq0snmVE+lU z@;}-Z^M!RN$eNnwO7sWlgmn5T$S`5$is{9yiu>X&*aIk_!*+?3U zfvT~}V)l48@pQH-;OqN}kQkD0?FN;0 zqABMuS-R3Ek0Gls;X?_KV|LZxhofCe@SHy$7u^=CyV@%?L=dLLV?nPY)n@Lvx4jiQ z%$nsTrEMH8n&8|=wSy|Ut>W7uQxpl851ZTQGt&z>_8eZe>*C|Cbko8U8ss%d%9s_M zH1F?~;4NG6|Cl|e$P3%(rPx0$9>s|gRT?BVaHzyk7@*$zwYjjUJAc&NeyKu>lst?* zdRfpGb7#cCZ-R&rRnpz;HJCh&&%x^| zA_{|#T>ha&!?)(Z25uFkHh-tVC4N4BRk{sr;8l(r?G%{QAnq@_oJOtY<1KXKX9%7V zv|-_u(V1QZtKA0+I4!#=rtub2?1eymFZRbB8z}(d$2Dc4WbWbZJ51F}+lHN}k-f5- zNTF~~%Q;1PgAijm)(gBwpFWBj;OuxuyE7rxz-x+=&E92?9ozz0@7tJt>6&#XKpT=# zrFQ==rd>QX%e`kbR1$@~2eHR8bQr}pM3?yYYoWwwW7V*qG~@vLx7R}dLc>2>B1-?D z-Ws_>y3~BKR_hi_A@CV1b?5gV`2*4U5MmKkP}%u1orShOac43}J$*S>K?9%!y<$W| zwnAAgcP3NWX)7!Z4IO^(?=SG%Kr-M~a7`B57V8M)g17GO)P;M~tPH7~)icgsmd_Rn3CYy9%C*?5aCXnx{&&tH;+&DWXCO|Wvm#j zU0g!4-VfN7LX=POggg|C1~%G83MS;krQBNgi{S+MQR-GHq&Oxvdog4045B=&nT#_m zg=`qDbwi>T#rR^a_6rKiMrV}`hOrO&4dghue@|m@abPifA}hQEUMyR>jLsrhJ1W1# zFhsR+Amt5GREKFH7O&%7&xAh*vyjAQh2?GPxkNax2VhO~mvK5H|12uSmJ7{Wxb;D5 z*1;DcR=i5x(O3DcBQ#R;&E1oF@0(I{s5^XZ< zDV#lf2rF|QAQ!v3$UQmBknZBkg%e_^eu9Q_&^bu_6qQI9NZq8~BBjVfMGp$UA^%D% zd75S$2q+*RJ!BvthJR11KiaS&w$2vL|Lnx7y}F?cqrDF$chz@dk}!t%7X*Cyp%4Opb#AU$;zfFk&8BvP)56bc>R4bw zZ*I;`wBC4Bkdtp%jK|6BSI498Bh#MW@5ld-vTqE|tYNmz#I|ibv2EMNL{Bgi+nMNz zZQIVob|$uM+nD6O=iYNp)%pJ1?^NyDwflef>ebz=dk^$L@cfGPEfZ^kj*Y=zfafsc zu!vUauiMX$|0BtM2ERbuY%_j|R)*ZH@Z$#lg24t~fpVfX{zP$zU3T8>{d+_Pd(GjB zkX2cYxS$KtxeJGJmx%m=EGQ}sgH>FX!gbVz5u7qRESW|8Z+)xhhC{U9deg=@IOh?= zZW+MfG;U-5asc{;(`KO_P7z#jtTpN=G+b3A-%}FSbP=L;YVRAH zxBJNo-Sd(@tNLvp6v<(Sd0Y?w5g#uXTb3d;iJr=(WDtrs+FYIT$Z`GmopNd2`QyO}O!m>nLP9t~y&wo) zbj_6LHo6*j`9~N^7#O^xoio={O$0tyA2ellLrXS=yeqc%{$g1rAh7fD$+QDffI-tw zwzmf`m1uOa;Gkh_59K~*@nb0><5Y#L$Cn2rgf!y%urG1waz4G(H{5!vw6YY5=d5x# zm_!A)iODdK0J4O{hfh(fro+lvxMk=E=gS|iE=qTcK8UI~OB6yh*74#c-+{}Sxbx5( zDQlcM=hHPOi7_^~%0NBwp{@4J-aG^^+KuWxB5$}C6d%wH*rCXfAcs2#oK{=qlGLXKat0fb|33Vgf*PH6+9KjR!UupibwGkQ=(qt`l>*+CmwLAku@<4=r}2-fCP6L-GXtfTAS1D8AqgTN~)Sd z{I4tH)#^(7pstscnB`k&aYFWc!OEy3>1I~uEp#3gncr!;GL3tb^h*5cR@^MryZjUJ zfqOpnurK`-5IDn9)Kc$ntC?B^(swD5z&RQ7lYUTl{4Pci1V&-keh|d~PR^xe7bi0D zH3=~Zp|H0}*|Ub0#<>{u zX6~z=AqCXea(;@eQPs~jnzre2z^EZ+N8nExIZ!suTf}$r?Bd!2PMFpJm2IY6(0=PnYS|(Cf3-gg4LEXM`vJ$K5t^%-_31#aEQ1 z59DM1$Rqq7A8qFkaS@qI;{P~ZMY<*=`WL5t8tI`OQ_pj7)qsn3zc;GmTtaS#0L*73!qDy|P);cp8vdW;Jv zwrje@QgeGhK9cq)c}Le9eLOFbm$!C3AURRW1`G3z23V1 z3QsH}@^a^C(Sr*)bO2Ay&2aUp3R^YrvGJ6n zNPd~)+0gAwhTYx4*hdQbt&`hKp>r*gE(|*e_dG5oliSG{c7`JET62kskWL9ygsP=S zvmL@k%x32R0Jn_bJ){*c4E@NVJbmEl4d z@2EIII6nHoO>FV#WWsqcQ>E?E!*2fQr;>waX5WU*g6dm4}T#fr8 zQ~sxov#s>+&~QHP#)RM8ve@qa9ZA0dPob1HdbxjpwCNEKQGXicAJ*0l);K-lH_u$# z`p{er6Zo)okDhh+RdkP{w}x1*gi5W%PqC^adf8$Okwt3jwsF67g*;=Hs>e-F>V1S{ zd3neUeCMqodrI4Di@;Z^MAlNgP+Ei&kk|}H2~!onaIL8N5ze&l_#L=oJ7FfOIf=wj zQPaA?-(cl_XXPG7U5G6)S!%TqhxXNnjKs+BrF;wNPvXG;-iO8{YBi{jx7ahP#29%j zjtrTiq3ZJ>{4axE|0%Tnf2w0^h<}SxNZOe@*jwAV{FffI_&>!d1Raj(24^K9pu1VI zO|CW$u{|x3WI1xlgTfyLPAA`NFEoFbcMSTW( zW}jwPzTZD?K>(Xpp(leG54be_<_Y>MGU7^nmR~j^z>8C^VfmM3#%!Mi}!Bkz5LC;Qe12A2syH#nq@9YQ#J=64X6GoC2Yvsbk zYq0sI&*reKgz^=Ir>v=kUoj9znD>UFNsieP-JAr|%G zGkghd=e%;?a%rHR(uL2UoyvqFyWzUkmZJr&tI1J@qU9=0-c@~wbJ!V6T5Oy1qi)Az!LcPndru8sb$rLjUnEtxW7~ z9w)|s?7n|!sObN~U;dxrX6_SN!lpt)pLOlKyw^S>G`(JFjQf)QI=f#62o${90#+4BG+-MNZj4m6$^ zG=ZEjVhIP)VM#3Z7lXWK!G7?$W;+s-rll@LdfFTMRteE0daXWrxV8z`cQw+P8Ux!O zxvHqeW41QbcFu~J6Nwr%CnnGbi2l{&5vov;gEqFl@_p#GWt3I&WjcdpQT9;Ku?0Gd z70T}}(LPR@u=gp(Zg}o48Pp8EZB=Av|A<-4 zl1UCPs?7XWNOge8XTnT3!sf9~d{m)9>IR z`z|5v6S{>|hfCg<>Zk2lb-_llrQN-~b0uXdoe)TF{os-hn46IP9#M7YDX>depE_L`p+whg!qnhf)^RxS1S+8*{quLw{Btnt+kH-!lM7g9gR(A3{A|hO`lEM_9iMQYcK?{;3C_G zz{em+fQp`8M)N`vxFiwxD*xr&q}|3wQhE2wPSpO(9s+acKOYk(K_o#?uF#e~8egbi zu+oC=iE4Tg<{$+p=c}&ceRsd(jr(_DVPPT#&0%*6Go14Ak3cOZFdm}wF8L_RpOT%CWokU z{>37b-7ND0uA>d7HBk;5R35F?C~$IDHea`C?vA}SW#q-l2ck|CN3K&~vkh4~7=)SO zpc8iSxtXC1?0miakXg5e>QXSx&L6Lt<{-3OBB@+Dz#quT-eGjQ0OI<=|52T;mY+|4 zT!@O}7W4Y)XY=hC)7|T z)f9g(%bW|?E-k{9Mk z61w2nYEN)f0C%S|fzo9%Mnf*x3fo%mOC}Ob>kFl}!f4>jbX>0`!wsDk)*XoL;#b9& zZ5X0Pn*?;tdf{ZRgG}lyf&JNjC8=Ym+J}7G;FtoDd)O+5RMuKrWj%oxyhqIn_0d;X zMY0XLl^&x%U$9v1=-b@{;mJ!C=x$|~u|CVpmd*pqE;hg6k7$BiI#9Au1?~nN!`c}n zFwy{JZ&8#@k$Z^QS;hN3R<*OExe(92e&2fBP>5dt?D!7e&*s0Mv>^$(z|2Z~7fL~D z9!*g*-R46!KW&K-mm^C)7T&R9Ymdp_;D!~7thRcnbIhZ{Nr)dDH&2e5P2kC=tSK_l ztJ&z2Ip@30(A!$^^L2=PY(5V#53c(N`*_BriE?iA6S+sg6^GnE>4$nS1%GFeEyb~B zP&C1u*BNFYH0Xa%8vtzehtlp+OzfK(4AZnmOgkXusx!4X5VhQLb@%eWpkMFNcZSxQ z?CB>t;$VG1BA}4o5O{t-aJ@o%BD4;9_Wu&WFU7ADX%|tWjiZ)<$LRhJw<_7cCf{A6 zQUo9>ft`0Dps>$11yL$l1z^m47E_u4NkF#05<pj<(&j&xG+;4L6qFjJQOE-f=i4YKTdMMsI(*kU%WGgbE3g z-P?oJ*T1xBy%59;oBy$e;laRI{tW>659OMSiJiI4f6WLq^|c8!F+Y{i(^wFp`iGiL zWdlr^8dc*{up_`rsBrVRlHixdDR~v4so5PZ<~02xn7f~bLff_4td{3m+Eax4w)YyY z*c0R8B!-xmKYLV9bR^R>UuR z@|5C(PJ@37*l4&If*>U+uS{?wivB_6m}0yT-uRkS6E4dX^dDOjcM`HqPp#tH=v3^L zrvmX4I4>2p_!!S^hFnOQ%B`!= z_)E6&nACKW2BIvjo(wzo2dgK(I)>Vu-fCmxgP>eLEk+T4Yjl!Q=1>5Ca!xE8Cpi!78DTQuY`t~G(ZbUzlJ%O6%pvgh2f zD}#g=utCP5sbiUqP-P={(vz2}o_11j-*eVHIFSz%Ii;+Mz}*Eb(~G>f4%ENJr*yLx z=_T1u{XhZA=WOV>zt0J{cn{fmsn4!+V7P=U-^;aDZcKHep@Ik|gWN5Y{bfax*e!;} zZuv97SXUOhe)h*-`Hr!)HdR$NPujv!NB4f|zM^*nafO3mqQxdd(`%dF?uP`G^IqdXFk zH)3b<4ZmKK)?30V5bdZj5aU0S?2UaZpj0B-TVoaC>3t;GyvTHkA(J_VlB51F z?UnIiR9n`}Ii>KCJOdj_Jo)-rHA@Fjnn{F1Hf44=ZIh1x+dKu78X8Afsz43jAT0n4 zqoEf*&xB z{(8{PboGNZNz07r+%4Q5Gf|MQO1dL!l?z)xpmS3MyJVG^Wpu-X4HzW=z}{vdN6f%A z%L4B?BoicJ+jrhEDA4Utv=Nq0#NUFQ5_8BmL@rR>=Wey*_Cs}iK`}F4MDZ6HHtMtd zNU+cquzU{49i^xq8TjCmq$d=Rkynt$(;@kV2U}nyh)E`BXU>$Xu%G{;Dq+kdDpIU=-o6s&hH!u{G!N#U+q>dKh zAI~-3?C^&2Ysim@FPww4)StUBH{y7;n<5}{%#6nCuIDYm=Z($v_rJ%-%wSXuR4l*J z0$U?Q5z+2I^K+5|xr8-k~qZtC{DuJZ|RUWIha!f$g=3L{ff7@!ZL`El&kO7z9* ztU(rZ_ymAd9{OCzp2$gx@qP7M4Tq1EA(!p^ce24o1IBRGC_0(SKgL;klF62jwAO0a69&A_d$-31v}qp`XeAn$>Si-YM@|J0fd-p6_=_?@LX~#kQ{|phN(% zNkkm0!6=Pjr@Y}7qTMNhsWAAd!9U(J?%UVIgJq$pqh*Ne9jwdvRz0~0d&^nc$1nmY zVdP$~Zg<%ZZy(CIjWC0)q&8q*aZkS8*(RNkh>6c;#=zJ9u}SqVBIDKPb{@pI`Rmi4 z3@(UGgj&InFgvXaBG8iq*!DZ8tnK{SI`Q zB>f?(3O?KAxZ26O0m1AH1QFhK*Y!HW*e>p(o_!A3GlER|b_=-aL}F^s2IQa{+$1HK z`D7TosBJU>lS}m!trgQ>%jVrTL`h%Y4&&S*(vGfC*TL>*UifG0#gIKolb{Ej_f%C+ zOl|9#M5YgV0%GC-6NBA*YiM@jl$DmR69_`eXEkuTI~ zcBDN#aF5uD?%-a!0wapmcgittFe9?yLeyY<0)S1GbbEO6#c@aE-a;tFIR6l@tP%fT zgoAl3@WTH(lV!wyP{#Pj(X}A|myXW%UyC-j%JV9iepg&4e1ND?<3Q#zsE#|etPCqs zpLC2Uap8%mY4`O>xB3pMOQP9aU|`xyAIVNETMo8aiM&B(OV1mon+#6eZ+wghRG9$X>a#APzh#S|Tm zWJ!Lboc@Pl0qqRnY6nl0aOS;26i6n-xg_PevG(WrJ8FIf6K<;?eKJ( z>Okv=ZuqDJV&6}WblMT+m73CB{u~EeoyPr=$muggc&<}o{dJ zO+b<1mrN1(#p!;IkoJxR(${lf?qjO=2uY^j;QgyGf+tXPEx;R;Y|?;L)c|kP!1#Tj zK0uLlc8`UFPe9HAxgy-9UXo3k+lqkLnyLZl@OPt^_($2lw#I5$oAXEiaj=4akW}`6 z<6!^8`o9vC|9OERS<_JYKdyqdFdCL4Fkv~Y27LNwnlf6T|kx1ro$rb zJou#ZP-hF=1fUKlr!!IW;KX3*@<2X}xmB@9oJ)2P$Uc}?s4U(yL8T;9g=ZfXXj}Jw zzm~xwD8@sf2_3iKpfo}{g~29z3MXirPHnR1Pgd^az+;VQjr&g#hY);AO4_d??s0eo zV$A3_K|o-Jo36XyG9gW@F3aM}0pjG~6Uglq#K^ZQgt6D{Wx(GJI`ORc34c~VYt{}$ zz&lBfXd0Nwyt7ay^G48GYI7LSC_u~ZT$6<*ZSWmq)at}|ELkr}oENM9UQI|!c#4>9 zIN0EpI`G3tfw+ExgQ=4_F$-#&qEdUnFIid8JC@hydcWY30@<}E_GEXybW0E!UQjicH9$4oQ0v9hE z6>eA5_?Mc57!%1)fFo$t3AT;vf$Vs%&wf@OgT90Y#AZHdCp#(MsL=0(b~-9TZG9x` zUBxyH$UeW-8+-~jZZJ*R-7*9dd?X3wG5@*IX{f_#_;Z_SPScQT?$O8I#+9`WYK|VH zq5POdROA9#}OX(}gfz*}mj zKQr=8{7Ro!_djWc+k@(>eXsmZrM6#9{{~?G)xbCNR{1U-W4D(wr|9f??Evr$Db*0u z`l_jIqZGzv+c5CMaq(GvE-QRrP@T9Xj>F#^@NIHO!E6Zg^rJ}zf}y;&<)Oa(3-chI zMSv*61OqGK`L{$e{~aO!FDR<@;e0d}A3v4Yc98}U`oPAhqhv_Ebu^>$V7~>*hC$9q zNYbRk;K^9*F)*y@MgFW_tZ?nC1_0Iyppqd6i?Ps_o0(SHmzjR(H?2)pG=BEHZ)I|t zSoB|i_dn=z-|jm0u@rvpKE40)et-j;KO{in4cu!Mu}mHzu;Q%X6YS%ZS~>o%xrk@= z=!&b!xurS0@k1BWnhw#rli%2N6<&i7r`2a}x0O17+Lh0t!cXu_AJ@S06I-(|R z{;*Pso7Eh0`E4yqWl`+=%R(61Zsl3RAyX@hOi<0uAp*7efUHW6U{S)M5+>|jViAE% zHDO61syI|l`w+sEonEKS=mx&1$>xt~J!Q?%(1nbC(Gb-EhrYO%o4ut)DgBiY6CSYu zVzNJsOu99+F>|c=W{`z{r92EtTP?!T&I*X4LWp}>24I(@`-k7mv9TE&aOr6$mpCv= zS{*1iB3@MkBOWSb40LS!1oTEAG2uX{JsZ;gAA|Q0>zH&621!ii|!2B~?e|erMTW|d`m#*EZ7#8)< zv;s3g30w(q|Id>eeACjgrWK58f*$+L8e(7lX+24lLQ^1{_y{YHmV=G~_Y+6o0>kOr zc@j#8N=*8gNldAgHBm|JB0!FaM|6aNg$NTMlcO}i&pk6Mdd&&Jrmw4xJ*P$kcVuz- zDyuBZbkZNrqbtj!J+l!T_s8ofn68AFx4eM-58hdacnGTCQO}^ zVRkmSIIzO60sSPr>@;Yo%kx1QENH6Jz(IlQCw{c?ADjZqkW14)AHNYx>8F2SmD_DIVPZlHFeM-n7rQCU#>;0h3OKq#nsoY(4~ zyb2#P&SqU49O$%wrBfa@bFge7O)1)}V1fqYEu(6Q*H>!BspEMsc2;a`s@11+DN$<7 zQmU1qPLI^>NKWdokmi1zu(vQanw+Q^i8Y6&RUJWm!_2Bhw@iVHW%tvy3Zf(78zM}z zeAV9RK393moP!bcd~Ibm0N&k|=x1lMCQaV()=J>e~>}d(PrOL59 zN4r7TkkyZh8Gw0Dr|O7gYZ{+5w*bS8<#Va-m>d#y10Q?Jj@he5IA0T+qrxEbXq?oAB|r zmx_M_>PYFaN#Pic@RB3UmHAB*H2AKJ_T?D#zzx2UGsVnazr{xNE-}%wX=DgxwKg-t zJD-@%gHL|O;{=sZP*>q=;LmyQ$_x!#*3R!E%9t%C-7^gmjG4|FY4(e^V8lha)oN#V zwGY)1kb=8b7|G>yjk>xqIAl8&UdpMu)h(whH76Csq!p#}$BUU7SaqSU0tLUYUc}_l z6^obLDw_m19L@VWin;F%Jv0XFh+=mg$HV8;>fpOQE6=F0j5ab45?|Ut-%mW?z2_1e zqf@RCI3@V_uwj1kb@UUg;Nz6WF<3jAGPjUZe}Crt-nw~t-ySJebcy2a<2P{r1hc`B zR=RPie~40MRijaGIe%Mq7MNs|`s(YVFW*7$3;nD#y$^-NeC7eyqmXNE%300S`<9A> z?4|KjE7ef#l+#J-I=ixo^Q9>!H97fF#hY|)w^}nHjoMFhQ=d zUwWAzMVhU5<9dW_3(?r>*+Mus@Cxlynl?TvnnfU zeqt1x_>_(81$Si7eN=Le3=F5r=D69MHD$O{lbq!B`HSGE2-Ny0Cwh^Ns!%#IOv0${ znoyWL`1tA>X zlU=Yay66`p6MV8pMcsR9jDKa?{E}y&U;o@q?gUZw7&oVdh0892PjSZV-?OV~pM-0QuA``}Sm!3~h^Ygb%?bdSzw%JBdevIy(kEvOc^2WiNw2=Ua^fLx`RhV?T zrOWfy{O8r*8pYG0qXjF#5dLEVpsoRR+AL~pNlI|UBCu1TkLm7z?Vu=ii_tuR*hBR^IB> zIt}R=Vvo|fREua&G+~>&Raa$AQa#S)jha67%%y$uY?}iaf9vn=`ex@g@c`k(JGLxy zc-s<-9N#}ix#--zg92)poJ|EbC@k&?82goU0X%fS>kHGn?fLnEyz{o$iUGRzZ#Gcf zsIEFRlSh3f!;Qces^_TF zvOvnA}0V4PK-qmmhqh?*`UK-r}jrV%$Vk4JJM*JHy>__D9*qU#dln3ZwFA zf_N}Oy49ojhI~cpDqFcjvqknzpNaF!gTae#C_*aWbH$@=?T}PSoaeM18zuN4Y2cR* zz?!1HO0&a?rk?rPMdC#397>upmr?7j;1VO+D~IWrzOdq2{YMW8&A4r@kA5n`0sm4d zxpyL~FzUzGnPfr>BgAcrk9KObQ#@&@39G!z)6ZS-PBZVaI=dskw~ z2BqV%2X8N-WQjXuzPM`intBdn(HG_gnRJ|EM#gbA37MR2k|PXlO(eO$YzhL`#6OSb z_0NYQBQ3+1Lc#0D!wz7+1a$woTyJf z3-JeJZTN?#;wIKr|6l^g3T~uuOTmKeUR^)zPEpx=rw?+Ed z%=jMfd)7#{qg@)9^}QD?tv1G`i-@4gKAkDqWN}1TQ|ms+rigox-7Lq%aKut6Zo|G< zuVWU2GO`X$f$crb?LLh1(8e=-F`QkZdf82?ZYpL~g)*9JSb4kv;{qrv-5&T2d>S7R>*|QHiR|9oQ`w`w zN=^ULESbLX4(~?(`i-fX*W?xGA^@srOw0ZK=DU!~SsB=Tpe3HTd{TZ}F25cW;F_{) zurxa3lbgPjFsELuM~ds@M2dSCL%`&zzBPAXYq%Bfj#uiqCiJz| zi(gprPr5jJbO^C5E{z6)mhczSUmi?NN9%EP5vvL(W`f&Aj1?&dh?%VO??%MEt?Lzf znv0(GHG?yJObWeVpKuOF-z)atyO=hQ=1CUkb6r-gg?!6$gyxDzeBL6x-YS_oCQ6kT z=1bcJEQEYZa)g!-;aoY2mf!wLIoO5LV6rX1z4iG&iNpLxDL$F2pBGb;5m+=aBM`*P z7fg5}eU;T$QS8RX1OiDFKgR{q{Q&F*oOpITJK%ZwDt}K<$=z@0V1iyb@XaRPQEu|v zE2N^9#U2L`@AAKaT4CVePf#aGM3%h_lXj5!KyilG<&OWO)ea;u5uQo4 zuq}jW-Si397#T`otNn3p4}<+`8k1~QiY-BGI9HB(BNH*z?%K>+`)m%$mr+v~qdxU& zC()|~^oBBL8sC49(4+oF+l}C|kM6wl9wNNcyLcOLkKq3&;O9QXGM3SF00D;O#$Z4u z28`WqukDe@hBb{T>6ei#toHnL?a*v%j2ZX7&XhqIzt!DjR9FXwgpOGx=^bkG%A5a8 zOt{pWk~grWr=0SVKRKepCvroZ)qa68@%^tFJH9 zt`j{LLq1zs#CKl^+&j%Bhw`BdH`Ef3cRC-Jc}CMn;g>#MkbXMYazK{jlt!8$YufQD zOQf1{_ZET$I-h)aE!w7Efp^f!lk){*fp_ZZbr+pxW#PLGC;5~}(U7&F4O{|3;bY-VDedvn4`W1g6d^S7+ zxV`+X`Y8x^DBN;Aj5En`m-_fa&6B(PYM8sU82?S4dZ1R9ADN{(pxzk_g9;{*5+xA` zri3~PdD)CRA~M||GVgzXsJd!P!`q*m1pif!`(e-S2><{)sA@gS5E%sg%Mj1vKQ`Hd zJ@w%`fL6LKgtLr==bDYF?&GZ2FJTh@k~CO(9qSPIB-vi&Q9g2mPf(aZTx`IfmuDV5`|3U7%Ru)!Xd;SxpQm)a(YcQ}T+OxANhQ`Bjp}D*d-;9C;wx z4HvtdlmK9vawtSe-#(=O{S)yH&*c&&{EL`;W%&K3S*MxDfYY-fp4b}=^k?W{MZT{;MjNNAxSkdu)w02Yo z;AFQ=-ZMeR{QJ0Wt%Uv}SSUW=A9~E}FT>P{Q^;W1!^5jZsPC-K2iKejvEp--IBWI9 zy6-`x%z{xw(?^YArb07JIKx;w14>a`86@e2jE3Tn&K_8AZC-0V(#z;raEnJ8W!h{0 zWt4-sV3T_QKkuzWyzfP%Y+;-*cSv{Uit_lzF?Oh%l3t4EIbEn{BnKwM4eet$kzVq6 zs9;t$tBc5u@Tyw@O^1{$M6P`~aHG}~9``eJ45{OwgzzG+av_q3_OU~029VJSUfLNE zf9EBZ%d=$He7VMzP$k!n=thq!XsBgF`Iv*zdn`ld<81Q0|Srhvs8IP@A3IF&EYP2n9KAIvS8PenoF0_-IEccz8 z)(O?XDq4+fZ$dU_q}caAID-kY-|(HAA14?Xj`8|W=U?!a$*zl{Rx0w5E)ji}d1%+5 zdEJU_OY`hgZRP#5bPdaw#JWvf99({!^h<)u`Bt@q-{;)i*!owwolUI_J6AA(?)%2t zDyrl31PJH%GL} zHp@Cnls=w9m8CIDn<|+eWWjl>O@yZ1^APNpp+Vwn^z9At1cRKBVMI&zO){zIxs0+) zI6?GK5FDgB*Nwp^;>7TY#vr8jEjCtb23^?)O1V?&hKNZ6HQfE3{v_OAY#Rnq6V+Ef z44TsF5~7q7S{P_sP*#oEl&S>HDpjde`Jtl;-CzeUmzc~}Jc{otm#E1_tGIMJ!Z=5> z%P#@>Ek-J4op7JfBnFOUjVCF2nfPTvDlPAs#b=>FNsSs|a671nY^W4~QZWgqY0{L@ zD;uJovtz~Rv4!D7B8%csgxz@nmopp%(^`t%c?6d;1cBlp&-hXfvl7bNG__AQikck~ z-`{WWto%P?Nb64`8u&jp2OJm}`@a=KDo*xhKxb$B|Nd2dvZlWCx&-Ful4A9sD#kvC zHJvS-Br3ns4+7d^S6Iw_RCRtjM;S7M<-3?diIgZxf)~0E@Erb`nY-SHaG10J7!PC6 z_|0JKKW|;#sUfAr74{S@%jwrA{uaUQzweLSU^(Khuw+>(EO~pB{fNKY4V7;z4AIK@ z+?#_fb(%CbYj8_s4l6n9tvp%?^>J!`^6P=fE^KjYQm6+^Nm6t@&+;P3Byd6c8w(Qp zL8AQ7N?hPrrI=A=xROa^?W6hvKqobW8XNd*<9=rl(BmGFW-q)6))ZIWL2rnM8aKh0 z1sLXF$kgPL(OEV4?!@kx_LOiV)SO+7waaBQjup94k!e||gJI%x!Se07;JvH2*Y4_G=k{0ot zvqB$V-U_QYq@K_(fnu=bXd_*rP##9n$8r?bzKPz_M)rEB-+y<)a`|fjJT^1Oq$C1# z4h)SSj0)7nN{u(gUG14OspcK0O z5Ytb^*4FI&^%D^yg?TGC(#_KX%lui>c0Cl|GRMbWFQ!a&BY1w+3|E*I=_kCA zS(|QJq=a#)1mYDW06mZzL49?M{6bdd)z-9*{Y+WZh1(@NqU@!yB8#ZsA0@Pgx4%zz z+>%X|^aYzlA|YMdmC6J}(~V4tN9fmy&thBA3 z^4zwmEOPzpb(Mgv5txG(`l_?3$-QkG!Jf{{Om#BYO4nL4E`I1G^$^9MEb22ja` zmaCuSMPP2JWYpFQ<;FT9wD`&M@Oj)wav9kW47Vce z?lPGD=MRX9ipXZ<6#5ifjHiJ2+`w+xI@zs;7`7jO9I}B4wj0B^s9%I2rtP7tTX7{h4q!O zB;lELQYE7eC8IER7m+c?(&N{;{4)!8N3^!k*L5wwp8-{XvtN`y8FlWBuPj%70EG^$>hC7Sq4{ zM=9Oe+GJUFIscdi|HG}rXcI^S$;3D)(_nBH&L$^95EU%;CLqI_wW^yQ|1JfhG-}Q) zX-UG7{`|SjxYoWY-LdZ5v}V|JY1guT`BYy`;QDpEkv^I@8jS5X+V#BQ+L#e?|oTZx%|8~7zhZ2y6*Sz0BF`I4cRuv0*iKNAY+VHw}am3-GM{S^S z9RTwRt$+bVi>a_~a@p`h412lB?Yi!-qv%zIF%Rx{#y?E-+cw4-I|_p3!o-M+FBB&J z{v1C-_c@Wu*O9cUEDINpMbYk(i>Rne%s|G-+fe%uwr&D^Gng^!vxZt>_iulwT$i)( z7QG}LQ^Hqo;NdYUtZ1t9wy_tA)C1Ij<7H48E2q>)ra5`EHjiroAQ@VTwxIaBoh+9H zQ{-?R#qkqh#t}G%P!<_&_@j<0{|s@9c+)~S zrtE)8A~j+L(ewZ(46L$)Gle1p%b{v1TVc7T%@}u3(DK8&;y2*Dt>~fO{P<-;N&yIY5crVOa8$3P^l0U{FlMZ09l9&@nPOG{Yv7$MY}A7IZnIU9`$2Cq5YKbs23;=a z*6-2IfYhN}lzWLyYeX!F04Z>Is&0{}!S^u7wV(kNiSm`6;WxDo(@x1FrU}7L;XbNsC9OB2DZN^92eBeC(Ag-;fL#y3;wK zAc##k#!c_PZexnTR0zRJo`o8Lg1UZkGm1OMQ?{dY*|s=%*cxbJ4+WkI<;ATFkspSI zikTXnURBm{1BanacUBtjCH4!cS@P4&PA9Nx+0J2r88x=RHgg3U-zKH{FLgSJ74rJo z;-5}R3UvTIRQ*7&5(ZfaKvO+{vVb|=Hs*z~xEKl1rnsao9JUOLdzmLvd#};Lt(ktiLSoy@RK(i1xuN{7 z!KizFuj=R}FzUB<%_ zJ8hb)w-y+=LyUp18$!FJ`hmAOtl&eS$@Bt6#0ZI&<%=-Meoma0mS)O700^J0!D_h@ zYf8C;4eX6Al0|a7RznAqVI6+*+~k;o{o!nvpP7m2!3!Shq$KWzYrTSmB!?T&R|J~5 zJbuT+5QK}67dJJ=9Ozm<-1o^uyNQt`ysAlG{{|ZG9ssq7pLWq8VlzAtV-d8J$}?yc zA@xT@NWKkkF3ioYG&cw6s0ue#m{SrR_0i1j*XnKKB0osF%v_A%V2SvKcjl6V(<6G~ zD$@gCAl(%ufaLD46cSlB)@tpLKc~|?w7y00BHAl{L+K7YM)X-6T)y#nV>8~9OEVgT zT{neVTrjU<5GqHOmWUnRz)>XZmk&NjBSJ26o0)*)QjoB4vaBMBywcy~VbJkLn;k?h z6WFT0OHf)a+A1vw zzN^Q2ZyL)*8D*I79UP!as~(cgkhgQitl)z{lSP>7kt!ES&)#l}X)QZMtn` zWovD3bE=Z_m=fMI=$>9Q+zR_MzC2@T)%Q1nzc0G{8<90zlR2jU&pmHBZOJ}dU%m2y za!16E?41pc&c+dnS&e>WEBivMW|#E8zKxaNzy1$r?-;Dfwr&YmE!47YTeWQ4wr$(C zZQHhO+qUhsy3W4mbobp~bi}^jkIaaCf4z}o%m*{)GltF{&SI~PQI8VCauA=JqMnn) z;68Wy3(AMg4%!q4CCQjnlH%47`!6qxJ#9rWZMt4eY*v0J7x#|gNN3=2c-e#VD4<^+ z?UgAs8#v&LE1jOVzHn`!&p`U3!}K>@Id0&_XLI!&E->7RG0*h8!fwxK^-TFZPCn<{ z5+9h~qSJYK8JMZa;hiZI{#5pZ_($FQPV5)^xv75uQu3-Sn4ItEt z$0CYp(TS$W5tYV;_G~hvH)=PgX^E84S_oxL%k6b_2r^TCN?#3Dwp1644(f|_rTBI5 z5GM9xr}MGKj;Y8uwfYN|3JI#gEO`&Yi*6vJ9B(Z|jJY|V^ljZ~? zWl3}5x7Kh;7mY*m8>P{S8$nEQ0i$!IHa1AjIk266VIz)W%1bGGCT)~#VyqfcWKPf< zcJ-~Qwke^{I-57A8Y6lwhfOEY8{p}hCZ82HSmhf>Ijsqt3tr2bjp?z*565$ZN}CaI196j*BO$E$6O<`nc5q#9L)~m7`rL>T&L&6Djl9nKe9Eg+ImM@TT%}B z1H-U~o^oxHsGEs2>b#a_C~fe0CfOXyb=s9CZs2N9#Tf==SdQ7{dL>JC<|aFIEkN+@ zEQEzR$P3L#2abWWPRAq`Er@!)>A$rM1^^AxgS@F?Ef+;6!j)$tLB`!1HsYbA)Cnya zbvW9!Upx&(iK=DpH!I9BrPN7f>M3IIK0`pYhPZf(6)z_Qw;BRUm+&H?PsuW&-i!>| zh2l*XStKCy3Z_9d%$GxJq+3nMRL6F?(EVA;>r1SI4M(w(cBOchMGfM@Z`F`)6vnD~ zXmYR-jlZwd@*~UU(C1x6u38skCgnpbfE+aSU?|)Y;e3(OtR3Pq9qSnQUxoGnt! z7b;OJQNE?w5>YA}goqJ?>di#%%=1G`W+^tp&cvW*2wAeK6zgZ3K$3-P4^JDGBg2R~l zK;~a7{;=iQUXBq{(NP?o1(CP0!vy@;X&pPM0U*3D(sM%-GV6!x!1MBlPbKIK^YFis ze$mT2qy?Qb8kQmHb2l4afMbtW`&BkHH7U(-Hb%*=mAw)s{(uQ~Z;3DFoWprsM*&t` zm_QYGE!9huT}FgnpACX7Y(fjTI!Lx^n=aWG>MfC`^2ten!b z_+Vt|zDXWF8C$;YAZ8gG%uagHHl=>Cq7{iIzr_yL7&`BaalxvB?Wgo2ZW30&_$TJJ zu{ktn+LM``v+vQsafPHUowC!2l%1SF=7UIx`vY5f(C=ZMOZ3>^*-%BlLvMr%q^oRE zZ7+`+_{{oBIKuAL%C>th_7(@t!)PYFkh1bsln>d0!s~DLY%R|4%k86bDyI$|6=Cx1 zVKo(=6CGK#ly4UYH~m3v`uY$^<&yPJ;AB8c#}7BQ@>`5(wrsbuj~rR)oghT^S*i*f zxm1l^pcwB_xe^1Zw33%m2aL})w4T8<`fsNf4_)uWu33WG>u%7mD(`PjzZT@VvauyN zQ-^PDl9%@#ar(CQSGEr)pVPKB2wd(!v&txTwpO?Ncrg{&JzC57$hOsYCea2UqDs`h zk~I^$Db#HlLVBId_HegK=^xtn^|w3SV5`~Xx|&d;O)qaI&&3K5LQJp7MnJFbUmU(c zRW}G)rk6L4T_gEj`0cOpzD2JjCp$&2GC8~AceXLdzqKWMz5#-VyH~@}hHpOuSiiMw zSwnCLp(Lg6!_|>@Qw~Cu7t9~_3=Nv3&ZxtpY-2{>?&ULsjoz4@BaFTs{isyC&XY5!(bN2 zwu(({kvzIR>jykIkW!6kH$c0JPwc4LTLE^FNxP|W(5ZIN#gW~EbNkA-!+IL8bm4>E z$${KMV+}Gsbky1&P(E0HDf{?AGZyXgNvoBlts}$L1NVwrZE(2P5>?$ywi*AhQ!+q$ ze!oI)o0k;)!6eRoeK-1OwmQfj-;s^iZU_TD^6iBdV;%|)6MKDr5N{K|uX;z;npNwJ z1C|2Pg3jg{9q<{tcX%6T4;ye9!{uGSFGsh>P$$$^oMkn2 zlnfSdSPG}FGUM@QN3pp8`G^>voB-e|UdO?X+!7@nkL@iNoGlyNZMIYnkLIu2d{5FP zt}kn7q0Te6E0(SEvJy7G?1ffnUE<s1H`$J}|Lfh^mIFXZqhWQS~xD(`cVC=3Y6ihCy1=pPzRL z9zkFl-h^3jSs>I{4~4sIfi7B|3-#U$P|n+vW1|b0UdP`uX0jHQDYvv&WdP$2ZBt z&)E;lRo|psRhe2b6WkB9+?L%qsGn`MprLaJa!gB9D0C%OdX@uRp#|dPgSDnWyV9}47DibBGz18 zk$VM*8)8lFg-#up$%@8T^>LL81C|RiD;4Ub9DKDy-ql4zceRW|S|L2%wJTi~tF%Ft zESZaxs$}A2G&_W-OV(r5ZM(={tCJtIGuj*_l^sj_xeSXe!_CjK8G}R@;Hn_cM-we! zKc0W5l1E*(t5`rL|wke&&6Ln+t$y`}w}#pc)N7 z|2+OPw;nP3f6aCLYhL)j>ozCKja$w0AaXKG>M;{qf>qy9&LF1YFLpTL3M8PH!m`R? zQ93F{Udr0q8WFCKde`#l^@7~UhOkQkXdy>37*BN`b1`;%eZPS1FlIH%^rr>7VL&l} zsiWvoXDF1|8)IsXl;ARK6O)r|!7V+#`vRxm;Lphm53nUnDHv!Q#@2Y0^%EvWVPua- zw<>WuQ!CD5m?hG~R{>pxdUHHYakUl)T%BlRb&s%oLFMWX1j-2F&ZRcY;;GoD9)>Di zXbgh*&l)ceAwS>W-LUF*GNd#xRl%X#BTPI7=}$X%j`rEc%L|q(t$i8nU_nVmA$P3< zttUhZA|o(;u_Xsy*-rVUCd%NPrRdrxke*H!S+(;fNQ*?}fxCL(Oc$N1xziyP?w11SPevi0hQ$Z8ZGC_Cw zcO}Mw7-9^)V6%HPqX1al%AR^_Y_*@zI=waq7oYSW{d}_d@%}gQXY~YSqA-9TKO+A8 zCip)hf~cj9h2g)%Z~oG97cg|tHMRJ+;%8yPw)6rI?6C3f4*8up(7-lniH{qSpe&DM zCxvL^a{GguU^cb;B{;<8RpBQ4?eyK~0Lb@`Z_@n+z6(GY!Xi;;?)v)r?bOw$&c$+-;B1i!^}-eRCjuw`qjncIRXWq^d7TXxQ%e0LKSyCKnin|&i3(Z{Ig z_0uqE2WL5xG_h$nA)i;@6~YYddjiy&_Ou4{GGqq}OK1gM6W~*t&m`9?(xZ%Bl=@E> zB&KUFkbTkKvwf1pe%~y{0e_; zv#fzOCfF;m;M7&?iDetsGD8{m*exYaqvU4Ya8 zJ06JfcBl1w31xpIjW5i{L_IJu7ELSmq-Rn8U?8ElmD}pTOq|HJUA2;GKlW%cr_B5= zImMvWTPBo=(_zgdmj;a5ig3D|*k~=ZY*g_xAN^p+cW!GKt&rNJ3zDDfhpHQ)^OA`d zXRxTv0Xm|Ld8T7?bRC2>ZdVB7HG(>)m{%(K9)L|R60tR)h97l5hID^Y_n_)b#{}7B zQQjcl6#`7-P9wWOzl!;y+_Wm~oR2U=ctE(;_Bh8RgLg!Dboao-C_{7veU$V>+cG75 zV%RLq=Ix8vV5+`DAE;m50d42E)$RP%(67(QNS_mRI)lIe*E z)I%al0E-m9rOZ$%$(?`1G1LJ)ycZEX^80PSg04~nM&gUjioek1AcXH4N38cI@-9o6 z>BN%2T&e_eXMQ8zcgPZ+Z-yHv)si)DNcTZUC=us4J+X=tYFKa=Jb5=hneh3LRX zh5~yLN^{Y>Ck>SCIFSd?rx8^aas0i*whg|@Y_GKw)}P9f9Hqre!)h3-&q{Np6RQ81 z?qgVSa9!@Iu|K3^^;6P94}%HAq^|GlZ=;BeZxbvAe{fU$gB#=j2sc?rLp#_1fc9T5 zA-jzbetYU3B=Gsz8!0aHDX;@T{6BzO(^OMVcIfn-j3qxPrHQdWesGViv~xwfq*R?| zm0g|vmn=QM#X$_u93Oc!10Ccbp7&1F@M7e=U2vW?D&4T0sC{3|#_FYN4O&JQHmgUD z=59obH%SEHn@IZ{f|R8V!4zQSSR+WT#4A4H7Xg3MVwI*++oMvvZqY4!zP`sCHZ5YA z8gLNB{Js{c1)@m)b>MX5LYT$pNp9)KB_;hu z6lZTAdH)isLy!X{ZiJ*R0^q@`nvsp z!KPwh`EStT{?d*P`nI&U;h#&gAwT8=WT2|#J;MSeYxxB{C@N4<(iyh&MBefOyDBA! z6HY-$?eYT~{FeRa?l`_U`~9S?aR;}z=l9PYP#SZUUNfXZwy_dWi>y)mQV_bdGr(hs z)>NLuMx57G4By4grxtL@ot{X3O}EvxrBD=b zlcSZ3_4un=p8DtNUQ+Rt7qJ}O0ok-sQjBq(Dh7rX^F4Qf^KpUkkC!cjwjfF0wlN5G z^3*bFezS4dcLx)*7UG&Fn{^a=!ObhRqme5WekkV9R2=drFyo3jP9wEtu1pmyH){xu zTTbG))j*=tkPU{@Cj2uS( zs<@hiy>yR(;*@B$!xxcRIHg}44hnkWBr>mjddk1_Q(suWX_M-ty~uN(zszSh3@j0+ zT_>Hrv<5dS(D!lgmfKvbS zmpAB5fgSJMe3?g-AvOrLX>OqIDgmr-+vRuK58i>jc($l-2+Sy65sEi*1&a|A4m+)3 zwNdb>vJwMtL8V=ml7G5{8AE(u?OEtY*NIxAGmq!-9AP;DPNnp1nO8FAm9qOsbFMALy z6p-5oClHmrbi~)6=AP5Zsbhcj^Dilf=~`{UpMt(}P=*Si;m3D;ZN3b}SMJvukyN>N zau}A{aAE3`xjy_RQTa28UI%2%3AZGMjK#sR#ej!SpYo4Pp%VG-xscb?QEZ-la@X3M z2pINX5PY3jsRLsA!E`C4AdXXCXTbq-#f&DbDh5a56kt-_>?V?v7#2a~jN`&MZNoTN z8+vbsd3)9!b5S0GDkrA-TatXzgMg>NdY>V{u2Gb6FE2oE6!7!Sfp~SZ; zowKHaZ-;kUYuLF?CJ|xlz7p_>pcQaTb_+-M#*4sApw;#`MYgD3&D@h~nFg`>J@S6(-utr-Nr#ZY8hyN9lb%;kcT*Fj zOSms_fJ7BL;AOZPhKf9qkBzLIJ#TLrT6&)LQZ)1w1=9hyOzhMIv`Nk0K3&FS6qppL ztbJHfji&z+5O4D04u`pkx%Ht3R;q;tiB4fe#i6&Nv!b=4zxsVecSXzPe~8zf0#d>AeTvA zzKIqrGN*hRJTOd0YiOkePFW<29?Yc#mg&?A-!ehm|L_~V3;Nkp1I461z&19uWEx=d zVTbdPIdo$c>~NBoHdj5YQMP}TL7pj5Rk0WaML#vVU=isX!=f!Z&C+>4{5u=Rf@E+a zY7s*-@hs~+OQ{_8L|}-NeZ)|;Y_314CqA6v_J&92oSUv?;U1H^5*zCL#sz5SXT>i3 z+79w4`bms->c02^g?)#xmt&MRK{xI2atQjO>g={2VGfF7_V8amMC%QeY%jphChXoo zE#dBu@1JW``u<(Q4WV4veaAiND?#6X3kH$_D?$lt{l9t` zk^ilxSNR{4;(}T*Xeuh4LXU=-AC|l?eLsKh?WGmLvMO2v+m(Vo^wvfVXfz&lB;+mEu{~)803P?=gVk}kYgSqFwEn;x1p&66U7pDcC4rt=9 zvM2gQ5N}Hni7h5MmU-~0HB!UEMuWcYH=yN21_)n?XMMGoreo-cu3F%;4{ ze_#;6Rk-m?12f{84@LYSDU}ro5+&Zi0XDdoWi3imL;pmzTrULK=;n7 z<9lfkYOJ~_B;Zpg3}FsRLJI$$pj40{y(mw_rA%Ovg|UFRBL?XU>0*UIG><+1yN+;8 zfQG3aAEDZ?aO+#rQ|>atZF^A-(Ra^~Obp~4GWxry%Kshm+`C7|W||cPa{9}$&8$`O zfU$TG9kWe%hKhP0z%9pukIE&0hVh1DcO0ris)p z@~Kp+Rn)o}+G~Kytuq^}C6fK|+w8MymwFYc!4?LP!zVoWz%tHpWZm!mZ#Kn&C`!{E ze@H_M_>cVO{@M z%#jX!eXTvQGjuKbUr7eX2PP1;J?Cv?zmC3p)z!S~`{fO)2Z5s^KcLQE3{Y_a=m`2y z!7X+{|C--Lgr7pjJ-4AQ9pJF>u?tXH$KFIwI!Z!Bi!z>|E^JdDp)90BhoPvsbheUH zEILxE6iGsKomnm#-^{U6R3v8(fXtRD3t>|??ofU)H@XN?^EpVvh2+q&m*iSK?ox#v!{cwJ-ueLFTi`wW zE4%n@H7kaeLF@gApUt);-`(lm;wnb1A#_SJeV#2YJ!@>@!HrNY+<&WBANZkm>i)1p z5bz(FEB!y&@gH-gaVrE`L{8>vw7!6(1m?rVATC%vCQV$P#4-mNe<4%BypXCG68%Oi z5+{az1&3Z?$G`zUn4NssW#2MYKBngvjcj|IB=4`cFTfsTrZdvqi(L4eFm&BpfsD}K z2ydht(ozRA@MhNuN>1rATPv6@H(=GlMu=exgmjpp9|~%Ncd?=h#Qmh4aX8@l>Ntk9 zOl>ihClY4uBo1k!e1+aFuyAR&irHC{tKUg8CiPq3mLT})A~;Y<>-V(n9sZg9F5bq= z!}#&FEYlGpV9WWu3zyzk4$k?~TuGpgr*4i81_>h_H1N#)^i2G* zPy$t$-{}=A`iihy1NT=$5DV@`aWIGOhu}=qi`5%t0Xg}N6YHs@;HoYz0xqxIl^h&+ z6<_}TJKAm=5SmaYx{0up?ONlUX^KMEWRuzWDTUZepzHR^SE}*%6^mqhWE@{{JZR50 zP>wh76Jnq#+bO#c6V_8V#Mxq32=rTf5+foJizwSXDDtD0f69wHiYpt)FHoCg@z(eE z`IjFsXv#9ZblCSSTrdq0#t{@AX0%+!8o_^0Fbabl^Pzv(F!HCop!*-P;eQtvVt{{o z&QbH_loZtM>k>ExLWn(H8KBTvfh16j(BoBunZ?P739CKf7sf0g7%v|jq9>t2%*jIY zvi9;K_mt@AX6GLuD`EseExrQ3L<>;wo;#+gvd7iW$2w6iWAGd=L}@s5L#(eGAX6aS zEf*VIgj@U0>^^b%qSxBT9zApw*_-$1WakxBz@M_+=@$yv^hT6Q zC=Tc>Y`aM$3`A{)4Tqx>6o*^w(unUIkku%#ey1=OOXY91hZ$9L4S}}grRCufk_+nZ1p+(F zF%VP069QE9fUrDv#2<8rIBpbMty?>WBx_`#9g8u{T*q@8+p8@;tW+GJFR17nO4#sAsc?zkf52({DWT2DgmJYW3CD~8ITmXHtdi5fb789~Xcg)m( z2+FiXCfq-oaGl8rWUWIYaT3w68!U)y|CP6=K;9WEslcYxN`3#Gr`%yC%z{?A>nRAD zeTgsqqLkczskoo_!I|r540-IYz&bo79BtDTbbgD_-pVKi>92UAA3dXO?Al^LFl?qU zfalR_{whM6jASK(3?{n5^8TY!T!x=?ts+d*BCP4_SHNXix1yu6~Ie~YOr{N;}P)kM_^XSGY5 zLkgOPjAtjGljwW85SzL-6|;smRLoF;DuN&}$M!+tdrQa0#_^tyIUN+H_?9=>pT%iB z?X~SZW%cwPJ-z*-Vn-AJFF-#;nDxf zcjZ2|>4s&>uk6)B>$8pfagyN-JSO>7>W&d}$kpV)@1apbIAQ#tk^sUsv^L1`S};!$ zQ6%Wcpei*s-o&!tjT^FNkWShM(UajH{MXk39h%j#1wmGeuBS--Wpj1x#-~}yreG#r z;sErOkN~(nI0?T4bc0Nm8YI2rbp=|x>`4^iGBFcv0QDi^xKBbnIBBE6f?lL*GqyLU z68BpKvrOMN7NNLBL7Rcsv3?a|iDC@?yle;n+b|q)n0aC`D%Eb4lO0D9UG@y>)nB$a z845kK_1?0vh+gl~5f@ECvY5?j#qKYPJ3Htp)nql!URzZDZK+kUT(&`Vt{9GA$fPOa zC_sdQ$=^)gW)2=iEIF`b-uF^1NlucR3et47tO*X)j3ARsKaQ?+&lZ3L=km3@IO>#R z7w{PE*^;y?KO$1k8CSj7cMDz_yjd_9O!jMUq@5R!}-)1^)UmZsG|2*mZ z{6`h$|7Q*SSN`~$WSTVZUwK570b#kMH3a7d7KRPQ+Ao>N4;P%;B|DQrx4wQ}*C@jK zeh1JaVFd$`SNbUzqU}!(+2YpP{-L2)Mk~|ew1L*XlO|!TFWw3!K5x6}LU%tHcSY81 zV?hy?J~VquP{*Rm4(O< z>>7dDd>7Z=SWJ1hTR{>YmYtuEL7QiDw>&23EvIXT0WdhXo>eX$lkq z_Q06n&>rKP^VLWD6o8hq^5iK21hFc^(lcxF0rs~|AAoIp|9=?`{|O|l|36-$!+&;t zGZoDL5}TX9Oh0Voiw(|`X{1UTlTQdntq_~z=`%yvE54-L>vLMRXh!`?~LOJNbstC*S*?Gr9aMSw>SP%guMawaA1?b`oZ(72m;Cavd&i`M{qXdLn!vMCv+WN zpp;n+)*0ruG}AN}eD^*=JDy^x7D-%-Y_lVW6pl-aSBJOt?4`?rK#q$J#D?IN;fGTD zhM2ZTA)<~Lh=UBp%GN;PuU27-%#&C!yXl=M&IWt=W^w!E$ydjv67*M%Up50kq>8sQ zTjVRLcQNF?W2>rnRrCP;{ANS~L`4j#Qi(dO1}mUbcD6Da|9A{i&={`aNggg_SORJ& ze_;^(-OHv{IUPAdN0U6W+N3|U4~K%*43w&m@o76{ znS)4^Thdd3ANfwk*No$__Y9J%PrtHr@k~hv%wZXY``k$8*#_0N6vFjwZvio58zUyY zi{}V@iC6*N!y!W$nL~wIQUco!)lY_I@P%zHt+h94{U9ljQp4nI0Jnf$rgz9iwJ9`-M7!tZ{9ux-00-}_0(qEE>9v^O zhE^P%zF-b;YJ?NSU>#mT8}fI+uaE>XWxLC?J4D=nvwA@^{e;{rP;~C^M|Zmo(5P-o zI4W_Af>41p%Qp`Qc4^@H_*+pap!oZ(4dus=LzaKEDgNJ?{a>73R0nfWI9&9aHlTB+ z(@2t70r2zxH7W)R$3O|M=VOAL_5*!jZgfJD?sqC!y^o4|`%0MRh8dMgqf@O)GB`C@ ziWv&UidiGIrbXjYp}VDdr8vddx7YKQDm)+N&Gk3Ow|>U!Rdd!O=dp+L%WeBJ=MSB% zd>Ma!8T61Rg?Im1==`=}oOJ_2zSyd>EE8c;3x#En_J}qU!mJx%cw3l^k>#Da++Hfw z=GpE+ZLBAIC-Gq+f*9Bq|U@`QK;RT*+ZD+p|&_qAwpSi-kuZ5 zfIK*6hu#Mxf?7uO^#!AWrT~B-nw@BYJTUFx-EhMG-BoNvod`DP3LQREeWUp%frtZY zN_BM~J{LyBQ`BfMzp`f?vm0AK+E_d+dvxIyJgdnt<@I$^c#f2SQS>lJiCzU?>wrUZ z_*@%}-)xB1#P(FRZ&_OdO$5K2rXo!Nit|<@_kp+iOP%$Datf%^AVECEOl%3@9|j{P zMG3NQi1N6)$@sPDke_x05=>G`_X#K9C&mH?hfI=jw?hW=XnFU46CTakYwgSE=Z*>G zLLQ=%J1CI5tJx`#Q58TqI>vW9>#9mMb<@S3G)~L9x74c07{w$mfkZ|bB+vEJVL6I{xn4e6=F3DMqAQ)Kg#PN?$S)TC-83#z07Zn( zAYouOW7DJ)HcG#_rO{B-SW7LL5k%ZukSnK_1R3ZrG~q9F&|WJhvkfbXO6!dqJD7ae z{089`sWK7*JCrcwackhyFvuRiyV`|F+lBq~(jI}Hy;iL#jG=_U{n^>+pw+i*BV(hI zEIG?ZRNBZk)!RA{Oc|A4K@YSq4x`;6gViB5>}rVYK5GITF9tr zhJlR0-LSLI`tWCgyGBwwbI=~xm>vf8!i_E#$}WLlUqy=KQX3>dchew_6gpL=UuYrJ z>X5;74wP0zwCPJrl=M#gLjMkdiS9#e05@VNOsIXSNe|02S#PT&ftwt&isS+|RWV4&n9DzQ@WSl?L^S$3n>7xd^cbW(e`3SYZ9Eh+;AKE{wWesIv9rFn$3ZSBJ(%i(C!0a48AF zFzz#gZDSM-Y09#NTtc^(>~JHQH>2ge9dcn27Ms%^TU+L#)}zvq?G;?cxhH#6-jN#? za|t3LH*C}pRN+kZNeh^EUMT7NBVYJp0~k%x3Ju+!_qebC z!TZ7y1B3y;Y)<$HOo9cWo*|^BR0)jHGrf$$H=f#GvVyM9UwqeG3oz9zWpKcrg!sic z$Ri4}6fUsxxv=G8y}9w)8?HufJAc6J3!{5g?89-0#jvWq)92rwS#~mj8~1(OK^uca zgB?IJdztw%_FMDMM3Stws-zChvw0Qu)gXtZKZap+@63o=ar8&bOFg4cyu6rWP4BS| z!O>ZWTNajb>UryuV;09x%DP}no4-))pXU5r{LYI?zM;FvXY;U3LA|8HN-KkB@s?)R zY;}7i&-M&Ul>d|!eUj#vD^+M<%_~9D`V<`2o@`*?B`r$*pc+1yPim*S2KWH1HhU?K zTE3y_z0lAXkB>cxqN}&9GBZ`rG{;I)ZIL-WgTk`jqdJR4S81Zk_3Nr{xacs%?(q9A zk$H6bC1WqiAH;s%VaUHQJCk$v4Ul3#@ zaMv;i5yGF$VO!@vB^Icy=(bfWa5#h+HC;PV+n5%Gnm<64Mr@@CJ`kU;MZXLLunB$H zcxxH!gPWngM7z>tC%+&wrkLm=c;E6U{9ehjIUPY_LOSwh5%q9H zin>$*@^XcimSdW*2c5j@oG9h0T_v9hw1!fdcxu8tWxVaa1Oet57?DC3_xZ>uKNA$V zWr18Wxngd6pHKnCY^77>l3^4YW!vYi(Y{~6S$g?68Mz)CL7D%#)S_V*rOrbL3HzQ? z7_F|2qi!|_xS?GeHRIXabpF|R1UkxTok~*83t@6EZ=&|$PE3sfTlk zT{0N*u}=BY9f9gaYTH7&N0rJV@dU3jYJwH14smy*t)c=2TE&cEVVo~=WC}rA#^44q z`gz&b2->1?V+22zc`iTLEphY$E&?K9!(p7cL2GoT%2HubF75c85mtXTelkYQn1qx!2k~MtcD~S>Ue5yc+X;Nr z-H~J$Q>$QFpd!cUmO$TyMWiUFmwu~IP%-*YC!eI0-yWe>`ZawXW;ofx=(gE=CYi)Q zfuUsHYDpIt3X4YNsWzs3k%%z+=1Vogdv$`o^GZn+!Z)IPvqq~$#F`@hQxbJfGOAe` z1C-LbCl?H`?g#mHwZu5{hx=oWbr4CGw*j;Ig&U=1BAGX$n$)z_?T9KR6j;q3YCZDJ z_X`ZVRH|BcXaHt!v&Bhd>rL9oD|uqBF!xfe3Zv(8uN~CM<^CLmJ=Dw!7oBp+Diht3n<*{_;dYh8+H;1%7ZS9|sqo6w2+dVWMoG}lon9+M(Hd0R zO7LA-V`EbJ=Xtv|NGoKXCc+hK1+?BRFxPkTT1PI`@ojDRUz3mJ&*LkGL0rF=>`JCfEMc2C2Q7|+{kj%PQ~=ISb3=B(N!5FwrU)YaV8Gxen0(rjI32* zBZ{uMtXY!MpPxN5Y3F{aUUQ5Lu1NA z)xSWe)dY{i>SK@Oa9Ux{EWB36XE>SAOw-w_RX{KxN{Yq0rFSNi#nv1ZMB^EOH3o1GZR(_LA|pR!kd<_)-mR&1@7eRBGo)$S^? z7TSs*UYL}WT7K<6NGXCc)EvOVVGcS!LJ23a-;A{<1Je4(+zd4RNCI_$ukGm4PLk$G3x3kgy6a{W3-+o@i7 z8&K!z*;k{Qpb7-#7Cz{M)ugRqxafopkxWH7zo$ebEDI^WWm_j^hB4U}wKYm^ZMT1e z6ll1cbR3UsM$U3@W!*c|-i;|Jv=}Qma__ zD`NfDqW`$BW|ea+tA^v^8lCVUiFXE}KG)dJt#8L8#lAz=TUvNVJI)J1o9?*VJ=syB zx+;aP6$QBO$=Zp-a@K%ShZ3grhT+r(G_CSPCq@f7l9-gc+Y<~G=R9ti^Dd~PbX2|V zL;Z{XM43BxBhO9{A(d)8kN|hkHU4b{fBanKMIU=@BVxMoK?;R_Z9=eI*9?u!)|#RN5k@ ze>Hw^y4ZC+aepu$%g+P-uniw!JUfDH%#^9)YJ)68gly|EH!hs`MR7YW??sO<^o}zW zeT!({GNA6MSnHZbG}jHN6pgLujxaz}P9Hy~>k_YsFDuudJ767Y|U5ae5T& z2l34Z;mwsQ(-Ku>_Q|KVgXz%-Z*C+{WFdadtZYRaS&QRv<@{O(T{+`#@032&s;Hg~ zYy3o2uXxPWCt*0IX1PCVA76Zs$}4gw5h|^XyhwhKk9H$>ddhcBewiEQazhxiXLUmP zazlZCs_Sgg!QndVk)ot??I6sKU%RKvx4e{ck8grJFQ~@Jz8W?*(`dKRPxBkL?hBsK zyKQRXLfZPumvLe_7!X+9AzeY`8O`~Lpa9sI0FYR(u;3K5QrhzdZUZTl3q8E4G+^k z-0Ijh#BGWPWb`0Fb_Rg86fv$_ni^O#jjYxCwX!UfkM9@(*;d9!sC)HBXr6v4z;y^-w+EX$kvNhA9)VcXHW!)VKwqmyb|RjF(s7GsgRkBuEx_U0*Mbk{dS-M;tE|TNr5Arzx_j=dXb6t>MLCgpU$B_fPBSQo zdHDq&HPMNU%qWS&o0*7CR4PFhsUiI;i2*b^Fh@Exib7OLI^aq=By+>I(OdZNCWh?u zL%h}M4ZljjyBf6drTyXqmT3#l{v?F_S$y{c;SuJ-ogmBsHK@o8P5(e87X1Xra+@Hm zh@$Q?Tcyk4mom?n$Q?GmqWDP^%n@jy?t0ZXezsp9u0N_9ls5vq<;|*5*f+1dKH7=B ze}JrmrCl$zfNnoBs=aceDraa{Ahlm1%ymlN-6O_<+p=0;U7^L|Dc%wFVzsBn-E!bx6Q5K%1DsUL3SPJ?`g5caMua&Dd=pK|} z2m(dS6Z?x+tL?;SlSPy-g-BOH90pkoaonJutj`r0%(QoLbovmbXy@!8x#`ahZJqL* zGK2X~?q=Om7sywt_NPiZ&;VB?(|fl$C~l7ucT_BFFDh;eiXZfOS)&L$2=F44{nA(@ zsh=vnh!1<6sC_edl9Eu|CZW%^K{iJ)iM@XsHHE%ldyo^Nu8i1=k&b&3-$|5~Wm-37 zN!Bg$2&#MK7(2AfAXb)nRn`yn_!$}DFB#ErZUFLOht-B^n=cX6{Clx8K=h{2E}pDg z>8>4E|3En*rieva+|FGsZhby(BP;2^iCwR%-{Q5AL!8P%H;99zJC}aLjCi67zqCH0N5^R34+jv)mkbfUdl|ib$%}l=3npL*j zDw~odl(=znYzCwtx3f%pipv-Ud;)B;T}ES{vRY2k+~3IDznr|hVz9vCXIhOIx+PaK zJq)maRy^oc7J!I({F(BsE!+34%mSmR7%t zQA(smk9C!%o#zDC!ausRQp*dQ$(pv9)EZ@ZV>7xT)C99FO!RDA$QSo3EacWslcSEx z@M%vXHIHj*Yekq!kEj!tSnBnpVjom^h4cK)VA|}it>zSh2369QRxOT7$mdM&p!?6; z7uNA6Y;2PHLtCRfD2;^L7k2yTh?Rr-9N4H_LYwi8Q52Y8W*FrqTDCZQ~b~CQm z98Xk{ir1Z)Vc-u9jh67UCzB2o4=-LZKlw!KfqnuV%BiOr#W+g%%+tbY4HHHzfq(gusjYq|N!Z58fwoOKNkO(V}0lTwlzvx(1l=|!J{GN;#! z|5O!fDaqH*Oa=A@Hd6XgUq}Z`t4^-$XmiA^U}a?GRhxSJlj7IG*^_P6du=7>w60=d zpk46l2u%W(QNJRobqZ9a%oYR26`!j(a(xhz zFgCG&2@g(sMDS{Mcf-oAT?-a=9g2=Q;s*34?GP>W;P>CoqLq5cKuCc6_(1~mk4`qr z*jd{c+BujS{(H(QQ~6yUV;PMzoKPv`kO-2gcC!RJ^6|IQnJ_#?@&NJ*D9{QVF_hF9 z^yk|70Jc3Da=9j_rgS8ktr?WEdPjbT#z_#el z=l3rAuKP{**JbaoH<%y8H^yj^@O!}J2}gXi{MoONAhi@?fmcnw*wNWaA22xeXi@|=1t}1 zQV@s@qbABTzEWVJ-}`M%vV4Thj#qYGXM6OiaA30)L>XeYI9*| zcx=t&S0|6vS_9HJAFtpcC zW9O=O=v@@}s&$bRewr&VZ^mL6Zfc<`X|Am=niPXlRN?v-1jM)vE}{xabN(;R-Z410 z1=|9RcerEQPQKX5j&0kvZQI$g%^lmeZQJ%ceQ)=z>Qnbs-}ipbs`Y1$nrmv!IR+Wu zabURm?tWYVaJ*d6mS_3m16{Hqud}!~+)uu*teiEiv8I3?^z8;=Z*ad3L&GLHBwwi&|$WeKK zB|yzM#uvLzqoQ?EuS$oNQ95Lu>q&w`4LX ztH=!yRybZL4dUI13K)+92IJTNiMRl^+$oV7_E5E3o zDmRYUL3Gu)Oq#h;+ja+2UWIy32S4L;TZGDX8xqBa+7VoHPf^a12O6SL-I9T#xabU8 zWSf=W%d*V0H*hP(GZLJVnn%K@5|75|&{ig`BH@_ML(51{r<@muxwtgg!injG4z^r& zxnbQ=j%nS~P|ajAr`RI$p{$5Ib;OlBOPXfw6e9tNzGU0K6VS}D*CwJvt!5H`E3Uqk z5FBA`S$w{3gIt_@w{EiUf(R@RiDJp`%GJd^|TKRf1`#&I*r7#_LH{#OSjN&`Dq_M z-)CY0Y}|%BI#Z8N?-#T#I5(bMS%X;Fl=4T1#W#PmX=s-kJzu(4bZ!Uu&3tj%{hd(A z`iwT(&7<;PS>3=yV)v~t7>IaP_pZ5THQ?SaKZ)aS@I>{+atO%oJ3zZ4uV1%{R5Xyx zjuXb&xjF3c;}@V~yuxDqJwFQG$@v0sI)e4|z;n1TANX9zirU4(b3|?Od}*%|^Sb0r z*LmqCXo#sXBuL){v;^moy?Zqt$Y-av=OK4tSH}Qn`dv~?Q4cf>S^bDn4wD$B!=s;I z_)v*0K`<>*9ia|ki13DNUDLRPi0_=;j&9rIH7DMF_;Jw)IzXVs$D*%T?~r^XI=v!n z>3sYpQOH&PC8Ohbn1T#4-(oWdMTa?ix`<8t2VLj9ZTZIqw6mrlzM^*E^R5F%5ANou z^k8xK5Wy+RX8f5tro=1gQM7_JcL*f#u1lC=NNfPfXzhGF9U*w=361zwt(|rGVZ{!s zQ2n^Tgi!X=L(h(gd!X$J-YMp%-QgD~`|MmYFnr!Nph7%W#dEW(`@q~Hwb){%&>>l? z-?5gcD#$j3XR!mz%Iv_9{985EluFAFFT}Uzn9HiSMFbXr*grQwp#beZEux9vw*cEY zV4cvC8!W{y&UueSl8#ZyCo*xj^}vdkT9w_dV^qUZQ`h%uIv3xF9K;%+N4ib0m8)-T z9rr6kFBWoUZu=9wHHFz@_^LfiVD5~zOVlqe9s3~e(@=38?7>-K{R`uv;Neu;(ldq$ zG2a+nKm_X?O{q;EeTDt&wvRGxxr*5LkPaqW8Og^Q7rYN>Z{*1*TDRdlg7mb*>-8)A z7XHdt_@(8Ac2D;A-}VDTUDRoj|6xM`0nz;D#Qe{_wSS3dTf1TjV{{^mp^cCcDistf z%g=*B8f(6XAkCu-=+1!~Kr)**+WW;@L0fHTE9~>#>Ml4gD0w~@^1YCM5aY#8(*z;R z>B+K(Zf2Qgw>n;XA05{C`hFw#pxS{8!p29e&bTeoz*O~CmGm*BvsCG)M7PZ_E~blR zNU!fo5C80m)CVV~LV3XmSoivE=`6GQde>CS4oeIpGH-{5$sKPt8Ve+XT1qtpm;$`> z%~wWTYxI#J=nArTjNwipEdPK#gB@|HQ*`LcjSvsQ5b3UARe90bJW$Ti7jP|W!YQaW zewDF6vE8)>SESnNZHL_t?QA2&qzK6}zhL*QjuGXRVsaxD#-BoTf>+1q)oYQnx^vJD z!e!E`1#=ZrGYR6N+%*lUN3}`cov0>&M+OeMjSq-b_6ooZX$1zc*1yC;)wgU@Csvql zSuE6n;K|3=(6yt1F5V)!Q@gHFqtZ|Sj04fzagqbrf* zG6emIqeFsJ=WUvIxbn?l%8H5qP3+CW9&X1*sByQ8L`%8*#u_o+lD|_PVc8;cBkB-N zB0U@zg`@g{H90KE>m@>(a!%)mf`;OiXMo6tVPMkYSkfeADdzHnv@`j=U;n*sKMq7vv?oLmHSqk(`yWN z76NQsj;phyI;s!W2J}5N7G!02UW zYyq8tY1M!4Z5Duq4z8l z??_JKB~55_fDYvXH{__109SbRZZ)L?D*`?qu}Q#}_0^IoBwR8t?JikO%Cl~yzB~O= z1ebt?^FH$Sh0609V^akC6qDUN>J7kXsVfy9`JyqoxIQ<0cN9sx5j$KP}RP0R}$Zjsv87J z5W?Ay8<-CmgC(W10no9rb|zD9Qxg{x->)wpkR8M;jMBVnL8e+i2tl?a#ZXa3gsCJ` z(fqJ^P*bWvc0?K7;#M)3?j7MYCgvD-1!#pU7ti;8cXwG$v;3ldfc;N0!DBulHco^H z(u;Rkc{GW~I zofvNP&RT|lrUaq_1rgC;Cr$N{O6+et#u~MDO4k0fem z4?^c)p_e_~guOXM!0D!_Q@9v_rOPRwd z?5EkuT&whY=~)wlmP(syOOngFS7R9Wa6T;esK?J880)Z|B!%War5 zu-sk=rCrSCmGPAM_RpU0FZ8e2&`!*p}vdCj1%oQNJuR+^!G?j%-Lz?X;xbx zwO2~@IWtSyTG?7#+gxm{u52yMZY;Gr8aiq!O0H|b|rWSya!v(bg+vE~#XAtol5Yr-n+s=%Sj#3rLL z44M}tiwRYp;KBVBS*i>#s7PJ>{Vf*EARh)c-x-Mnxo9WI#}Q9EZ%gg4mW+=j`;!%9 zs{A<@EK?T^S8IbX#D$3YTofI=op&FWoW$MWAkg+$-*jRRD`kqb5a)BP=WA0n=~iwy zJmK}qLejJ_RjB(%m1q(X^lN`dcrwO^w}6g2ZJ6tEsFKCLL;ku&%pQBbjx=1kt8n3LJC55`4J=wJoCTp9UA zfU2z`03pDUCYLhaax2siQSsY@El*Ii#DDUHNZI|NiNq6`NL*6GhTE78YB_?uw95-! znHr{MT!{MzLxpG=W|fLa-zP+q0k)#FmsfA%@Gq7dUQn(ZzYtBvPKMJ0;($71Rn4(Y zk~Y#G=9sFz41hM0X?UzZBn-%#Nqw6}ttnB7rT!QslCuqQDWlG>qy@sLJJ;MsZ7eTE zogiVBE$UBFL!GBa`4j9@)lx*WrXosc0H)5OmB65wLSSr4I2dSgjVj7=ikNV*5bJX{ z07*c$zkBtb-#onMy605cbJ>*IEMT9edQZ1glJUvEWddT^pHDq8}ATgn~)pqjNB<5KjsQXH(I4tDS&twIIxU{K zz)5uWdyS(3oA9=89)57U+XvepIh^fJP!Ro5k_aQXaM}>bL zRK{IEy(ILkOR7|`EDu>yGG11SE`?`Cv9_fgyl2nf&wz;83;tNVE*XIt3E0JCCZgF-3Dnmvo?&KL% zhLp9(O%sp9W+%``5yNOQ51ctcJYG!0a!FMO0h^gn=S05$0L_KgLye^pn<4@_#y?id z)ERDgn1-1_C-Va5BAsdvNX6+WRG34&ZW6ai|12UFG%(((JS&<}u58X8)x?ic_0SPT z^d?4ifP;7UHKZatc*SZK)+ED3Je9#@ytC;G$~qx>zT131arNALsCivpAbYeMH@@Q@ zf4Bpn5s}I6LdR?3z@nK2FNx}~h~3|?w1pl}YjOGrjP_@`J{2my*ZG;={IS5fe(IB; z^Ce}k(>acwy$z6z=#Y4I96m`X*V`_|dFD=fI+Z^*E;}dV1mocpVbO>TS?zakazDvk z>&q0eNkih<#>cX#b!BK*iD{sWC5`RgPMkyRaraQ!o>3t=nu}2)!$q~+n*%Wx{6a{X zch_oOX%egGmsP3s-;F#UKvYhN<=VZR+tJ2@B%rX}pShvhj*#nf)hSZWaaC&Kcb`|h z?ocR80Vh4qk^ij6IR;mp+>cJShVtc{vJd%)+y!_^Oef4xz6kUt_(;KK`TJ6Dzsciq z`kQ~I;+CHeZ^(J`9R*vwGw`8shOd?&a8@&C9y@j=Xb6he%SEiFh9Df}ro5f8t4!-) zM9%u*+o@1g!1E1Q`kje?q@Rfg373;IysxzTiPniVo@$Y+KFNE&AVho)ucV=C<#z_G zdz)2thKVg#6(OE*Zen<#>}bWK(vJk++z*h6`2;3SYid($tM(?6oDWaZQM5=@zs- z4p(`+wBBMs++3el@N7tnx8q1oMAO;+>ZEQVDM7VZ{LU>wg*X^{V`%eEv@M15TdS7_ z*VX)b&*jAla7nPK65IdVAF^R;yLT=qQc|AwlbGHbt5&dk+QEZq5yb<=W ze6o*TQS!--xz|U5pD{_8idzgl`5WOEW(0~~rHhX-;a(Z9*elXRCjXIT(ec8ZY)Cjy zH9*NO(jkLMIb%>R+<^Dl!3p2_n!JnmZML7|VFL3mmE#5TlSXnH7Nv6>N?juxUwnL8 z+Y*m4PHrDY0>M5t=r$$}gGYqf0{msegBZ1OS!QAu5m|qORZ@t?}i%0ox5d}Y9 zA5U>mk_+UMVp*qjKIZ0qk`yA8Qzb90B|I-Pfm=XVd?6Kw2LyPeRoXUmef5jCnKa-8 zF+XUPtgB~#IzFgB#p(e+(hR{s7nIo@hrHG(VI_9z?z1IP+RKJqL!%)$B-X8g`5Y}S z)vp=T@BOyK#@MV6{94-yZJ1}DQJGeQe>)GvPqxKAliA-71!xbRm(<<9!6GjoL)-|b z<$XX~gtM=X5}La*qJT3gqa^uefas77e%TR8*-sd& z5mpOfTcz%^?YuNO&Url2HL$f?QnzgaGZ0(ouBsLi{!0f@fZOkTf23C2aLQ@f3K`Ir+pvX(+Mt{qJSx zF#s~@<}b??`2Tb>{J->d#&OdUfAw`k-PVn@kFwNZFOalho`~E+9ZHeGVWED{;+#R& zuJxpwFLrodK==}wU_#=@PYm6!U1_gxUoT*Gk@i8^ea=@|#mUoZT;;4dQtD>MOor-w zRnRh^;Ms|dn1>mVKcEv?Iun~nHY}TtxGA(yaZ+q7SM4b~ZV!onFuIi3@n(@;{=$t~ zq;H5u4Jo6PfOZ~)cHR8)zB|qdkyr{(v@!S~pfiM$_Szca$xv8od3k)}quHs3(UEy> zJ-m?CPkL(`b0=JeX>|RTW?}(Cc{3}gzh)o!~KQ}+G!E#xb>{igIjDb-RUG^)Yz+xuy2 zyUXi%BIo<-38Du~2^j>^0&);RL}+J2v6WVl)ZD5#g*Cf8fEXcd7H%^Q6_PAunOO)N z0>*@cH1a4!BDv^Mii2};{3t|6q3qS#n4Ovn6;(L#7dlH~wu!q^+;yE`nlc-0sB~gMT!wR12f5HyNYr(7%=a>$4loQi^QM zFp$k2$8?<^6wb06n$e7lpb8*acZt0!^|?c20fYbtfz0en>(dcq(#SCb>&b}v^}F`Q zFAoW)Y~HOaPOwtZYH_XQ8*^C2RNl0zil9361c}8_?$NXd_{_c(|YxK#SdF0+F-Oz9bPx4(vfxbf^Xj0)yD!_9Ns$e z?oX~iWRl7?7sH$q9&c1I5rt-t%RlITuXh&6v_I2agtpZmdY1Dy@KlG+7Cz;ppKxz@ zbzwBt>2O6cI}#Cp#`1fxR_#i_E#GMB(bO_G*wx_Z9_?A9&OJ5A&hfVm2}vZ)69U651-ad-`0LcfBHg%^cqr* zsJ9~}t|9O_B=S>_L6ltypXM0y0^jJ7o|50_!)esox8-Oz?Yw0orni40)$kS4?8rEW z?`%Z#@Rz$WwE*L(u@7Q@vR|IAxe@kBfBQ-Bu*Z7Dv3)xO!o&8G=}^`RymcSP1Q+Lt zO>mz#j1iwGgy&6vKz~N?jQ^`^0mwa~-JGZ-ocmE{1ZHF@?z^~z;y8E&Kdk1*{_p$q zJU60lyqMU7`ffv~Pt=4+RYyZxGQr+aJJA;62jsV5&c6Nm`1q?Zx9Eii)-~MIC7t~> zc>UYGOWlI*8SN36dUUMecC%q3k3T{78>WBYQ=gE=yY_;)dlBYg^$|HNVhfT@ITV5k z<==siU9^T7?_j$^Cp7YC{FGzG{L`1POuwz78QCuHAv*>Uh_LW1)Sg9q*FFM)x$huf zQ@?9E7MEX!sLlPX<0x`r0<}T;YSb~BOOr;s{lr48Rz(zrla~pZ z%7T#3M+%fW>qzheu`#_&j|N-`7FCB9_%E!IhTP1q_gMU3Rmz zSGh3TI`ICFH^g-fVF7<5<9f=3BU1tg0t$rt&vVVcy}E$2xs{Rezx1b6Bps1O5xzu_ zbTnyvAop(=)d2+RtSQz6z*d=l3+QbY%w#C@c1DJ2902{Bx&>Y;%serZ}3m2JWc-C2uK&E zC^q{KgV_np4gHh7M=scY=@te;t(Ag&_-jA|b}^wn=E^3(`a-c*_s^g8an7f+i@V^= ze&_wNncF`0Fdy(xr4E>A{`J124$~to;AwaLH9Bt6OEm=~&BCk!mQDb}$_=d>Prd<= zC}y7r=cM-Y$;t`4U>#-xd6WKOON3cf^6jiWA)XTAu$;1&0!H^@fW11GW42&~@q1%_ z7P~(pI239Dh9P?s_*Z(o>_kyLcMX$Mn~}tH+VqR{drZ9PNjM7xd7Nzd0z(O=2h&D- zp=aB8r+T0l({E{cuFhI%ZlrKu-9gOEqPb#FFx{rMmDHw50jsVsaug|mP=(rkiY5(; z)$slh9A}uIYMKaZP zJ4Y3u1SzJ@ctAo+@lIO=c8hrHDiKln06AH3j@TL|gt4+K&02C+n}5@#*cB=EwU$8! zds-8mOc055q%QrD;zld2m|hr(_1J2F%;&G|F2I4Yben@rqe3)FkfsHCNLn>`f>129eYDLLQ&kA((~ZMbb!7F#MG$zvLk7!py); zDEz2%`4AuU&^}ut5Q*FpuI?>%1>*u)Z_XkhPiaHm%e7pFq6_(O1AL%hWjYH$=tMN|>sof@5#l(7D6xj& zad|wVQGwr^pIujhuB)7KG0gU{Yr&X@g!y(PwCTepEJa**IJh}X$b?9hsJ@qPrYWZ4 zLYP~qiqNHE_VV}S6CCb$ij{9DsFMEY3$W;oVf3Y0=cNy{r7%RQpiUba0sP7Grp)HJ z+^=EZe~;9QOTP2mf7yk9)!2alPm%gxg|@ciya)=Mm( zs<1S=iTVK^%)I_eO<9ZujHwKOmvE@WJGQNLLaP`c7;}f zGORrPpQ{s*wn>=IpqBG#&T?wEHiP+RfEu5yscyQ{2dArjF2YN`V@VH zPMb6&1*P&S2-;j+i3S5Z9P;O;>kx~Yc)lpZx?*^Y1W(G`s)&px~%23ZA^JU302V8uy1HG!j`&rduv-0Dy6nyyC zT9(8vnau*J&kYi_9b{Bs+&gMpg%!dB#jr|(-y&w!JRS8Z(G~^SQu&b*LyQX%u>Q^# zUEx2(F+{>Y&YK%1) zNc1X?Dw^tZg+x%=TV%#`upB07`EjgrYnY6En2_0@KH{+6bdbZKAwnhHzy_9Ih;~V( z$L)xjf`+LV27`-jHfDGEdtc~`PSdb+#w(HisUmzBCL_>0NIXv1@t?Jpx_RUa2J>0s z+jnRAmJuergw)c+1+}&xn0tKfd8((I*PO9LlD2~|JuC-iMlCUj5ebpUpM!z>vJ5v^ zpbjg2Ge>A!nWB79F4@8XBC#0M4eYMuVjW`3a6DtSp@b?-{SUlp@z z%NsF@Qo!wi2e^f^>&za0af#giklB;f8n9$*)sLN)jj2Z2a2OnW5BJQbeqcEle_|8i znEJ^!7wga5acSx1Kjc@l+ob{r&w9(koTPhC;WrrYf|BzJnE#@?xm~!c!C2V<4V!m> z4>RN^lle8M?dR6~%k|HT3c(VGTsKTZP`kDEms7JBmiARInBBDZ_Aq1gu=py2R}8)Q zZ`iKBWp*%S2So9-d@zB(FWMXg8daYI0|C8&{>Pf}f0Y~f9}#55e-gMBIwNne!okhl zKrj5tB!Vr4DHk%92eD4>BciAr9*vX2^d$1k{1({zCw*%pKL`t&1?}py_f_jY7oU&M z`%gV|cv=*U>{zFr+uEmK^qZ4F0Ar?`|5Fi?jx9IhaP7~mEv=v-4rmhzutIxc=NADC zI%O>pC`q&wYc(}}w>SKj4Uc1}{-sQ6(Ia8ZrS}rAsm=kh!>k+ik^pEr^*`2?2GRQa zwhwW`ieq|eS7UPN-XR9#xYfC1a?{{72#@K}<12*=>IN9ujXti5nQGm)vV$W|XgpW> zk$Fp;f(y8eNk1Rd3YkXaI%Gg}g=|}EMikG_5mg28Mi;*JY0}CH!(HFe`2>mN@J7T( zq>&KkbbNQ!+P>rPQo{%w$=1m;$9|E|ighGW7Kzw&i<%QgFT#jQ41+lnO(@0EZs@yt z$=|Uy?ast{;hz$$*Y^+L%RxBX(q*1vX7utx3nJoD;0+Ts-T3wLJPT;rVKz<2PyI<-sPW>))Bm()*Rm6BB~LHa8(e@8n0J)8An6t z>GL zK!#M1FokVCB@8qsG}2eFCELd=jE50&%_*-%$1B9m1@dU~;7b$n6J|2|Z8NjyyTcXV zCMBATH8GH#E>^i5uQ!HWuQ$^^ufM-ON&Pxo%JW2a;u{hp(=bg1h8&8yXnDr7qYg8n zsHqKaCkij<* zC5n0c+dxyS-G6|CSYH+p2L0g=)XxQu263&@_Z}$l@Q0{~qcW)nwc-B#d<52KExs)h zf3YIZw7toTCcvb@90X@IU=7l~ET_Q&;XfpP`p8+0lWgOE4)R=Cm!K74HR4!Sr7~=w@918pwhn7Gd^xU0$wUtB(pa^mEZiA_3qmWYgTnx$_&^fGC0DUe z=MOiRU8^;$vU{>3?q_ZUs$pLfq7C5_#hTwt108V?DK#3HjF)t`(CJa+{g@CF=Etfi zAl~l{qN(!ssWv-iQiv^p(r6Kexf0Do0~+j?g?OT-`eMw|;MVOIX0bv(^Mu>DcD@2W z-Ow2jlLNWn;(T?rQKjWJ(&OUE;zmT&oTUQ%e(taXx6zROs=9|z=w zLkP_P_X(oC-Q1!192ECU!TDs?;*+V|&`+IqZN7uBAD#bCQgmmGKDIFP_R$2bnwQ0A;Ju0wo_O% z6C8{j$W*eE!Te(vfrpGonthAy{%3A~t5Ti9sRp?=xu#*THHOCf`D6JvoT%Vdk9n~2 zy@d3IiF=z_dGj|rGz2#axB$5}d22(YwuseB#6` z?w~YZWmKM!E^UxDYDJRaG-u+Hpk$$^hv=k?tlDZsLsIrkO`grad6YGHO53*M=IcI8 z)yS;RJ-DNlt{_=~MkUvEbKaw1)KuSwsh_iN8ZGxJw$b?stQBY&1gIX6`$-Ev*pZ}n5V$Jb?yQeSoqn1;KCcffO;3+8@dr=FE>m-p(tSPJXu z6TZRqRjl?3vxZU-aV-JzYv)jDsFkCdAt(3+r5o=*at36K zs4}CIvNlK0KA0%w)=KWS#2Hh#A=%KE=jfTg#OS0hXba# zfx=N=Zfc0ji|6dBt1hwf52>w8Yve;Ky*}493&IaY0Rv%=BJ=luE=_BkaAGjlbU)LW zG@)qMJ9efOO$aNi!tl`eMMKF>G*e&UyasrPG6}j*Gmv4s9H~$0GB>5256$f{tE1tc zJg212QGt0YPFXG$TAXOGFp}2RUUc4DX6~1h9#>IyTBN}m`h!1dmZ*b=%D#raxhR%~ z!hZJVLgTl8f?(!yNIt#?9`LSC(*Fb4NgN_)froz)nis{)-Yn8(2~yfTxG}ZqbQsFb z`vgtSY1xxcX15`L2{=>OHf<2mcfAf)n@PN_2veA+i)v=8c>#Y@l)(e?6GIMw(StAr zg|nqBnLM##5)aj(aaonIIQyCU^+|yr^FDLf5AHuCafPCVV%B(#FTf8MVQEGyH@GV0CaCm%HnXF0!>Ea~1jhgzlIR|>LZeokkv z{JN1(RJIYiE_I!~_plq&6ATrNR;;{k;B0R03?JQ=-t17mFmg^V+^JVzOa>^^qw+JK zfPCm0z7ELV9xHwjps=V#?J)N)8sX9CuMgD2TN>F8fS31Zx~*RHSt?b9V8iRt-ax)# zYDosEScQ~K;KuCXzGAv1ais^FTAJwRMR69|PaGe8`5bTyTBVsz2UnlK0cF%3ua6%* z9KG>6;CGAfROTl=Mvk|$@~~0Jb>ws4U_|f2jRvTeSZI+E?cWY$9U2|jup?&2l7qEb z$O)Gm6OAJyQVuZL6q(C8CRK5%PE?(|y050yEmtL((3rhe21$y=cBarm!V1Il-eW|V zG_O+!Mk+hxW{d7kQS`IDPD3M&$Y*Ubg9QIiC!~CcP<|*K>13|MvqzzEnFC6Gge=) z9Q7!q8`q36B-(K)@2E_!ed?B3B{))5t!lW_;Z|`3z9`QhYDZI&1a|9KD@#(w%`Ma| zERB^`4ZS6*FyzdU*Dn;;Re@h4R$^4}Cd4@8!5RJipMMs%H1ar{vo= zk81bw8Rc}Wy}53@u*Yt1g|>}+U;9}fbdW5E*){HP*OPIzD`BlVu%$&m}$ z+PV3ejvh!`GxIj=7wYf>LO0i2?*yE)D8xflkQ}QeW+puna*r`)7rLq=!bZ!np{#l! zq6u#peK)g^%~|aJfslLn z021fcU*Qa_DBOG}1|dyZkUS0sNBf+E&gu406~Ai7zf!S6Rxf^%9u+&YgoS!SE&0Dvmx*d&$PhOqo_kw@;d(SHUr-xS|+n!jeMywFiu0Kgaq zt#%HOM9=kSJn%^Qnw(5~dpuyU+BRjjNY~aFO|MWWsK(ju(sCC)KcGx!Zk?MbZLaI9 z%sFPCQ9Kgy9+zb^$}$mS6%ZS)0AKTfvu)+8_l7;wf^2LgOX;%fe=&sVZ3c1ozKEROY{<80CgpZR_^ zk&`K>{HS|lYC3o_xEYeDudm=4b&E-t@;l8_Cnc!g8Ca0IARI<6g@HXSf^h{C^ zczJhjNGKMk$1pRJ;CI&d?;-Wyp`2l!u-6!VrRtt)bFwVG^P`<6;7G7h~h%N%vo<~d66m|8wQF`Qge$r z?j)16-B~3I*fi;Gbf*DDO;$G_MmzZIP5A5tJ(TdPYAByuD0y+#Uc7=mGqR zR_~%3vG|o}VX|zhGB0t7S1${vE&;1OL+tsli_uhxi_y}%Yq$-Zp*l0;hT@4FfKSwb zF_m1`P{13a9*J9|zy~=svBTFp)LpZE9bso*+y_}T#>|97qaCD+UBGtiE44j&gS;JF zbD1H{(-rLboE*+l(JtK;qG0c*T2+w424yfWvT_}KVOZYh-}Y#-09gm?m_R@sT>nwv z^?&!&|Mtp8O(-v=#mBEHBQje??hz3XeOv?*5+;HG3Me`Rxd8EBeFk<}B=8t%69Z{b zl9bNFjVr>I@)gRKWhzxlP;wjPs8x$gUzVDd6-yRu724KSXb0b$?^{wP3538t*5-FQ zTkX#qj?=DFu2Zc{T{oky*+78)L#7=hrKUm7;*d4Ns-=Y(Ij^c@SW#8k%do}};-!Vx z%ZjQtHRgC&;Y29>jT?*+HvJxIiZ+S{I%XE6y%XUobaiWS{;vx==1gwkI8_dJcVI6J zqaJ1k3_-B@H}&FCiR#$FWx86VEUgI*goq~5%9ft_+2jURNC_;D8s&;Y^fU^Q@Z6RB z^vO}VxpEj`qN=>6#sEqCa z5){3Ga?yVNx#5(lRH;5F?I;*1>j;t6Xi2IjZsLxNQ>TqE230dk?p(i_A1FCQ^e z2O|9HKb^^HaDq4|m;l=Yr|g1&!r>z96Cp6dOuM7`WEHGZ@*N(-qk;XrXp3BUm>u~# zEGg!E@f|qwDz`8QEb-SMOGtrh8smzuGIC62|j)Bim`X zFjRt3XN}KKSG1*J(#(_*tsK_o>_P{D&Iy34gxU@ChmK6C-VH-HIFIZ~f<@_!`LU@dlLU2QP~Uj(_6&L{BLHE} zBuFxG^dA)l_94twIRu!m)Z}DjKR!}SZ1_Y;l(mMJ)Ndbe?Gg@3`?>Uq$_&=k@FPUc zQH3kdr7C|QD$La`HD>=A-;;sVxeEN>~1Gj7aHCR-MJh0}Z;(yFhC!M{zVwDLDb z<*pV`ZJu`-wQP2vkyZDo0%q#<;MEmcK_r9Aa2rU~5lbEwG_Vt=-ng?QyHXzD z1>I`YZD3Ws5@no=JxwxEue62 z>0pYO22*S${V1lL;x#_dztY?3GGr(f28GMXS}`M9LFG7kPVH0V5GY!j*<>gHwPl`< z-A>WMlX(J5y1ryKdjI$?2&I#@-u*OVo#|%^UjJKJtts$a_`Ss)zp&FSlV!)iGbbt8 zA@1U|3E65ekt|!`<&dF(awdoM7Pf2HfDm&&%I$)%XF5zuSq5uG)EmBCV_rmo6uW|j zKAT*!5YqucN3P)$6b)Kk62r~hZ-{?16VObJW8S<6;Ug5l&ghL|>v?N9w~+Z7`H3aq zJ4O!o5hQ|Z_E?0?MY&rN1rd{(FDg^3Kwn&%(%Um_KJS0PJlwWto%8_XAxAI%R6&U~ zk>udqME-I^Mh!i}Nj*}fY>l1ToP!v0=*J!-sq99>AHm@=8LyV~gh=X+gy}jLpJIC_ zMrhIXDmaPf$Go7r#O+-Ztj5l93DOQH6(vu=fMuwVuYOkVCGY=3**OJi0;FrYx~i+o zMwe~dwr$(`%eHOXwr$(CZDZ$boY{?;i!-~InRglSG4p$$q#qoKjpYxKF`R%wmumC~ z@jqzgq2Ifch|?37r99KMt;28lXk^32PmhE@uO0VFw^Pp1HFUAgaz>r(AK2#R&$A<^ zFH#Cd+>cxxT=tWc! z&+gNKsE@_h1u}5<^_d2vWyP5!4)3yWPIb}Kp+%9&91jvq!TI=8OaW#U?pa<-=ChUG zNPT&rOB7D$QfZ_QVTDTplHu8)4)(_Vg!gPDpzHzS{HUd{#V(LqXGuZM8sl-r2W#l- zlrWbTN=|VC!?p|g2U5T5#}b9>Q{{z0o-7_nu+q^OKFT=M4I~=u#a#SRy4-6cVUnqC z7QzcNICCk_PtxtPGiR)eJ{v}uWwF#O zuKm?%kfFm&gA(zpzHl)tzQwDq9Lb_---w9LLj|e4fbE#Taw)i=)vd=dmJmL`^BMx< zz2e9%iTisg~MeB@QY!MA}Lm*!EGT9UI!k-U}s{ z(d)MpVe0FCewV4r+0oPqTu~rGl^Tb)$fBWzV_hC5NpD&)pqn?ZCNz>JvSdiVMQZg+ z&%mD1P(@ZhaMxa=(<> zbV+cnJoy-GPBd~~b#C%uoU@-^K@Xw7CfFZli8~9{;JwI>^SQ@Y?;L7u?j>QC2~F=S zwq%A-adzFi^&RRk7o^8`s6CGV1%0rxug4rIpk1>u;lj4=ejcZOq^2aSut~Bm-lS-m zYca@bwmte&sdG22D%B=~Un%ciDJW1-)^|W)8_# zvtC8yC%gE{4U3G=kSJsng!s0$TyZeKQ{F_KY}*UPB>3UskruyoST$jO}tdFUOs_4XeeF- z$(EVs2`|{i3fXQdxy{jAnu~jh^KxfRJ6qKLYgqyViPQULi7#8o13vex(@uh;XLDyZ9&VFgX!0sl8ZzX9b`l#+!<|VXK@rP;sG7EhAl{QU?orE#8*Rr9KLX_mO zPBe#`Tr%4c4fG7Z7}f<*RANAQ?zo!eiH})0L{&urAiP~^;eg1#Vm9j4Ijvf>$8)l% zrH;}oj4mDM@XWxp!VWJ<&w0QR@TWw^CaXu9xw~YP`ldlQD{ApCgu;m>cL0<}l0;&* z1rZi4rQAF=1gTLB57CB+2@)uOUUJ-8I9PNcZGPM8r)1ZAV980SoEMd41v6OzoI$%J z8|#TOPEvcw8q#U)m=!qUbjgrP`BC(D%QCMPDff#{OD-aBA!9|DzT7jI0q>RU70}K* zo08Lnn+Z{cH%r#y4V%C0$k=@9H>`gPbr$2K@}4V!uppL6$M~84Q5c~E(kiks@N{L2 zNR_KL(O|!#S03I)2d<+Z7L#e~tk0cyDO@H@ilp77eqzn0rtI&GG+evcugnFGVTt#I zKT8wFe{w``d=(ip5MJ}Tfk6)q13e{if=6M$kNWS(PEa349oNi=}5jAjuvFrCAg^K#r*i{Ezuk*Uj=mptz?mvLS72t|Ii%m7<0<&D0d+d zma~~n%0#Y6O0xRI`1S3K$Tv|S8(;)0f0CxvF;(Rb_fIY^sTj8B-;P-gL3AQ7!|DK2wy_eG9|I7Nq)d)_U|nye zqI;TfdNN<{GNN3Qy2ustTbq>Dr0)@VO-;}%$ z#`dW59oAYMBsGP!*hIPJRx8v2UEEUWQ)KHHM%s)>qE<%9w#lsU))q@zf7VNaqHM_FColUy%Dr+3p_&aoO*jctA?@mLE^ zy~&T9ufG1f3ndnBxWnY)rZ0<0gj&Ek5f zf8`$~YguvAC-N7e+!B&!gxry?&wVLUF`vl2) zvW(_uN)bs~#)qI?p$vjLzoH_rkRE*JT3(??R~zT+onFAY5Ja0dp)-!II%{1cK-dgF zTLHkSKAOCkk7Y5a?IhwY6pZfUdW&NK8j(#&aVwERmC2y8%AkTN-sgv$WZY#mqS6&W!+>4=5$&Ln&}>a0ND69MMP+e5nAc(1j&BD1 z(#7dY3g9@m5zb53-P;bbB&W4@sI<22nC@9u~III~WVa0lM$Xa&|N&a>LFU&{^vR4`z9lR53AM71%Z zsmb#dT6%eizgHxFa{xR+R`#T;A6Qh~BPhRMU3OoD-r%$zxU1i3 zaQZ$lE|kwZYS3tSlX4L1)X@ZWzSx^;<)~`LX2{JHWLq%R-Ui{ z;vW44_rU|kF%~(wwg+6A_0PU_bQ82pwbs07E}A2gM^%ZYpCqfc#rivEz234I&o@{Y z&m-=A)qd^j<@{@!eKUuSq0J4Mc@u8@?{XS@VKs_=; zz~4N1i8{6Ly1*4l`nyQE0N#i$W$g)mOHF3oage@U~_ zH*=%uKBBj#&Y_MeDTtmqc6+LIEYB9b#9>uswwFRchv>GmUB}2Ub(Wiz`As1qThAw+ z*Wi_1N&ZY#p{$jIPo%HE9WwPD-It``DP+8#lFm=y=Ua(2*)q##gEr8Ylc*ExH~m8N zR1DX8wXx?nuC-?OOvV}cLt@*_@Pa~uuJ-X6s*W-nriDbAYbHC6hfI)uz}-st=SeR> z^ErG@A+ARf(jszhgHB$0ozi5223>(7XiEWWSGFaQ{bcV=`_*o;G3@&ooyYl`a@VPn=M37g*?!N{dP|Hqp3M zNM#q29rI=U%~gn|*U%ncCl+1@6{;tmj_{MfQ!EXPC_eg$gtQ;#e381RNcE7>#j4lq zp7RL?>l}d3-CMyX>P_o2i?U2=F8e0L=x3JGOsK`J$Q2h7=c<{`KJc7mew&2d%N7?v zbq;+*_v#BvNI;&0Zh1KZ2u3?9!rfbqX^WNgT0Ab?R{zvY>V_(JnK&!QAP(M+S8XBe zQ94IYw$ZM`K)J?<3D^31HvW2^j8~=Lirljx4Sb-yNAKq@Iz}!Q8a2WAVT4YfK#NzM zB~mr$)QU1Z!dM)Ifu5RSz}*RDJakyMgR;_|H&Mg$F$xXyPGA@Sc2mvZ-}(~ASswK1 z>>S~QgL?MF4u3oAtvBvGNIWwo*ty7(e7GFZ`{o(Ne_J=AB}-V#`;+qb;O!?stLkQg z>!Zls;AHmYjZxc#&G=#}v#`}&3uUXj&iv3CjfbICbQ)3Rm!Lo4%cue>1H-evMMixB zjouN<`6mj;8Fpb+2Lt3Pz+5vjK@+@veU5De_02J#*A%7pIRTf`yccQ?JvW+SneMli zjPcdnw2GE8HuyCn>Y^W)ih#Y0tTeZfYVQkDyKTN0j@SxCW}-?78;mpnBi-N)>gN72 zE0_c%*Z}O$hK0|OY(%A_QhOiyv#4sxf{@(DR_wV#8=e{O+T4VJ$6mq+`7?yVl_EMS zW|j)TQDbP%97S}4!i0>4V}SP_uK)|?XE-O!zB1rKr35=$r3)^gVvM(4&glj(MY)8{ za#aVSsZ_-=rIGp1tE+NLphqN!fU&wWyWpYQl8M~KUwLxcJvLN8nR9D=+`Y?N$g-n` zJMIFL`6`BcENh^k_IU@X3~)~zzv1ccZOxXkXq%RM+M*bmBuo&QyiF9l^TWUC{zdb3 zq6X`%mYYTMV}q5=jiI#r9mHeL4N5#snmpW$Ibj+!T_^*sbNyaq3Mr7X>fbnR1TkGN ztr#&`%+)PWpv~p?>PhY5=o@f41!V2$DSHxa%YF6YS)8$0A+Z!tN6Z>WDME)Z^#d7^ zn=P|cwg+1KhIL%h;~lM$%WVqVhW_5TtVo5{o!bC6K*+x|KSufAv0?8!=BVs{k%;ry z`|>BHBQrfoj_eig!91IRD2dA33WDQ*#+=-VEk^dqY3oG04hEOdTtoD3y^(7xd3;eb z@J1(Eq6AxC~_ubru!-nJ9%8}zx(kM8p=B& zAq5*aOrTYYD4eQVfGuGgLY!8B*UARL7m# zM-7{-plO|B#9U~P?2@bqZC;lTc2$qLhBDwdtu?8TF8Z)1jORM3QU1)MH$pWyE*bB- zV--NmO?+Dh!kz=GNM+2oIhtZjX0;n2d6g)HKvrj^!{4jnS~=L8Lt1zQxr}xC5jNRe?=@BB9LrF4xPPv6=~)b77QPtthvjE*N>7KhBK1 z;L1JM#79SHQ9+4GBfDYOwki4%(Ji@V*Je0?IN5Mw5dq`p zk!QmVoo?Q57TuYG5^EiomsmqppI8-4dgHfA{mqlge_}N`3dQxnM>jK_ZgsL1o_u}s zddIp28}P@e;ukZH^EHl>83WqVC9E%yC7+YEk;_(SWdrTXnq7xR>Fy+GbM(2x3fouhx4k56|<=3dFa!%85%F zF54as9Cf$!()|3Du+Zzn5q7s@s;Af$`y^4bLVvKUhADsUdudjj$?4f@SOaWfzYBw^ zhOFsv>o0|~N?)3{IvKMcefr#VG+d>9wq8D+G3a-Q9W12|^85V@8bDbAao%<>aSwA@eH6;pF{i(t2JlL7GvZM*rHimkwoRtpc%Ecz;qvx)g4q3Skq(4sfe+h*HbFyu zezmk=-`uLhxZ<9RgJ!&Fww!H82l(iUWw<|gHeZ2X2;+SQi0H0obL!D?7zIiy z9hbxwh^4bd5QCf;u*Db$FZtwl7aIQldjocVSJ6&d(bdhR*UC86;#|nD`2JgthaMi{ zsc@~-mbc2rS{38Tq^VmxAcsqAi$X}pUbVRu2|ReMxa+_k{q=AsV^I4?^T2RLej9I6 zZ0G{ADEKPeeby4nC!qy956u<(y0O)^!Zk5+R6ApsW>JQmjadvfuo7Arcc*`z=$Zcq z&byP`%{1rrVwEpj0m*@$YxHNOP~wFB48{;9aq@5RwSPLyZ-D(JYkr00wd_i!hHAqZ zsJAFrVx7*!Dlng+^;r}F%pi*V30{??33_mLM~nd2cZfIVsZ}FM?n@bvF#$y#|xav zXhJq3lTpLOrWfwC-Av`jaU*^|!A*7CY&*|WJ_q?IgZ1@b^aNbLc1lOWH-sGUC&1?`v{I% zBQy`HkBW08>Jrau%md{7>giu6m;{>-E8Maz8Fuyc>T#k-ukGhn*~Dl{@J<{^bf7?4 zW_A3lA77t9Ni_vUfW{sV- zqk$8#rDel;HKs_-M6WHZ!>&c3HZo2gc0uNuAf z@_j94>Gs_`@`;2cS6hF(*40~<00##*?+*NpFit*bIe6HhnkKizgO#E(9o>Z6c^y*I z+%heKm6t;(bEsdpY%yFAdU0T|ltR+L<|Yd$D>? z>Ncx1hgH)e9guxoTdd$v=&k;Y#u3Q&N<4v9fE=np>Hs$$B0h{ zGM`^Lu+ZtN5nhlwS0m`ys=0h2yL-N2_*jmYinZ0VDokJ~^_H8Y&~O>9a(KGJ!C>}x zRJ9(XgBkqIP?rg3h0O8S9PB2fgwCFW*Bttc3lxhdoI<@9x+F^=L!#AP7h!gHS^RI#9y_r)1olBEnvJyY58Qo}r3! zW$KGy27sT%Ha$r@+f9t<^*%=ewqz+fK#BiZ0gJ3jb7V=tfnj4VwiHK)0Kew6#X=8% zoi$xNK0Nim+omF z^AjtZ?)HJi`foqTT8Haitnj~B{M#BKd-Q&r`DtePbS+swnJtNe~QC8Uv zPiNMXbH~uaKt7(N3LEKnoDl))q@SRpBv{oh1SJLpnBj7&pRNMNYK8R<~aoMhcsX_gGL%~x`Bfhy3eLzq zQA>srCmS)shY$Bqu8}ycQAQm0@xbs_LKd(+2+B258c}H7qMFWIWOQwR!bx$RYal2%+#5fYt&AC4$B7MUj)vlhP`sWxKkg{Mo7Z9T%O z-pa+=jJ#3_rLsJ(;`{RYeb;%ZLul5A%`rk3n8zyJ4uhlZKAF94-gusMq%aXeavh!n zdsXe^8(93dxF@f1(Jr`1# zws3l280MN+>k1&nwCBx|W5!aEOj^)jjnQlGXDP?FC-UUC`@z&ipuRv2A-60R$-bSf zmV-RM?yj#a?aphH^lmQ|$IOT$khr z7?_PQe*OZjSK^4vhIj-w7gFxC0)+P>H`JGuN#^Lz14Z8A(At)J7!eyb4kd;i!TWPJ zr~2D~Alh^9(A=%mPX`wgj3=Y*iO(|k`Y%VPs>3e3LpJNP?i$-xVkFjkg0?_47dYft z*`C~_wQD_@J>`g9`weoP;0TqGO)!YMx4V8xx6_ihXFmQ*bAf7_SnGqyAr1Iu!}VrO z`U1OkMOTEZq~EO91Z(UuCY&c!Xe`LAbql_mfjKMA?&hS(Hnl-i@Rg7;=<3$3| z?rMl&4_B2QZo$*&CC&{w z=^SZiAjif~x6aEuR9vvz8jz{diWRn}982QH`tR#(h)%pdA7F?FV+bA|LiKV}i;wFR zBX!<4;Uiat-9#JYf-tB(uCgC^#zwr#8E?8s&`x~%XH?_p2xlyhMUxH4JR|EB`k{Cp z&TzgA+va}_ArdwETM!3dNpBds6?tt_JZM*LV%brI__|vhuwrA@+#XOoF-i|7v#_v9 zUJ!ZyGHVN%FA$A~q|@f@6O^-MvpPaN&>HlCb~WfcAgV9lyI>#dai7=nQy)!whJAB-1s{7wWG9@J(%L1Bnv4Y}OzU*vvtUdiNvzTECtjAF{= z@vR$BqouuwOwBlpG}viCIY~z{trPV@tyF_q$$C;|1gBK>?=Mw?7;noT64ay>tFQ8+ zR-S@M=K5MZG)ZdBfj3C%976L|r1f5iAEk0_fnQ|x^ngDE^ZS0oGS3m(TD$~Wa3i7d zFE6GZB<54C}bx<=p*sdT&Sp)p4rzyqo-EJOz@t3wtgn!-6~ zdRd%eG^8b|`c0Zds*17$tkhQkO9j+>YT~CW3ES!$z07qG@=9raYMfE?c`O{>sw=G~ zcHbCL3Ts?`gzLP1y?1<6v1DIFTq$?89JEk=imJD25Yz1uwo^2JxTfpdBU_#V@1K7R zl8q2cLIhH7m2QZ#i=B4XuFIadLR_SGRW%1~ZXjF-)BPCx^S3lFmZU705_as;G6mfv zt6YG4h;1xrEdgIjn#XJsoJSy!>odA*Yr>iu&yT=u2D8_{}(*JmwkjzxELft*}X4URvurty?|FTo=8&wtm-X-XYkC- zKKfNlRy0b3cSrHHa>i*0wyS_OiH77TbEN2!*Q2Jt&U$|H{Jks!m}%QN@eIe+Qhg=u zlT3d5VBY>{g;yKAv9fYRb-^~@!NntuWA|Tw zcMH&YDQ6fWDO>h!l`C@Efk!N{EDg;mK$l)QcB9?ZQQ_e@fUJci!mcA;nT_SAhqSH~ zY2*~G^HI>-V0ZHJH~F!E%;=7=fD~xLbbewacZvX$mb=Ve(<(rWT-)yN@l1+APUKax1 zEO$$t1Lq0Dx)4WsW4njj6tY_dog75Hiz6)xC_};v6s*sUZ#nXw{wTJrz*n%&Z@<4N zlY#8iFr>7y+~DV=FtC78HafqJr&(_+6hLb{Bt`BqKwLOOpbEz+ksp)6p;FKWOopJ8 zYhf0SvExrD!rj7^cYVDqHdTO7TIvx2}RB{sklF`Yl1huJnLO$Q2xQZz+oTxyTcBmDa z5^PA1VQB)oSQM~Qc3Le!MQDtm&rd!?nY3Fb(O)4+D!jkoDNM7L;~UU}!(W_;BgOPc z*U=PJ40FPwh|Zi>kD9+wqnhrCE@F3R>MMC43|)V@N=LyyrT74JMjTa%EG?I|XU0?1 zq(RojS?GIN5Z8P#kANxlTw{*U=KWdJx%Up_e5SYf*11v@^!#CshP~g!;DNhq^PL&E z{rWe&7184=bou`q-s->GZfRpjeItEG{r`)%l7#&w4`0cG)-yPFI4D4r znQK~nz{*H_%nfV02Lwxu!AZGvRxYfn%==&S|K#iV6oK*v4g+OTLx)<~gSg>YW7eLd zgI0&02q&uh?8Y+z;-M7y9QR4m!{?eKqT*b6O-(5baY4c0T{)|-SEP<7!tv@QPwuN5 zOhO5CpFf;=4OEcirs$lcH0J^EyAeRlN08^>$`xj0Ej*GO;iY%~Mk<(QIMnh30Rnmj z`;Vl8e?R%Z+&p9{YujQ9BXb*as++2Fo;5QE#?&J(aL8bi76Eak}wmgD`x zxRHssUhcTQPMy!`GxYgYBTT?RfY^E08)MPNsE)XSppccsmE$ta{W6(#;gkJyN9s%2 zYE2MO>6aPwk5Yb;>yYr%g^MAL#jX*ABxT06p^zlUsM z6w@uo06Dd(>^uLJL-u9mLv@&XXW4JJh`ws3m}m1C0+kJQ;&j73>`YU-wu^8ga$4$T z^z;L=RWwlCG)h`ys2dpfGTLPrEu&V~#6LTM;x!vl2IQR{+6WG3qI>JI)u-kBI1bP* zqFA(r>zparG)1S5MTCSSNBp}){;2zddzy>v*2b!74mb}&wotZxgP@a?ZZ!0n(noVl z8Vtp9;`qUv7Kae+tHrqCwC-8Wp*_b}zABd{LUfp1&r4j|lVjMt`mGEy*G0dWz~j+& zq6BJA#>sS|`l)>~^siRCvYyR0b@l4I(b8 zj(j)@;PVn|4&_RX7M%dpGXxL4zIXltQ)pypuVPQZ=a?Lo#umKn zg&z+S^Ik5oHVEn#Jq2qoLJuK7d2RNQ$Kz~FjOF7cFHJJAD1p+*S0!YZ2mQs`X9e#m z?G=|mh`@ z8bD_B!E9|0mOCfs1?Jh3$c~xG%^&{!o1o_*U_cY}A8w5R_8+yf|K`^JEoV})ltto4 z=2p~brZp=Ig+u0--|$Z)C{<(P_n!%$Gl21?UvTbSy0Y2IV&1Kp7Ydjg#NCZy*j}qH zg2QhdyPocHxnY0FT${e(<@NrhHkbttj5Z4&Nf^ibI7iGE^ zE|X|c$F`4VfTlGkt-WoGCT2P=PXxaZYb54w_J{P?G|45Og1Hb)1eOiQ{)GBYh~8ff zEskv1PGaDUZF-k#;1B^(N1J5hPOr)J9gQSvAv#K$ z#VMhgFzvWDVAsIuVuh}sPsYq!ncA2RdQOUj%?}Cvl~C$-r&dzgHOy=N=7^hL{Lq*N z?YHF?-1hyBmfxv?63v*EqpL^uEV%K&|HOv2I)w{QMdoidW`9-QT0i9c;Y5=gnk}cO1mSaoyG~kHiviTtg@RbNd^chCg$NJriu;xA3n>1hzId-Y4B9zv;YYl8 zJOq+ZIeA${lI_4=V=cdTZQ*ndWY1`da@29}pyxJFrc>gYX6BI3(MQRY#5op!xrs^mvm7wV;J6WgIK|r`v$eF99rq|} zu=&z1h{C=0a-`XPd_L}D90;K`;+rqb)-!a}UTS3-iV0vN)mPeob*a=Qq=f&{$bJ0& zkH54i7(3d#DHuCAS^bkNrX+2P?72Lrk*R5DM1cfB}QO3C7-oc3u2CUvLpn zd+^rpwtf|+fi@Tq#z4c1K>IeE0zhS-OdCC+j{yyyQUp&I5V1`sAVC{2D3p2P3qSS! zpxz7SXEo7t5>}f$fR=!ExYJ>K?7$ec>$pp~bPn{y7WqPzcdsV1Tm=d4NJ{fvViBxI zbp{p8+XFQ)WjhT$1-*GiCTA8%`LM=uJLiHWIm#Ag&ZJ;n`Ymy_ZB@3UZ1X@pw)O4s zEm4tCi)J&;H(Wc5RL{J1%9(T?S*1f#ejJ9IEK)fgYZ){!iZa8sRK2f6Yg2k=G`bSN zC&ZjHQWq+1CCB*VM1amP!f2k~Z}-(hp(l1`RSWxM!&gygBIm)h)<|kzWQWSsad@Sm zwMDtA+j}{QU;o@Bh|FW&%U(|mqej>fN=235>Ps9>6{V*(44KPKJizpJp88qk-rlI$ z6Th*{nY!#jq%1^E+&J%6IA#->#3F7Up+;G#y%c^ySRIznEZ^v8{)e8d6Ck8|8WYD@ zCqf&SNmvBHq8rVlO9B8gi$Pc*8kc{g#>cVcp(} zX|rGG*g1~FtUuVM$TM3+&E||KnRoaZ4l`XRnLF$!F4xz#K3}&;zKpL5{78aCrYHwo zw#LGpqgNaKOH%@!X2wo}gNkqdVj9H<_6tt_w|zCa!a@E(1J%UclLvoF7LeA6;~da6 zXikf-qv@DL-%U||qjOMq!0XymG4dp*iK$M5&O!Q6I_s7AJEL^W)^UeMPH5B9Xl0+! zn3~b542gGJsBz8IYFfe*Bk++1$eO_S+5}@e*#NIj(4`(0o44shaA>t)L#0qmkGPRn za_To&p#|_i>gD)T)~eO*aOqWmY*v-B#%|F$^B7ZfeDZZeQ%tMs<%UHC;VD}*O_O4 zwy)MS$%jFBqzo-*^FtFcfG@>X=(&6w(x!MMV(}~>6SREs3ai5bdtf&n-HcYc6t#hB zll|p`VzAQ9mX?;?dYqoxQyBz#y0+$^B81!ha5YR5JWosTowjW(v zH43$@@tY)6{rB$MIaL|P=RIsYIwSL?K!C;xOJxUqye?ig^n2~tLJv@?q&<{*cQlWH z!y~!I32xq%M=7bduqYYwIpmf3H$wNw>Gv}^?7WWa=O7Z6iL3ts^qC(inx3mK8bk@Y z_wa0O%}7hIYVa5Lw=VqoU5Jk6z)+i7j1Np%TgZ%eOqKrQFK3aS01S+hp}|c-$C56w z1)gxG9j@BowMb~ORVcL~?RF$D*A%*FdDvcj4QxJ6Wdw$?jF~fAHuk2JVk?;(_O-o_o*7e&5M`|=AWg+bAN)h zfdr~Va7ErMcQ}@%8T=}V^zm#-g|tH*vKerN1b=Ku>RVC{6f7k=wpXxp4jS^*^Zxpk zke9PIfzeJGc$woj$kBDK;V9cqMurNC)qT+NQVQ?u*&7hqcF9%#EZgCbSMZ$K zHd*oa%x_)dGpOI+fBrS%=28hc-v0pt+JglGV*XDN*Vf+npQ2+qeK#vxeWQPVY;Ea) zw1Vy(O>AjgH(bl|CxP#;oj=h=X{;Pp1_!($pTO`RzqY0#%QL*?WM^gbt5x&$Se*r; zt_2oy-SxC-qvDpl1%gzQ^_r}+67L(`GbXR;Y9nZ?Tsp&>wyv9p?Hz~5nV;{c8z7xw zY8Y(bv3$)bHDVVXlX4X=dG}_;X{9Z<4gyBSqWVn7tYA}i9YQ}c4(35evrs52joRcY zsUd==$xnYOci_5#Wy@j6g!m4%If^(4ptLJnk zgT2@YoyhO(1WM+P?()!5{U+4eCTUc57<7&6<%zVQ;0q)BptJ@4Sj5#5e^)-eeF(rz z9H16Ev9HYd%$1b7L@6%7yLJCmMGef!6E>t&&pHf(F58ny<1OUDh${(B-ij-F3J^&j z=KnebP{Tt=b(_2|L}bYe?4pQ;MM!`WU9wB;^Vl`AC?d1WzTyB#K?)^R0i+-vgNpRA zNPj=$wwxx#pS79P(Ue7=X{QteGimEax>a4POlkBom~{7Qu=`g=W697%N1AL?YoL6< zsHZ1K1uL7-JT@U&bRj@U9W(}`V>hr7*1pi=Ta3QfYbQoYZBsLbNZ5Oa8GhgWdR@$- z#FtNxD8Qq`wD6c%?6qlTy)hIeZa zdX7&MzNvSkfR^H<)%Q1*)+Wm7ybRGRkt1W=;x)c^_=D7Kh1`V#zR~yC3AvAB5xfv~ zAkMx_3v1;!4bK|x)lmCybe6Tr$#}{$=oQAJ@U2^^4fPq!5huTinb2#OKGHo}a4k2H z&s-8*TE3_L@MsFG_jy&bu2L7i`F^umccwyTh9AO1t*>58TAL0G8+`-SO=9Ym5XJQ#g1zCtM5-m9pwY7`=KU} z0@}-Lz4DDH@})_mv76(RGONCZ@Q4MKDq@`%ttE$zl(S_cS5ZR`(z|9)SJ?xAfj;(l zvTWaKsX>>{=9m$g@i}@Xe!WtD5^~_&Z5Nmh)Rlis-K%UFCC{`trCvF{)JL1hQi6V% zU{9Qh#g$HOsegwO?f5~n{XSo+%m*?wcBT`SdQPQURIruVYgzB@!loaFH%aZu&u*^p ziyIF-n%8nha!f3kN>(?()1tHlZFX!$5*rhLcDEd4M6a|3^(5FG>y0o+=o?)Z2J0ZR z?r?Mu-~@$W07Q7!5n32>olD8>3C4XE$4Pgc=R?H-zqQCcKlmVhDFY{z;d%!L9Yr?5z0V8~0QV!vN6_**gHl_QNLK8Xb8=+!JJ{5X)GDmV*ahDkL~?_(RH!ZC*Cs zc5lNkZ(o;5iEs~h;m3E6(u+l)$jf?RSW-H3q9jTlcES7r<-hStj1)M+dNHt`=}Ph9RJGMAfm~rQ*g?> z8(X27ooO0r4bqLY_f9^D?XI@_xmV|azbjnN-lfZH-pw}#3%rjhrLN0e5YC}x`VPHk zhz9oIkG_d*;zgBrs=@e-w;f-buGL++dT)ivubOnw&K9#V7xdLs-07`Zcge^-(#k(P zOE2cGAwK@Z`e@oWq$VXk9kArT<9pyKL|fl51id^Nn68m7#Do{)%;fCPxYQ^y)@_Y?Q*XY$(CkL~{#d52fDNw4y6^fdZ%Bza2l>037iZm=G zxl4XTejvHYzP2YXO;un%o;DSSW95m ze)Cfhg!;B9;7m-V8S1bzl-^6<*QEmH_gp~S${*y;MI&aPrD z0~p$o7y6PkbsGJEspZV0y+9O};rOv;c#B#k{;F^eDkL>8M$E``L11ElmtJJfT<|G$ zHa+JMXoii+pNB3x!>ZY;n}}Y-*O6h?!9Jx%fjAgkEZ3vQ*~mK^nJ7n%pd&9Q%PyiV4hu zF?faX9SamycG^a=gn7bTpmE4-V7z@Je*Ufp#qutWL2L^#30*buh@)g~Grmg)JBrG5 zwnm9wSNKOQzg}2Q93P$j1$k5$_~N14`Tg)&33djul}a6`RdM+m3GjHW$4b|g{jkJNUm7EU>K_VW zwV4M1a9L&S@h_mQC_1ZQ1WA+TV?94?;B;q5wEl%FJ01D8KYSmV9$>@le32~ti(F_Y zK+@kyZU|r8LcRYy6HU7Cu@%cpZHRC_&FUX@1J#JA@+T0++0yuHGJc?S)eiUo&Oh-^(MVpX|Z3kWAaFRUgDB zmzMn;>fy8%uAruALK3+2BVh_%x=YhIko(Gh9uEg=13HtprK)h=1;@W?bHD+t> zJojB}Ygch(YDHKlRR=mp5Nx`rGB^Y3lUvwN@)-F>l3;?s$lyZfA_uF}S{xz4hs&bg zBS=dqO|g3?J)iTaeo<`x9U7Xc-DJUCY~kFLYG}F#OKFBOXl8MZ?CVk*HweQQY#1S0`U)ES?qN|QFJevph#g6wQWfAf zH`R)oln3@5csV-=~s|^ zFbdm@Eps)qcLeLNlWXdAi0!VZ9Y8cs1n_-RETL)!kI0&8X?b~Rd8G!{EA&u`JT=Ye zT=|jtrh1^6gUS2#Ts*;}EEWd4ry`O3{m zKl!5v=k+@0d7kr(bD#6P%LylMuoihY`8d!-Wi48JPHy?bg@UqGl%o{^K=bhRc`R; z(J=jNlzNbbi?z{%Mon)5aoh^oQu>Y&vKM0m0xu>;R^5dLd0}G&8J{XTb?k7zk8lah zBa1ZKE;BiXCa@qEU6`W|WSo3E@!_`T(`K*Y`xL=p-UB0~;V=j0{rJ;~`$|b+gE14DqI0yeVvS@dk6;9rPbmzH<|}UAFPGtD7{|7pchAA)-<^ zfghaosWJ^NEildQna$7npdn~fRo1Cqa;o2f>O_$*c3{(T$%Q&lAkc_7xuQel-MmsErjA$)}e)V znaRuWnmd*tdnFDEffTz}%~!m+CW9cbVo1w5-)S(*g5km&l$-Qs-OeXwbUg#`WWC!z z#>uq{C`@+^hhKKUp-zIGf)0~?ig@D6$eL5gu}p6Ig;O=t_nMxI!lp&*GBbnjwX(M; zrz>ujz8fYM*s5c^jwK|ZGukXl!XGC$F{K5+Ea`9u}B4+fb-+L@i$8N zfvwgdIdAjEqvD-oCvTJTH9?J0s+GIqZ}b^qFl@e}F20bmTHOJo3>(gFrw^@j0{m$i zoHi5W{FV~jiJPASHfWM18S=}m*SF{_wlMdOcS7{$Se?;9pk_i4i1zUD&TrxV@-%he zsv5+b%YBL*{9r?v7m|1?&U-K=R-`uGnZG=k|bmWU-di7T%t=5-WeNhE%JzD~i6Bzqe!;ziJs zzp)Y$$+al)2J1p$x;#4S1ytmM+E{nrCoGp=-|8*>BoX)hEwv9jvjMn}>(aC2pj&;_ zJzBvJUYFZa;Zx{&MNEmh$}7=M!S503%zL2fr~v~NWQBr{d@qw}mnfM@G3cTVX4821 zDl7{n=Ix33HL0+pyXUHa0Zq+mr~&v?k4b^VbBECxh1aKx;8B4rLe=5ixQ4>kc$wTP z#RkuE6FCfN;m{VYFiw4VJv?136K~zntJpvT9a5jA@pMEHSNe7enyb)r(lalD#?$9d zEz?hGWRsIOjFuE#*63@I;)+QL3;p2RCCFr70`jr0&?PSyL%pp^^6vHt225j<45@^x z#G>f^CruQ|BTDEhsnDAY@{6ulgp_~lD~=1${5<5_bWg>F_yv0I7~dTW?FSNIr4#R{ zYddN2y>jV=N6eU9;rAqj5{y*4>|&sVNgZFK1KF$jEvxtUIDDMk z~IWyoiB{8h0TD;7s!cz0OJvl!-w0u}AFfP5M=s8s; zP2>BEN>4I|;(QGk%AmA~Id4OwmXaQQdhn_Gg%HE_CXDHVIn+=gb`jWY?vgG)Z8^3* zb~?peVT-W2T6w$^uWw9m8Zog6dmnS(=V!{Ajav7%$UiJB<0YBd6)xH}eqy|KUJeX( z!j&v#gV$SSKM=Gg(|hj5Ut40{5$P&9Bxe*bgq!`o4Xb&I>5+Xh=Hso`A5>qV7BO3s z5_EFjm(DNC-6W#cV>dQcG4K9jFW_RP^+J2XX^zdu@Z}e#l4+Kzh~P5=*w0;7&WtLA z3xLgWW6iESPOZLI5hF`bvXPbi?%M|s)A7I`ZC__wr@zR_C;8`+77KN~87F=%_0Wj< zI)UzbKdz>?UA2qCe682X6}{1rC)Y@1VBqTX!Ollr8gkAYD!+tV~`t9{j4$<>y znULu6q!Q=U5@*AD2WOp(r|V33-FUi9cS+#m{KdS@t>SLc&bLO!U*uF4sYNR)*;-C5 zKK+vGuhZ#S{)pZiUVW3)roKwi7d+sR7u~KurU@MP zzIe(1e$<`eb2)r^Lb2`n$f>DcYpGI|hE;gD2@9_U_Qk;!EyPmPjXs$aomMl*Y1?UxN}UQAFABW%?%O4 zS=9}d$d*%>w!dlJf}KuhysydVu`kM{7?wo6BcfFLq#}oBnDyfw6bW6)YJ3SH!nE5i z!s5?^A52#WrFk)Rh!R-bQZ6gsaHDyOh7qq$D7rxv|INO|n}H=K1wUA%6NUn{o4Q>% zQGTJ2x1qtZPa!tx7?Ky+CoMCKQb|(^CPJZT2O zVvl{NiCq%|r;E|!C}>jVtevOi$c@8aBvD%kaiNQ&%Y?c%_q`mRj4FB^wnoCE<9NgcD1YT+2=k?$LWFFux8JAhBWmSF6J$~#Bv*9`gw*v znTwheDi)SD6}2^P-&xlF=T?BHrcY!n~eT( z*(Yn+f|>^zLkz31>G%zn&ah!pTZ1k0)aZvE{T-P|jGAo38_NliWvug4Bv(tNc}m5| znilT31Vm=GEURfNg$`g^ab^yQ>6!%Uzk|MMI@2?kO;D;dmZ`jCX|gDEVe2?YOgUV=vcl8KH1wn@6Z#%`kGXOBTOI6@T@s;h1QJpqwA~jrUgZY zd;KzNIuBzMbo%;;#91oyb!YkICR*SDk%ol$$z zo~|6$EqCLU&xeYKjzi2;Kj;(JiH*y|6#7%fSk2ciXm16sZ$5mu9{F%R_~Clo!*wtD ztx#?|l&Y1mXH_gxVeRoqk9j866q?9+`3J(r+_>t0h(<2ByF99Q%~p>amaU(L#BNZ8 z3ub$slY_k|6Yr{tKBIo!BU0)ckwrt+tVnrx6t&6cYad2=r@>`lb?H)}#W~|yEjMQN zrB%!)n{}_t?8fJ+H_j~gcs?YN?%;NlaSe81x@l|NdQ;L^UqgLx9?l4dR=hE&bnEp0 zVXqJA9pIzp&oI=a=#7XnErvNLS@x<;M)s?;d!~GdF@_X++q`?U9|7W)Y-o(pH}Uf9($oC701B$u_;N&%7C}%J;ofj z;&raM(glQI1J{%bvMrH7ASz@Ki2d-f8^pv6!uoSDj%+*$Fk zcCY54on@mPBQ`=smiE5D;fI4JXDR#DvxviP=*;{TVw-fae5mQTpuJgBOwp6;bL5~5 z+i=lSLqt?E67g&%wCjGB1O->Ls~+9)s(Gp_r{l_tH5Aqk#j^8X@Z#)TX<_%{8Ji88 z2S+H8s%VR!s|szwus6rP8@zGx3Rdg2BHQZ_gELt4xMpMpB`gKwRPs3GA)FR|k^R$4 zv94d$Gb7g=HZmGQxHn(r3P%Zjvoj$IuGPct>-2lQI+C$Eixp7m-gt3Iu*8%<7~4OZ zk*UHgH&GR@VsTV?kBbD);!h2PYW1jWTp7k?pGG-b!J;1WR7r~lgHq;EWsi4z^^DvjnkMG2 zv@fFvWX>1Jv?8M=nAsR)iq)RJCriNqCo;joGdQy*#(UcPd6muHlDie?MP~w}iP4XI zh1S``npMgMW^D+ELty{C^BrleazPERW{cO?>Qudovqg3%fkoKa8-eOpGD#_-AO(=j zc{1&nv7ZU@8)r>MTm)2#&1CnvsZjKdGKdn}Ejg#FIXcF`O%zh`T)? zJn>o2V{0w-2~wXoOGOpiy2KCJ0JS3hKpJZue%Tp(GLVmk6t5drsV{WeEi|5yr1pE| zeKK?;;*t>JRh|6Gpo&cUM)g7nXOJKDRM)br9@(6v2pSlxjz~O&(plY4OhA?4LJ#L` zbsr%CHHQ?tlyV9+nfVF}{kWJ_&b$QUw?bb`toQ^-HC`vf4fe~~Pb{6CS5OmNw}z9@ zYXGH%4g!iasS;X1dY3LuFoY&GgeDzCdR3{S1d*b2k=|SAU5bJfsS1WBgc9=e&zWz| zobU4NJ!|&5*zYrIue*KoF4T_ECl&$M?5vHZWYpo;=5ht-pKpU50sTq2y**yL34zr) z%sD?xH{sbYMBcC{Nh~^>U_*?5ri_?L6J~=%u%{o^9V~h3PpR)Jr&v5LeU~Yu^TcW9 z_4H6`gF05AIG23K_(Mq#b6GDh_1pF23XecfpL8Q@%~+8E!%Ts!kmjhnz`2-^i?a&|CeU?Sv8M^!Ns(^GNxe!UIesdou~yKlZ&_a7$7Z>`*a zTkz#U&#UlueQLYafXVbMx4XDkp`sIPpl1KxZBGVaYNPISp5(H-%R;e|QGa3uf*Qj~ z$-s&;lc2=3132yMid`6FlFFDXiM$5TP`2|k#m`3~Y-VHNQFFG%v8B@`x~b>sOYM9M z8WzZ*52V%&-GI?67~Ev%Feh1^?G0Mi{|;fjRSw@ZK$Q=x51qXK!l?Y&ylu+^O8+Nx zvOdO6`&})!ERipKMB0{LG06UE!-Uc@qCvW$d(i20N_TXWjG?X!fe0@FYT*LcZbrc1&?2E?UuA;N@O>Vc52&nCDZm zL}nBt5<_0FUI%Vn_kuI4SjU(-tvI!oNAe?z9hsrenfWhQF}pX?dvhlfTIMSgJRk+C z2TnPs`!#*^kvbG7!=q8&?#*mxCbBDNffAH?l-|KW@N$FxEO}RB64mReg&R{HNqs+V zj)7=71c}5ZKyUt;p|b^ zJptq0DeR2VD#QmTq}*kFvZmRuQbF&WkfWpVK}fDhlu13N+e-Mg*&D1>-b>R6o#7iK zrWEf(AKg(DmkHDQtu(51mT7*cCEnMiwKRC5b@LVM2L?yM^Z;E#>U?aFZ}L{otT}>D zDujc;Q!F;wrtV&bP25gL7$~$(kKa)%ZS^zrJe%|ON!xc_)}UtE)@V8ZK&@2~nX*{; zq@~OF#lpO*y?0jrrjD592*b8hh+syOHTKr4_^i%H_<>4_CQ>^+Xb!8qj%&`*z2DRM zU;WX2FB2_z3))TZCcN>Y#>|Di3M{zafw;k(+@IAw^9(m2hLqbqIKr;gG`r~~3=%^# zw~m~(Up$$|Eeo!x3%?Kwq&8n`ZSiD0DC{vT;(6Z_FeFm9y!}DvdNhd$Ka-}NQ!W2d zQ&??9QyGKjMWtMG8GWBc;)s9oH|UXH{C#6<((-2tE1!KtibVAt*L-Ju2NL zmAG}1?8@>;jEiBI;Xsr@Da&$%CBbTjl)^Q$f&iXM#HS38?+umj} zu9CsyIqHU{89BWO-?HE9gD3a*nkx z`F_#TWNvVKn1?6Y2&UXoFG+j+eJ+0ail9z?e3uO@4}y==n+8i!XR2c~n87S0n5$+I8(cKWj8!i}HWCGY0`f zZXI?|)+pzeaKk-Xs*fYy`Ru8fOtB~bD5G|(7kIHxq)Urg&~^ddmh%|NKLrHPNT4v1 zyW`3ol;BT9mG+<@XOdn~<+ztLmW|Zawq37-ygVTSYN(eqkI2GL!(sWry7tAL$Jb92xh-@ zQT(}zUYO`US>nQX`A%wdKbcKVP1C+NgZ&%gq^aN}G>G7`7&HsiYG6CWS9tiza?3558;x`b`{#{8qoJtJBY$JY_NQ=F{n+sZ>l z97!hu$u3`&97q(f2HfU)rq{#0_ps>hs^Lyzg;qhj;BVaV9jCkF=1wCG8uBC{Tg+0B z@aJL9bZAD~(tRz})y;jPZ?QJx0&nOG#77>|tt#kfcJreJ>SZ>Ka4PwrE&x{&>p(i? z%5L9Dr+(FyVu;|X3+?AfPqN?Uel?1vsaoCz)}&L94SD?wmS?W48kD~=9wDD7dGqtK z$a}^7DU|z?s^KO3R$is*5ez)QpHEXRsv?_|HN@<9^J^!qg6*7a91v?8COe8BQQ_O$Bj&VXiG_)r#F2_s^g&eC*Wu;0j$SLu*A8$7P9Z8Y+T z6~re!=E-i49Be&W3AO0l3Ks6V!s*nTG&jL?f#-0WBCeoYcC%qENe$TO9(mq;Z<<^f z_wVS4q>M$rdx=UZYEHq0bxT&4yN>+a-aUj01{G5)L`Sn5u}v1K#2UG_^vV69wkk?q zt40epoE{(%aya%>yoIS8gQ>Y(bn^=*`D5~IeyWOf;RmHTV*qZd3NC0NE zulYEVGn~5XYQHeE66^|S-AsJ~5WM2J>`))K*Jjx*(6N!4Qkeu;))BIgO!&FxYQKMs z-tb5YH<#KWlBZKv{ktg}k%5~`z)Kr^B$3j4 zm0Mm4vrpOVr`xX@<)-D#55WVgIDp2aT5-|^w=ZzD`HRrjRBAQO>eGd{4F!LmZ!bxE zwnN+L!;D(^(EasRdgP$w1D<+$uEUhFg~$6N`m83L+lf14m-hgk3dZ}t(H(N@5u1ei z_e@7kdx9O?@_9FQyO*G;&j~`u_`!_PE7SL|k6aWr>KC{JtwS%#lpZR%g4{gMV1Qv{ zRN&Rtv()-da8S`tRg^^xBMa6Tgcz=l6+Yj=4qW&O&Ktyx6WtNK)!ScazlLp$y|yao zZ!ppwT5g(=`TTl!}EV2NZ+I z5V%7=cPq0MyT4r;<>CvUF>Ba`$CQ&(3mXff#E#dFjwY=6eLpQvN>b0W9Y@@i=g)rz zP0tUZk3%)x$u@32-O+DD+ILU-L5SBD;AmU?FXEbbS# z=U>lHa0;DC+5>qZKJsU_QmlCNHeM!6^Esbq?x}UzKAwi%he}aRX>AtOa(QuyS>t-f z7$=X|v(k%6g)h8IX>^?sZPPj?hjufm#yR#X+WQYOWP$mypZTbH*?bzJZT;Ji>35?~ zlwhXZ?NrU5EKQ*GYZIwp`sMC<=}{L>H)s)9-G-xaSWg=Is)=BwqNcFMP;c9c9z^5t zOm#fn^jUek-9DqK&#|^4QE?hJ9W{vpHTN0t7jCo&In%m5BI$_m=$qVWL;gSS}82*U1#>!A$J}6>ZU3p+6Ee8a4!!r2Tw<&m#d@L6F*mXM=@ul zgQGh_%)tRE;(P$->rJ(?S0RSUi zAm}F1{}$N=;k^?l1OOPa0s$n~MgFsjBLABii=qNi|CV3|jA4u*03d(}0J!nb{p+(R z^3yd)<$oTg@8Id`gz!a)270*v8*l>aZj*8V0<@EW0IL53I1mN^;O-8-z6f7YHwWau kh5zp8KiZ;5gB0NZzrRMhB&7dE5nu0{Kmg$28U_IT2fQiGrvLx| literal 0 HcmV?d00001 diff --git a/code/app/libs/edgeconsent-release-2.0.0.aar b/code/app/libs/edgeconsent-release-2.0.0.aar new file mode 100644 index 0000000000000000000000000000000000000000..cf92d935790ca4d9e20ffa8e66d581f02470b31f GIT binary patch literal 27489 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 literal 0 HcmV?d00001 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 74a87125..e315762a 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 @@ -29,13 +29,9 @@ class EdgeIdentityApplication : Application() { // register AEP SDK extensions MobileCore.setApplication(this) MobileCore.setLogLevel(LoggingMode.VERBOSE) - - Consent.registerExtension() - Identity.registerExtension() - Edge.registerExtension() - Assurance.registerExtension() - - MobileCore.start { + MobileCore.registerExtensions( + listOf(Edge.EXTENSION, Identity.EXTENSION, Consent.EXTENSION, Assurance.EXTENSION) + ) { MobileCore.configureWithAppID(ENVIRONMENT_FILE_ID) } } diff --git a/code/edgeidentity/build.gradle b/code/edgeidentity/build.gradle index 60f72702..1df4984a 100644 --- a/code/edgeidentity/build.gradle +++ b/code/edgeidentity/build.gradle @@ -211,7 +211,7 @@ dependencies { // TODO: Uncomment next line when core 2.0 artifacts are published to maven // implementation "com.adobe.marketing.mobile:core:${rootProject.mavenCoreVersion}" - implementation 'com.github.adobe.aepsdk-core-android:core:dev-v2.0.0-SNAPSHOT' + implementation 'com.adobe.marketing.mobile:core:2.0.0-SNAPSHOT' implementation 'androidx.annotation:annotation:1.3.0' testImplementation "androidx.test.ext:junit:${rootProject.ext.junitVersion}" @@ -225,7 +225,7 @@ dependencies { // TODO: Uncomment next line when Identity 2.0 artifacts are published to maven // implementation "com.adobe.marketing.mobile:identity:2.+" - androidTestImplementation ("com.github.adobe.aepsdk-core-android:identity:dev-v2.0.0-SNAPSHOT") { + androidTestImplementation ('com.adobe.marketing.mobile:identity:2.0.0-SNAPSHOT') { transitive = false } } diff --git a/code/gradle.properties b/code/gradle.properties index 363b6f82..00e42cb4 100644 --- a/code/gradle.properties +++ b/code/gradle.properties @@ -3,7 +3,6 @@ org.gradle.jvmargs=-Xmx2048m android.useAndroidX=true -android.enableJetifier=true android.disableAutomaticComponentCreation=true moduleProjectName=edgeidentity From 796f81b18968efd2309c3d49355678d966e949b7 Mon Sep 17 00:00:00 2001 From: Kevin Lind <40409666+kevinlind@users.noreply.github.com> Date: Thu, 12 Jan 2023 09:37:55 -0800 Subject: [PATCH 24/50] Set pending shared state before updating Identity Map (#81) * Set pending shared state before updating identity map * Create pending state when resetIdentities is called. --- .../edge/identity/IdentityExtension.java | 19 +++++-- .../edge/identity/IdentityExtensionTests.java | 54 +++++++++++++------ 2 files changed, 55 insertions(+), 18 deletions(-) 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 81c7243f..ea699ae7 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 @@ -21,6 +21,7 @@ import com.adobe.marketing.mobile.Extension; import com.adobe.marketing.mobile.ExtensionApi; import com.adobe.marketing.mobile.SharedStateResolution; +import com.adobe.marketing.mobile.SharedStateResolver; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; import com.adobe.marketing.mobile.services.Log; @@ -250,10 +251,14 @@ private void handleUrlVariableResponse( * @param event the edge update identity {@link Event} */ void handleUpdateIdentities(@NonNull final Event event) { + // Add pending shared state to avoid race condition between updating and reading identity map + final SharedStateResolver resolver = getApi().createPendingXDMSharedState(event); + final Map eventData = event.getEventData(); if (eventData == null) { Log.trace(LOG_TAG, LOG_SOURCE, "Cannot update identifiers, event data is null."); + resolver.resolve(state.getIdentityProperties().toXDMData(false)); return; } @@ -265,11 +270,12 @@ void handleUpdateIdentities(@NonNull final Event event) { LOG_SOURCE, "Failed to update identifiers as no identifiers were found in the event data." ); + resolver.resolve(state.getIdentityProperties().toXDMData(false)); return; } state.updateCustomerIdentifiers(map); - shareIdentityXDMSharedState(event); + resolver.resolve(state.getIdentityProperties().toXDMData(false)); } /** @@ -278,10 +284,14 @@ void handleUpdateIdentities(@NonNull final Event event) { * @param event the edge remove identity request {@link Event} */ void handleRemoveIdentity(@NonNull final Event event) { + // Add pending shared state to avoid race condition between updating and reading identity map + final SharedStateResolver resolver = getApi().createPendingXDMSharedState(event); + final Map eventData = event.getEventData(); if (eventData == null) { Log.trace(LOG_TAG, LOG_SOURCE, "Cannot remove identifiers, event data is null."); + resolver.resolve(state.getIdentityProperties().toXDMData(false)); return; } @@ -293,11 +303,12 @@ void handleRemoveIdentity(@NonNull final Event event) { LOG_SOURCE, "Failed to remove identifiers as no identifiers were found in the event data." ); + resolver.resolve(state.getIdentityProperties().toXDMData(false)); return; } state.removeCustomerIdentifiers(map); - shareIdentityXDMSharedState(event); + resolver.resolve(state.getIdentityProperties().toXDMData(false)); } /** @@ -325,8 +336,10 @@ private void handleGetIdentifiersRequest(@NonNull final Event event) { * @param event the identity request reset {@link Event} */ void handleRequestReset(@NonNull final Event event) { + // Add pending shared state to avoid race condition between updating and reading identity map + final SharedStateResolver resolver = getApi().createPendingXDMSharedState(event); state.resetIdentifiers(); - shareIdentityXDMSharedState(event); + resolver.resolve(state.getIdentityProperties().toXDMData(false)); // dispatch reset complete event final Event responseEvent = new Event.Builder( 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 9545273e..cd018e6f 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 @@ -31,6 +31,7 @@ import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.ExtensionApi; import com.adobe.marketing.mobile.SharedStateResolution; +import com.adobe.marketing.mobile.SharedStateResolver; import com.adobe.marketing.mobile.SharedStateResult; import com.adobe.marketing.mobile.SharedStateStatus; import java.util.Collections; @@ -51,6 +52,9 @@ public class IdentityExtensionTests { @Mock ExtensionApi mockExtensionApi; + @Mock + SharedStateResolver mockSharedStateResolver; + @Mock IdentityState mockIdentityState; @@ -660,6 +664,7 @@ public void test_handleUpdateIdentities_whenValidData_updatesCustomerIdentifiers }) .when(mockIdentityState) .updateCustomerIdentifiers(any()); + when(mockExtensionApi.createPendingXDMSharedState(any())).thenReturn(mockSharedStateResolver); extension = new IdentityExtension(mockExtensionApi, mockIdentityState); @@ -682,7 +687,10 @@ public void test_handleUpdateIdentities_whenValidData_updatesCustomerIdentifiers verify(mockIdentityState).updateCustomerIdentifiers(identityMapCaptor.capture()); assertEquals(identityXDM, identityMapCaptor.getValue().asXDMMap()); - verify(mockExtensionApi).createXDMSharedState(properties.toXDMData(false), updateIdentityEvent); + // verify pending state is created and resolved + verify(mockExtensionApi).createPendingXDMSharedState(eq(updateIdentityEvent)); + verify(mockSharedStateResolver).resolve(eq(properties.toXDMData(false))); + verify(mockExtensionApi, never()).dispatch(any()); } @@ -691,6 +699,7 @@ public void test_handleUpdateIdentities_nullEventData_returns() { // setup final IdentityProperties properties = new IdentityProperties(); when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + when(mockExtensionApi.createPendingXDMSharedState(any())).thenReturn(mockSharedStateResolver); extension = new IdentityExtension(mockExtensionApi, mockIdentityState); @@ -705,10 +714,12 @@ public void test_handleUpdateIdentities_nullEventData_returns() { // verify that identifiers are not updated verify(mockIdentityState, never()).updateCustomerIdentifiers(any()); - // verify that no shared state is created - verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); // verify that no event is dispatched verify(mockExtensionApi, never()).dispatch(any()); + + // verify pending state is created and resolved + verify(mockExtensionApi).createPendingXDMSharedState(eq(updateIdentityEvent)); + verify(mockSharedStateResolver).resolve(eq(properties.toXDMData(false))); } @Test @@ -716,6 +727,7 @@ public void test_handleUpdateIdentities_EmptyEventData_returns() { // setup final IdentityProperties properties = new IdentityProperties(); when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + when(mockExtensionApi.createPendingXDMSharedState(any())).thenReturn(mockSharedStateResolver); extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test @@ -730,10 +742,12 @@ public void test_handleUpdateIdentities_EmptyEventData_returns() { // verify that identifiers are not updated verify(mockIdentityState, never()).updateCustomerIdentifiers(any()); - // verify that no shared state is created - verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); // verify that no event is dispatched verify(mockExtensionApi, never()).dispatch(any()); + + // verify pending state is created and resolved + verify(mockExtensionApi).createPendingXDMSharedState(eq(updateIdentityEvent)); + verify(mockSharedStateResolver).resolve(eq(properties.toXDMData(false))); } // ======================================================================================== @@ -761,6 +775,8 @@ public Object answer(InvocationOnMock invocation) throws Throwable { .when(mockIdentityState) .removeCustomerIdentifiers(any()); + when(mockExtensionApi.createPendingXDMSharedState(any())).thenReturn(mockSharedStateResolver); + extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test @@ -776,11 +792,9 @@ public Object answer(InvocationOnMock invocation) throws Throwable { removedIdentityMapCaptor.getValue().toString() ); - // verify shared state - final Map expectedState = properties.toXDMData(false); - final ArgumentCaptor> stateCaptor = ArgumentCaptor.forClass(Map.class); - verify(mockExtensionApi).createXDMSharedState(stateCaptor.capture(), eq(removeIdentityEvent)); - assertEquals(expectedState, stateCaptor.getValue()); + // verify pending state is created and resolved + verify(mockExtensionApi).createPendingXDMSharedState(eq(removeIdentityEvent)); + verify(mockSharedStateResolver).resolve(eq(properties.toXDMData(false))); } @Test @@ -792,6 +806,7 @@ public void test_handleRemoveIdentity_whenNullData_returns() { ); final IdentityProperties properties = new IdentityProperties(identityXDM); when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + when(mockExtensionApi.createPendingXDMSharedState(any())).thenReturn(mockSharedStateResolver); extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test @@ -801,13 +816,17 @@ public void test_handleRemoveIdentity_whenNullData_returns() { // verify identifiers not removed verify(mockIdentityState, never()).removeCustomerIdentifiers(any()); - // verify shared state is never created - verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); + // verify pending state is created and resolved + verify(mockExtensionApi).createPendingXDMSharedState(eq(removeIdentityEvent)); + verify(mockSharedStateResolver).resolve(eq(properties.toXDMData(false))); } @Test public void test_handleRemoveIdentity_eventWithEmptyData_returns() { // setup + final IdentityProperties properties = new IdentityProperties(); + when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + when(mockExtensionApi.createPendingXDMSharedState(any())).thenReturn(mockSharedStateResolver); extension = new IdentityExtension(mockExtensionApi, mockIdentityState); // test @@ -817,8 +836,9 @@ public void test_handleRemoveIdentity_eventWithEmptyData_returns() { // verify identifiers not removed verify(mockIdentityState, never()).removeCustomerIdentifiers(any()); - // verify shared state is never created - verify(mockExtensionApi, never()).createXDMSharedState(any(), any()); + // verify pending state is created and resolved + verify(mockExtensionApi).createPendingXDMSharedState(eq(notARemoveIdentityEvent)); + verify(mockSharedStateResolver).resolve(eq(properties.toXDMData(false))); } // ======================================================================================== @@ -883,6 +903,7 @@ public void test_handleRequestContent_EventIsNotAdIdEvent() { public void test_handleRequestReset() { final IdentityProperties properties = new IdentityProperties(); when(mockIdentityState.getIdentityProperties()).thenReturn(properties); + when(mockExtensionApi.createPendingXDMSharedState(any())).thenReturn(mockSharedStateResolver); extension = new IdentityExtension(mockExtensionApi, mockIdentityState); @@ -892,6 +913,9 @@ public void test_handleRequestReset() { extension.handleRequestReset(resetEvent); verify(mockIdentityState).resetIdentifiers(); - verify(mockExtensionApi).createXDMSharedState(properties.toXDMData(false), resetEvent); // will fail because of new ecid + + // verify pending state is created and resolved + verify(mockExtensionApi).createPendingXDMSharedState(eq(resetEvent)); + verify(mockSharedStateResolver).resolve(eq(properties.toXDMData(false))); } } From 8f6be73354b05a3f9cfc5c0b071a830f6fee18f2 Mon Sep 17 00:00:00 2001 From: Emilia Dobrin <33132425+emdobrin@users.noreply.github.com> Date: Thu, 12 Jan 2023 14:14:02 -0800 Subject: [PATCH 25/50] Update dependencies (#82) --- .circleci/config.yml | 6 +++--- code/app/build.gradle | 7 ++++++- code/app/libs/edge-release-2.0.0.aar | Bin 102427 -> 0 bytes code/app/libs/edgeconsent-release-2.0.0.aar | Bin 27489 -> 0 bytes 4 files changed, 9 insertions(+), 4 deletions(-) delete mode 100644 code/app/libs/edge-release-2.0.0.aar delete mode 100644 code/app/libs/edgeconsent-release-2.0.0.aar diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c71bdd5..ad3cbc41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,9 +48,9 @@ jobs: name: Build command: make ci-build - - run: - name: Build App - command: make ci-build-app + # - run: + # name: Build App + # command: make ci-build-app - run: name: UnitTests diff --git a/code/app/build.gradle b/code/app/build.gradle index 6bfbb5c2..037c4d34 100644 --- a/code/app/build.gradle +++ b/code/app/build.gradle @@ -70,7 +70,12 @@ dependencies { transitive = false } - implementation fileTree(dir: 'libs', include: ['edge-release-2.0.0.aar', 'edgeconsent-release-2.0.0.aar']) + implementation ('com.adobe.marketing.mobile:edge:2.0.0-SNAPSHOT') { + transitive = false + } + implementation ('com.adobe.marketing.mobile:edgeconsent:2.0.0-SNAPSHOT') { + transitive = false + } implementation ('com.adobe.marketing.mobile:assurance:2.0.0-SNAPSHOT') { transitive = false } diff --git a/code/app/libs/edge-release-2.0.0.aar b/code/app/libs/edge-release-2.0.0.aar deleted file mode 100644 index 53d50d1d8621238677a207c1129fa9957e3f2b96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102427 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 diff --git a/code/app/libs/edgeconsent-release-2.0.0.aar b/code/app/libs/edgeconsent-release-2.0.0.aar deleted file mode 100644 index cf92d935790ca4d9e20ffa8e66d581f02470b31f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27489 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 From 30c9650b147fe33b5ed7ca82c73155d188219d80 Mon Sep 17 00:00:00 2001 From: Emilia Dobrin <33132425+emdobrin@users.noreply.github.com> Date: Thu, 12 Jan 2023 14:56:16 -0800 Subject: [PATCH 26/50] Adding annotations on public APIs (#83) * Annotations, unit tests++ * Update test app * Update registerExtension test * Update docs --- .../identity/app/ui/CustomIdentityFragment.kt | 4 +- .../edge/identity/IdentityPublicAPITest.java | 68 +++++++++++++++---- .../util/IdentityFunctionalTestUtil.java | 10 ++- .../mobile/edge/identity/Identity.java | 37 +++++----- .../edge/identity/IdentityExtension.java | 5 +- 5 files changed, 86 insertions(+), 38 deletions(-) diff --git a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt index 5a3cfebb..b651095f 100644 --- a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt +++ b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt @@ -107,7 +107,9 @@ class CustomIdentityFragment : Fragment() { val isPrimary: Boolean = sharedViewModel.isPrimary.value ?: false val item = IdentityItem(identifier, authenticatedState, isPrimary) - Identity.removeIdentity(item, namespace) + if (namespace != null) { + Identity.removeIdentity(item, namespace) + } } // Advertising identifier features diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java index 590296fa..d9e9620a 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityPublicAPITest.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import org.json.JSONObject; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -39,15 +38,6 @@ public class IdentityPublicAPITest { @Rule public TestRule rule = new SetupCoreRule(); - // -------------------------------------------------------------------------------------------- - // Setup - // -------------------------------------------------------------------------------------------- - - @Before - public void setup() throws Exception { - registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); - } - // -------------------------------------------------------------------------------------------- // Tests for GetExtensionVersion API // -------------------------------------------------------------------------------------------- @@ -60,11 +50,29 @@ public void testGetExtensionVersionAPI() { // -------------------------------------------------------------------------------------------- // Tests for Register extension API // -------------------------------------------------------------------------------------------- - @Test public void testRegisterExtensionAPI() throws InterruptedException { // test - // Identity.registerExtension() is called in the setup method + //noinspection deprecation + Identity.registerExtension(); + + // now register monitor extension and start the hub + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION), null); + + // verify that the extension is registered with the correct version details + Map sharedStateMap = flattenMap( + getSharedStateFor(IdentityTestConstants.SharedStateName.EVENT_HUB, 5000) + ); + assertEquals( + IdentityConstants.EXTENSION_VERSION, + sharedStateMap.get("extensions.com.adobe.edge.identity.version") + ); + } + + @Test + public void testRegisterExtension_withClass() throws InterruptedException { + // test + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // verify that the extension is registered with the correct version details Map sharedStateMap = flattenMap( @@ -82,6 +90,8 @@ public void testRegisterExtensionAPI() throws InterruptedException { @Test public void testUpdateIdentitiesAPI() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test Identity.updateIdentities( createIdentityMap("Email", "example@email.com", AuthenticatedState.AUTHENTICATED, true) @@ -109,6 +119,8 @@ public void testUpdateIdentitiesAPI() throws Exception { @Test public void testUpdateAPI_nullData() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test Identity.updateIdentities(null); waitForThreads(2000); @@ -125,6 +137,8 @@ public void testUpdateAPI_nullData() throws Exception { @Test public void testUpdateAPI_emptyData() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test Identity.updateIdentities(new IdentityMap()); waitForThreads(2000); @@ -141,6 +155,8 @@ public void testUpdateAPI_emptyData() throws Exception { @Test public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test Identity.updateIdentities(createIdentityMap("Email", "example@email.com")); Identity.updateIdentities( @@ -172,6 +188,8 @@ public void testUpdateAPI_shouldReplaceExistingIdentities() throws Exception { @Test public void testUpdateAPI_withReservedNamespaces() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test Identity.updateIdentities(createIdentityMap("ECID", "newECID")); Identity.updateIdentities(createIdentityMap("GAID", "")); @@ -199,6 +217,8 @@ public void testUpdateAPI_withReservedNamespaces() throws Exception { @Test public void testUpdateAPI_multipleNamespaceMap() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test IdentityMap map = new IdentityMap(); map.addItem(new IdentityItem("primary@email.com"), "Email"); @@ -231,6 +251,8 @@ public void testUpdateAPI_multipleNamespaceMap() throws Exception { @Test public void testUpdateAPI_caseSensitiveNamespacesForCustomIdentifiers() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test IdentityMap map = new IdentityMap(); map.addItem(new IdentityItem("primary@email.com"), "Email"); @@ -261,6 +283,8 @@ public void testUpdateAPI_caseSensitiveNamespacesForCustomIdentifiers() throws E @Test public void testGetECID() { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test String ecid = getExperienceCloudIdSync(); @@ -270,6 +294,8 @@ public void testGetECID() { @Test public void testGetExperienceCloudId_nullCallback() { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test try { Identity.getExperienceCloudId(null); // should not crash @@ -284,6 +310,8 @@ public void testGetExperienceCloudId_nullCallback() { @Test public void testGetUrlVariables() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test setupConfiguration(); String urlVariables = getUrlVariablesSync(); @@ -293,6 +321,8 @@ public void testGetUrlVariables() throws Exception { @Test public void testGetUrlVariables_nullCallback() { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test try { Identity.getUrlVariables(null); // should not crash @@ -307,6 +337,8 @@ public void testGetUrlVariables_nullCallback() { @Test public void testGetIdentities() { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // setup // update Identities through API IdentityMap map = new IdentityMap(); @@ -333,6 +365,8 @@ public void testGetIdentities() { @Test public void testGetIdentities_nullCallback() { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test try { Identity.getIdentities(null); // should not crash @@ -347,6 +381,8 @@ public void testGetIdentities_nullCallback() { @Test public void testRemoveIdentity() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // setup // update Identities through API IdentityMap map = new IdentityMap(); @@ -382,6 +418,8 @@ public void testRemoveIdentity() throws Exception { @Test public void testRemoveIdentity_nonExistentNamespace() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test Identity.removeIdentity(new IdentityItem("primary@email.com"), "Email"); waitForThreads(2000); @@ -402,6 +440,8 @@ public void testRemoveIdentity_nonExistentNamespace() throws Exception { @Test public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // setup // update Identities through API Identity.updateIdentities(createIdentityMap("Email", "example@email.com")); @@ -426,6 +466,8 @@ public void testRemoveIdentity_nameSpaceCaseSensitive() throws Exception { @Test public void testRemoveIdentity_nonExistentItem() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // setup // update Identities through API Identity.updateIdentities(createIdentityMap("Email", "example@email.com")); @@ -450,6 +492,8 @@ public void testRemoveIdentity_nonExistentItem() throws Exception { @Test public void testRemoveIdentity_doesNotRemoveECID() throws Exception { + registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); + // test String currentECID = getExperienceCloudIdSync(); diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java index 793c369a..f6db0e5f 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/IdentityFunctionalTestUtil.java @@ -15,6 +15,7 @@ import static com.adobe.marketing.mobile.edge.identity.util.TestHelper.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import androidx.annotation.Nullable; import com.adobe.marketing.mobile.AdobeCallback; @@ -53,12 +54,11 @@ public class IdentityFunctionalTestUtil { * core. * @param extensions the extensions that need to be registered * @param configuration the initial configuration update that needs to be applied - * @throws InterruptedException if the wait time for extension registration has elapsed */ public static void registerExtensions( final List> extensions, @Nullable final Map configuration - ) throws InterruptedException { + ) { if (configuration != null) { MobileCore.updateConfiguration(configuration); } @@ -66,7 +66,11 @@ public static void registerExtensions( final ADBCountDownLatch latch = new ADBCountDownLatch(1); MobileCore.registerExtensions(extensions, o -> latch.countDown()); - latch.await(REGISTRATION_TIMEOUT_MS, TimeUnit.MILLISECONDS); + try { + latch.await(REGISTRATION_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + fail("Failed to register extensions"); + } TestHelper.waitForThreads(2000); resetTestExpectations(); } 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 7e87f61b..2184bfd4 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 @@ -13,6 +13,7 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityConstants.LOG_TAG; +import androidx.annotation.NonNull; import com.adobe.marketing.mobile.AdobeCallback; import com.adobe.marketing.mobile.AdobeCallbackWithError; import com.adobe.marketing.mobile.AdobeError; @@ -20,8 +21,6 @@ import com.adobe.marketing.mobile.EventSource; import com.adobe.marketing.mobile.EventType; import com.adobe.marketing.mobile.Extension; -import com.adobe.marketing.mobile.ExtensionError; -import com.adobe.marketing.mobile.ExtensionErrorCallback; import com.adobe.marketing.mobile.MobileCore; import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; @@ -47,6 +46,7 @@ private Identity() {} * * @return The version as {@code String} */ + @NonNull public static String extensionVersion() { return IdentityConstants.EXTENSION_VERSION; } @@ -54,22 +54,19 @@ public static String extensionVersion() { /** * Registers the extension with the Mobile SDK. This method should be called only once in your application class. * - * @deprecated Use {@link MobileCore#registerExtensions(List, AdobeCallback)} with {@link Identity#EXTENSION} instead. + * @deprecated as of 2.0.0, use {@link MobileCore#registerExtensions(List, AdobeCallback)} with {@link Identity#EXTENSION} instead. */ @Deprecated + @SuppressWarnings("deprecation") public static void registerExtension() { MobileCore.registerExtension( IdentityExtension.class, - new ExtensionErrorCallback() { - @Override - public void error(ExtensionError extensionError) { - Log.error( - LOG_TAG, - LOG_SOURCE, - "There was an error registering the Edge Identity extension: " + extensionError.getErrorName() - ); - } - } + extensionError -> + Log.error( + LOG_TAG, + LOG_SOURCE, + "There was an error registering the Edge Identity extension: " + extensionError.getErrorName() + ) ); } @@ -80,7 +77,7 @@ public void error(ExtensionError extensionError) { * If an {@link AdobeCallbackWithError} is provided, an {@link AdobeError} can be returned in the * eventuality of any error that occurred while getting the Experience Cloud ID */ - public static void getExperienceCloudId(final AdobeCallback callback) { + public static void getExperienceCloudId(@NonNull final AdobeCallback callback) { if (callback == null) { Log.debug(LOG_TAG, LOG_SOURCE, "Unexpected null callback, provide a callback to retrieve current ECID."); return; @@ -95,7 +92,7 @@ public static void getExperienceCloudId(final AdobeCallback callback) { final AdobeCallbackWithError callbackWithError = new AdobeCallbackWithError() { @Override - public void call(Event responseEvent) { + public void call(final Event responseEvent) { if (responseEvent == null || responseEvent.getEventData() == null) { returnError(callback, AdobeError.UNEXPECTED_ERROR); return; @@ -125,7 +122,7 @@ public void call(Event responseEvent) { } @Override - public void fail(AdobeError adobeError) { + public void fail(final AdobeError adobeError) { returnError(callback, adobeError); Log.debug( LOG_TAG, @@ -159,7 +156,7 @@ public void fail(AdobeError adobeError) { * 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) { + public static void getUrlVariables(@NonNull final AdobeCallback callback) { if (callback == null) { Log.debug( LOG_TAG, @@ -231,7 +228,7 @@ public void fail(final AdobeError adobeError) { * * @param identityMap The identifiers to add or update. */ - public static void updateIdentities(final IdentityMap identityMap) { + public static void updateIdentities(@NonNull final IdentityMap identityMap) { if (identityMap == null || identityMap.isEmpty()) { Log.debug(LOG_TAG, LOG_SOURCE, "Unable to updateIdentities, IdentityMap is null or empty"); return; @@ -255,7 +252,7 @@ public static void updateIdentities(final IdentityMap identityMap) { * @param item the {@link IdentityItem} to remove. * @param namespace The namespace of the identity to remove. */ - public static void removeIdentity(final IdentityItem item, final String namespace) { + public static void removeIdentity(@NonNull final IdentityItem item, @NonNull final String namespace) { if (StringUtils.isNullOrEmpty(namespace)) { Log.debug(LOG_TAG, LOG_SOURCE, "Unable to removeIdentity, namespace is null or empty"); return; @@ -286,7 +283,7 @@ public static void removeIdentity(final IdentityItem item, final String namespac * If an {@link AdobeCallbackWithError} is provided, an {@link AdobeError} can be returned in the * eventuality of any error that occurred while getting the stored identities. */ - public static void getIdentities(final AdobeCallback callback) { + public static void getIdentities(@NonNull final AdobeCallback callback) { if (callback == null) { Log.debug( LOG_TAG, 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 ea699ae7..fc5ab4d2 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 @@ -74,11 +74,13 @@ protected String getName() { return IdentityConstants.EXTENSION_NAME; } + @NonNull @Override protected String getFriendlyName() { return IdentityConstants.EXTENSION_FRIENDLY_NAME; } + @NonNull @Override protected String getVersion() { return IdentityConstants.EXTENSION_VERSION; @@ -91,11 +93,10 @@ protected String getVersion() { * The following listeners are registered during this extension's registration. *
    *
  • EventType {@link EventType#GENERIC_IDENTITY} and EventSource {@link EventSource#REQUEST_CONTENT}
  • + *
  • EventType {@link EventType#GENERIC_IDENTITY} and EventSource {@link EventSource#REQUEST_RESET}
  • *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#REQUEST_IDENTITY}
  • *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#UPDATE_IDENTITY}
  • *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#REMOVE_IDENTITY}
  • - *
  • EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#REQUEST_CONTENT}
  • - *
  • EventType {@link EventType#GENERIC_IDENTITY} and EventSource {@link EventSource#REQUEST_RESET}
  • *
  • EventType {@link EventType#HUB} and EventSource {@link EventSource#SHARED_STATE}
  • *
*

From 86ae5a2b6089c7438b8786652490adafbf59dd26 Mon Sep 17 00:00:00 2001 From: Emilia Dobrin <33132425+emdobrin@users.noreply.github.com> Date: Thu, 12 Jan 2023 16:42:53 -0800 Subject: [PATCH 27/50] java 11 for release workflow (#85) --- .github/workflows/maven-release.yml | 2 +- .github/workflows/maven-snapshot.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index fbff1c77..627da1b4 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -11,7 +11,7 @@ jobs: - name: Set up Java uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Cache Gradle packages uses: actions/cache@v2 with: diff --git a/.github/workflows/maven-snapshot.yml b/.github/workflows/maven-snapshot.yml index 748479fb..aa38ba6c 100644 --- a/.github/workflows/maven-snapshot.yml +++ b/.github/workflows/maven-snapshot.yml @@ -11,7 +11,7 @@ jobs: - name: Set up Java uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Cache Gradle packages uses: actions/cache@v2 with: From e12492dddbf0292f957a1657030ce7f2804199ea Mon Sep 17 00:00:00 2001 From: Kevin Lind <40409666+kevinlind@users.noreply.github.com> Date: Thu, 12 Jan 2023 17:23:09 -0800 Subject: [PATCH 28/50] Fix use of 'allowEmpty' parameter in IdentityProperties.toXDMData() (#86) --- .../identity/IdentityECIDHandlingTest.java | 4 ++-- .../edge/identity/IdentityExtension.java | 18 +++++++------- .../mobile/edge/identity/IdentityMap.java | 16 +++---------- .../edge/identity/IdentityProperties.java | 16 +++++++++++-- .../mobile/edge/identity/IdentityState.java | 4 ++-- .../edge/identity/IdentityStorageManager.java | 2 +- .../edge/identity/IdentityExtensionTests.java | 2 +- .../edge/identity/IdentityMapTests.java | 18 +++++++------- .../identity/IdentityPropertiesTests.java | 24 ++++++++++++++++++- .../mobile/edge/identity/IdentityTests.java | 4 ++-- 10 files changed, 66 insertions(+), 42 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java index d17e1f9e..8cf9a9a7 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/IdentityECIDHandlingTest.java @@ -66,7 +66,7 @@ public void testECID_loadedFromPersistence() throws Exception { public void testECID_edgePersistenceTakesPreferenceOverDirectExtension() throws Exception { // setup setIdentityDirectPersistedECID("legacyECID"); - setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap()); + setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap(false)); registerExtensions(Arrays.asList(MonitorExtension.EXTENSION, Identity.EXTENSION), null); // verify @@ -217,7 +217,7 @@ public void testECID_AreDifferentAfterResetIdentitiesAndPrivacyChange() throws E public void testECID_DirectEcidIsRemovedOnPrivacyOptOut() throws Exception { // setup setIdentityDirectPersistedECID("legacyECID"); - setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap()); + setEdgeIdentityPersistence(createIdentityMap("ECID", "edgeECID").asXDMMap(false)); registerExtensions( Arrays.asList( 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 fc5ab4d2..07169b30 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 @@ -259,7 +259,7 @@ void handleUpdateIdentities(@NonNull final Event event) { if (eventData == null) { Log.trace(LOG_TAG, LOG_SOURCE, "Cannot update identifiers, event data is null."); - resolver.resolve(state.getIdentityProperties().toXDMData(false)); + resolver.resolve(state.getIdentityProperties().toXDMData()); return; } @@ -271,12 +271,12 @@ void handleUpdateIdentities(@NonNull final Event event) { LOG_SOURCE, "Failed to update identifiers as no identifiers were found in the event data." ); - resolver.resolve(state.getIdentityProperties().toXDMData(false)); + resolver.resolve(state.getIdentityProperties().toXDMData()); return; } state.updateCustomerIdentifiers(map); - resolver.resolve(state.getIdentityProperties().toXDMData(false)); + resolver.resolve(state.getIdentityProperties().toXDMData()); } /** @@ -292,7 +292,7 @@ void handleRemoveIdentity(@NonNull final Event event) { if (eventData == null) { Log.trace(LOG_TAG, LOG_SOURCE, "Cannot remove identifiers, event data is null."); - resolver.resolve(state.getIdentityProperties().toXDMData(false)); + resolver.resolve(state.getIdentityProperties().toXDMData()); return; } @@ -304,12 +304,12 @@ void handleRemoveIdentity(@NonNull final Event event) { LOG_SOURCE, "Failed to remove identifiers as no identifiers were found in the event data." ); - resolver.resolve(state.getIdentityProperties().toXDMData(false)); + resolver.resolve(state.getIdentityProperties().toXDMData()); return; } state.removeCustomerIdentifiers(map); - resolver.resolve(state.getIdentityProperties().toXDMData(false)); + resolver.resolve(state.getIdentityProperties().toXDMData()); } /** @@ -318,7 +318,7 @@ void handleRemoveIdentity(@NonNull final Event event) { * @param event the identity request {@link Event} */ private void handleGetIdentifiersRequest(@NonNull final Event event) { - final Map xdmData = state.getIdentityProperties().toXDMData(false); + final Map xdmData = state.getIdentityProperties().toXDMData(true); final Event responseEvent = new Event.Builder( IdentityConstants.EventNames.IDENTITY_RESPONSE_CONTENT_ONE_TIME, EventType.EDGE_IDENTITY, @@ -340,7 +340,7 @@ void handleRequestReset(@NonNull final Event event) { // Add pending shared state to avoid race condition between updating and reading identity map final SharedStateResolver resolver = getApi().createPendingXDMSharedState(event); state.resetIdentifiers(); - resolver.resolve(state.getIdentityProperties().toXDMData(false)); + resolver.resolve(state.getIdentityProperties().toXDMData()); // dispatch reset complete event final Event responseEvent = new Event.Builder( @@ -403,6 +403,6 @@ void handleRequestContent(@NonNull final Event event) { * @param event the {@link Event} that triggered the XDM shared state change */ private void shareIdentityXDMSharedState(final Event event) { - sharedStateHandle.createXDMSharedState(state.getIdentityProperties().toXDMData(false), event); + sharedStateHandle.createXDMSharedState(state.getIdentityProperties().toXDMData(), event); } } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java index 4db3ee9b..710bbc31 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java @@ -229,21 +229,11 @@ boolean clearItemsForNamespace(final String namespace) { return isRemoved; } - /** - * Use this method to cast the {@link IdentityMap} as {@code Map} to be passed as EventData for an SDK Event. - * This method returns an empty map if the {@code IdentityMap} contains no data - * - * @return {@code Map} representation of xdm formatted IdentityMap - */ - Map asXDMMap() { - return asXDMMap(true); - } - /** * Use this method to cast the {@link IdentityMap} as {@code Map} to be passed as EventData for an SDK Event. * - * @param allowEmpty If false and if this {@code IdentityMap} contains no data, then returns a map with empty xdmFormatted Identity Map. - * If true and if this {@code IdentityMap} contains no data, then returns an empty map + * @param allowEmpty If true and if this {@code IdentityMap} contains no data, then returns a map with empty xdmFormatted Identity Map. + * If false and if this {@code IdentityMap} contains no data, then returns an empty map * @return {@code Map} representation of xdm formatted IdentityMap */ Map asXDMMap(final boolean allowEmpty) { @@ -260,7 +250,7 @@ Map asXDMMap(final boolean allowEmpty) { identityMap.put(namespace, namespaceIds); } - if (!identityMap.isEmpty() || !allowEmpty) { + if (!identityMap.isEmpty() || allowEmpty) { xdmMap.put(IdentityConstants.XDMKeys.IDENTITY_MAP, identityMap); } 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 36c79dec..f1ba370d 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 @@ -224,9 +224,21 @@ void removeCustomerIdentifiers(final IdentityMap map) { } /** - * Converts this into an event data representation in XDM format + * Converts this {@code IdentityProperties} into an event data representation in XDM format + * Use this method to cast the {@link IdentityMap} as {@code Map} to be passed as EventData for an SDK Event. + * This method returns an empty map if the {@code IdentityMap} contains no data * - * @param allowEmpty If this {@link IdentityProperties} contains no data, return a dictionary with a single {@link IdentityMap} key + * @return A {@link Map} representing this in XDM format, or an empty {@link Map} if this contains no data. + */ + Map toXDMData() { + return toXDMData(false); + } + + /** + * Converts this {@code IdentityProperties} into an event data representation in XDM format + * + * @param allowEmpty If this {@link IdentityProperties} contains no data, return a dictionary with a single {@link IdentityMap} key, + * otherwise an empty map is returned. * @return A {@link Map} representing this in XDM format */ Map toXDMData(final boolean allowEmpty) { diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index cfecc251..2cc44e91 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -137,7 +137,7 @@ else if (isIdentityDirectRegistered(eventHubStateResult.getValue())) { hasBooted = true; Log.debug(LOG_TAG, LOG_SOURCE, "Edge Identity has successfully booted up"); - callback.createXDMSharedState(identityProperties.toXDMData(false), null); + callback.createXDMSharedState(identityProperties.toXDMData(), null); return hasBooted; } @@ -209,7 +209,7 @@ void updateAdvertisingIdentifier(final Event event, final SharedStateCallback ca // Save to persistence identityStorageManager.savePropertiesToPersistence(identityProperties); - callback.createXDMSharedState(identityProperties.toXDMData(false), event); + callback.createXDMSharedState(identityProperties.toXDMData(), event); } /** diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java index 62092e10..07525550 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityStorageManager.java @@ -99,7 +99,7 @@ void savePropertiesToPersistence(final IdentityProperties properties) { return; } - final JSONObject jsonObject = new JSONObject(properties.toXDMData(false)); + final JSONObject jsonObject = new JSONObject(properties.toXDMData()); final String jsonString = jsonObject.toString(); edgeIdentityStore.setString(IdentityConstants.DataStoreKey.IDENTITY_PROPERTIES, jsonString); } 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 cd018e6f..abae0e61 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 @@ -685,7 +685,7 @@ public void test_handleUpdateIdentities_whenValidData_updatesCustomerIdentifiers // verify identifiers updated final ArgumentCaptor identityMapCaptor = ArgumentCaptor.forClass(IdentityMap.class); verify(mockIdentityState).updateCustomerIdentifiers(identityMapCaptor.capture()); - assertEquals(identityXDM, identityMapCaptor.getValue().asXDMMap()); + assertEquals(identityXDM, identityMapCaptor.getValue().asXDMMap(false)); // verify pending state is created and resolved verify(mockExtensionApi).createPendingXDMSharedState(eq(updateIdentityEvent)); diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java index 331f48ad..06ff453b 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityMapTests.java @@ -31,7 +31,7 @@ public void test_AddItem() { map.addItem(new IdentityItem("California"), "location"); // verify - IdentityTestUtil.flattenMap(map.asXDMMap()).get("identityMap.location[0].id"); + IdentityTestUtil.flattenMap(map.asXDMMap(false)).get("identityMap.location[0].id"); } @Test @@ -148,7 +148,7 @@ public void test_merge_sameItem_GetsReplaced() { baseMap.merge(newMap); // verify the existing identityMap is unchanged - Map flattenedMap = IdentityTestUtil.flattenMap(baseMap.asXDMMap()); + Map flattenedMap = IdentityTestUtil.flattenMap(baseMap.asXDMMap(false)); assertEquals(3, flattenedMap.size()); assertEquals("California", flattenedMap.get("identityMap.location[0].id")); assertEquals("authenticated", flattenedMap.get("identityMap.location[0].authenticatedState")); @@ -219,7 +219,7 @@ public void test_removeAllIdentityItemsForNamespace_onEmptyMap() { // test emptyMap.clearItemsForNamespace("location"); - assertTrue(emptyMap.asXDMMap().isEmpty()); + assertTrue(emptyMap.asXDMMap(false).isEmpty()); } @Test @@ -301,7 +301,7 @@ public void test_FromData() throws Exception { IdentityMap map = IdentityMap.fromXDMMap(xdmData); // verify - Map flattenedMap = IdentityTestUtil.flattenMap(map.asXDMMap()); + Map flattenedMap = IdentityTestUtil.flattenMap(map.asXDMMap(false)); assertEquals("randomECID", flattenedMap.get("identityMap.ECID[0].id")); assertEquals("ambiguous", flattenedMap.get("identityMap.ECID[0].authenticatedState")); assertEquals("true", flattenedMap.get("identityMap.ECID[0].primary")); @@ -387,16 +387,16 @@ public void testFromXDMMap_InvalidIdentityMap() throws Exception { } @Test - public void testAsXDMMap_AllowEmptyTrue() { + public void testAsXDMMap_AllowEmptyFalse() { IdentityMap map = new IdentityMap(); - Map xdmMap = map.asXDMMap(true); + Map xdmMap = map.asXDMMap(false); assertTrue(xdmMap.isEmpty()); } @Test - public void testAsXDMMap_AllowEmptyFalse() { + public void testAsXDMMap_AllowEmptyTrue() { IdentityMap map = new IdentityMap(); - Map xdmMap = map.asXDMMap(false); + Map xdmMap = map.asXDMMap(true); // verify that the base xdm key identityMap is present assertEquals(1, xdmMap.size()); @@ -404,7 +404,7 @@ public void testAsXDMMap_AllowEmptyFalse() { } private Map> getCastedIdentityMap(final IdentityMap map) { - final Map xdmMap = map.asXDMMap(); + final Map xdmMap = map.asXDMMap(false); return (Map>) xdmMap.get(IdentityConstants.XDMKeys.IDENTITY_MAP); } diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java index 68339f26..425d0627 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/IdentityPropertiesTests.java @@ -13,8 +13,12 @@ import static com.adobe.marketing.mobile.edge.identity.IdentityTestUtil.*; 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 com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.DataReaderException; import java.util.Map; import org.junit.Test; @@ -25,13 +29,31 @@ public class IdentityPropertiesTests { // ====================================================================================================================== @Test - public void test_toXDMData_AllowEmpty() { + public void test_toXDMData_AllowEmpty_True() throws DataReaderException { // setup IdentityProperties props = new IdentityProperties(); // test Map xdmMap = props.toXDMData(true); + // verify + Map identityMap = DataReader.getTypedMap( + Object.class, + xdmMap, + IdentityConstants.XDMKeys.IDENTITY_MAP + ); + assertNotNull(identityMap); + assertTrue(identityMap.isEmpty()); + } + + @Test + public void test_toXDMData_AllowEmpty_False() { + // setup + IdentityProperties props = new IdentityProperties(); + + // test + Map xdmMap = props.toXDMData(false); + // verify assertNull(xdmMap.get(IdentityConstants.XDMKeys.IDENTITY_MAP)); } 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 af87cc32..713cff69 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 @@ -628,7 +628,7 @@ public void testUpdateIdentities() { assertEquals(IdentityConstants.EventNames.UPDATE_IDENTITIES, dispatchedEvent.getName()); assertEquals(EventType.EDGE_IDENTITY, dispatchedEvent.getType()); assertEquals(EventSource.UPDATE_IDENTITY, dispatchedEvent.getSource()); - assertEquals(map.asXDMMap(), dispatchedEvent.getEventData()); + assertEquals(map.asXDMMap(false), dispatchedEvent.getEventData()); } @Test @@ -685,7 +685,7 @@ public void testRemoveIdentity() { final IdentityMap expectedIdentityMap = new IdentityMap(); expectedIdentityMap.addItem(sampleItem, "namespace"); - assertEquals(expectedIdentityMap.asXDMMap(), dispatchedEvent.getEventData()); + assertEquals(expectedIdentityMap.asXDMMap(false), dispatchedEvent.getEventData()); } @Test From cf118f4297032ee9f94395e664c0a6b7e11fcaf1 Mon Sep 17 00:00:00 2001 From: Tim Kim <95260439+timkimadobe@users.noreply.github.com> Date: Thu, 19 Jan 2023 12:12:44 -0800 Subject: [PATCH 29/50] Refactor functional test helper to use NamedCollection (#88) * Update functional test helper to use named collections instead of android shared preferences directly * Update docs to NamedCollection --- .../identity/util/TestPersistenceHelper.java | 90 +++---------------- 1 file changed, 13 insertions(+), 77 deletions(-) diff --git a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestPersistenceHelper.java b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestPersistenceHelper.java index 84de4b78..5ac5eae3 100644 --- a/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestPersistenceHelper.java +++ b/code/edgeidentity/src/androidTest/java/com/adobe/marketing/mobile/edge/identity/util/TestPersistenceHelper.java @@ -11,11 +11,8 @@ package com.adobe.marketing.mobile.edge.identity.util; -import static org.junit.Assert.fail; - -import android.app.Application; -import android.content.Context; -import android.content.SharedPreferences; +import com.adobe.marketing.mobile.services.NamedCollection; +import com.adobe.marketing.mobile.services.ServiceProvider; import java.util.ArrayList; /** @@ -32,38 +29,16 @@ public class TestPersistenceHelper { }; /** - * Helper method to update the {@link SharedPreferences} data. + * Helper method to update the {@link NamedCollection} data. * * @param datastore the name of the datastore to be updated * @param key the persisted data key that has to be updated * @param value the new value */ public static void updatePersistence(final String datastore, final String key, final String value) { - final Application application = TestHelper.defaultApplication; - - if (application == null) { - fail( - "Unable to updatePersistence by TestPersistenceHelper. Application is null, fast failing the test case." - ); - } - - final Context context = application.getApplicationContext(); - - if (context == null) { - fail("Unable to updatePersistence by TestPersistenceHelper. Context is null, fast failing the test case."); - } + NamedCollection dataStore = ServiceProvider.getInstance().getDataStoreService().getNamedCollection(datastore); - SharedPreferences sharedPreferences = context.getSharedPreferences(datastore, Context.MODE_PRIVATE); - - if (sharedPreferences == null) { - fail( - "Unable to updatePersistence by TestPersistenceHelper. sharedPreferences is null, fast failing the test case." - ); - } - - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(key, value); - editor.apply(); + dataStore.setString(key, value); } /** @@ -71,64 +46,25 @@ public static void updatePersistence(final String datastore, final String key, f * * @param datastore the name of the datastore to be read * @param key the key that needs to be read - * @return {@link String} value of persisted data. Null if data is not found in {@link SharedPreferences} + * @return {@link String} value of persisted data. {@code null} if data is not found in {@link NamedCollection} */ public static String readPersistedData(final String datastore, final String key) { - final Application application = TestHelper.defaultApplication; - - if (application == null) { - fail( - "Unable to readPersistedData by TestPersistenceHelper. Application is null, fast failing the test case." - ); - } - - final Context context = application.getApplicationContext(); + NamedCollection dataStore = ServiceProvider.getInstance().getDataStoreService().getNamedCollection(datastore); - if (context == null) { - fail("Unable to readPersistedData by TestPersistenceHelper. Context is null, fast failing the test case."); - } - - SharedPreferences sharedPreferences = context.getSharedPreferences(datastore, Context.MODE_PRIVATE); - - if (sharedPreferences == null) { - fail( - "Unable to readPersistedData by TestPersistenceHelper. sharedPreferences is null, fast failing the test case." - ); - } - - return sharedPreferences.getString(key, null); + return dataStore.getString(key, null); } /** * Clears the Configuration and Consent extension's persisted data */ public static void resetKnownPersistence() { - final Application application = TestHelper.defaultApplication; - - if (application == null) { - fail( - "Unable to resetPersistence by TestPersistenceHelper. Application is null, fast failing the test case." - ); - } - - final Context context = application.getApplicationContext(); - - if (context == null) { - fail("Unable to resetPersistence by TestPersistenceHelper. Context is null, fast failing the test case."); - } - for (String eachDatastore : knownDatastoreName) { - SharedPreferences sharedPreferences = context.getSharedPreferences(eachDatastore, Context.MODE_PRIVATE); - - if (sharedPreferences == null) { - fail( - "Unable to resetPersistence by TestPersistenceHelper. sharedPreferences is null, fast failing the test case." - ); - } + NamedCollection dataStore = ServiceProvider + .getInstance() + .getDataStoreService() + .getNamedCollection(eachDatastore); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.clear(); - editor.apply(); + dataStore.removeAll(); } } } From ec0aafc8758ebd7e8d2c32cc1e9bd9ab7f59c512 Mon Sep 17 00:00:00 2001 From: Calise Cheung Date: Fri, 20 Jan 2023 14:29:13 -0800 Subject: [PATCH 30/50] Update doc for v.2.0.0 Update implementation from 2+ to 2.0.0 Update sample code in doc. --- Documentation/api-reference.md | 3 ++ Documentation/getting-started.md | 53 ++++++++++++++++++++------------ README.md | 16 ++++------ 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/Documentation/api-reference.md b/Documentation/api-reference.md index 218a5dc7..d8f4c5d4 100644 --- a/Documentation/api-reference.md +++ b/Documentation/api-reference.md @@ -134,6 +134,9 @@ Identity.getUrlVariables(new AdobeCallback() { Registers the Identity for Edge Network extension with the Mobile Core extension. +> **Warning** +> Deprecated as of 2.0.0. Use [MobileCore.registerExtensions API](https://github.com/adobe/aepsdk-core-android/blob/main/docs/Usage/MobileCore.md#registering-extensions-and-starting-the-sdk) instead. + > **Note** > If your use-case covers both Edge Network and Adobe Experience Cloud Solutions extensions, you need to register Identity for Edge Network and Identity for Experience Cloud Identity Service from Mobile Core extensions. For more details, see the [frequently asked questions](https://aep-sdks.gitbook.io/docs/foundation-extensions/identity-for-edge-network/identity-faq#q-i-am-using-aep-edge-and-adobe-solutions-extensions-which-identity-extension-should-i-install-and-register). diff --git a/Documentation/getting-started.md b/Documentation/getting-started.md index c3aace0f..a3736e28 100644 --- a/Documentation/getting-started.md +++ b/Documentation/getting-started.md @@ -13,11 +13,12 @@ The Adobe Experience Platform Identity for Edge Network extension has the follow ### Java 1. Add the Mobile Core and Edge extensions to your project using the app's Gradle file. +See the [current version list](https://developer.adobe.com/client-sdks/documentation/current-sdk-versions) for the latest extension versions to use. ```java - implementation 'com.adobe.marketing.mobile:core:1.+' - implementation 'com.adobe.marketing.mobile:edge:1.+' - implementation 'com.adobe.marketing.mobile:edgeidentity:1.+' + implementation 'com.adobe.marketing.mobile:core:2.0.0' + implementation 'com.adobe.marketing.mobile:edge:2.0.0' + implementation 'com.adobe.marketing.mobile:edgeidentity:2.0.0' ``` 2. Import the Mobile Core and Edge extensions in your Application class. @@ -39,22 +40,36 @@ public class MobileApp extends Application { public void onCreate() { super.onCreate(); MobileCore.setApplication(this); - try { - Edge.registerExtension(); - Identity.registerExtension(); - // register other extensions - MobileCore.start(new AdobeCallback () { - @Override - public void call(Object o) { - MobileCore.configureWithAppID("yourAppId"); - } - }); - - } catch (Exception e) { - ... - } - - + + MobileCore.configureWithAppID(ENVIRONMENT_FILE_ID); + + // register Adobe extensions + MobileCore.registerExtensions( + Arrays.asList(Edge.EXTENSION, Identity.EXTENSION, Assurance.EXTENSION), + o -> Log.d("MobileApp", "Mobile SDK was initialized") + ); + } +} +``` +### Kotlin + +```kotlin +class MobileApp : Application() { + // Set up the preferred Environment File ID from your mobile property configured in Data Collection UI + private var ENVIRONMENT_FILE_ID: String = "" + + override fun onCreate() { + super.onCreate() + + // register AEP SDK extensions + MobileCore.setApplication(this) + MobileCore.setLogLevel(LoggingMode.VERBOSE) + + MobileCore.registerExtensions( + listOf(Edge.EXTENSION, Identity.EXTENSION, Consent.EXTENSION, Assurance.EXTENSION) + ) { + MobileCore.configureWithAppID(ENVIRONMENT_FILE_ID) + } } } ``` diff --git a/README.md b/README.md index 2f5f7753..0140814e 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,14 @@ The Adobe Experience Platform Edge Identity is a mobile extension for the [Adobe Integrate the Edge Identity extension into your app by including the following in your gradle file's `dependencies`: ```gradle -implementation 'com.adobe.marketing.mobile:edgeidentity:1.+' -implementation 'com.adobe.marketing.mobile:edge:1.+' -implementation 'com.adobe.marketing.mobile:core:1.+' -implementation 'com.adobe.marketing.mobile:edgeconsent:1.+' // Recommended when using the setAdvertisingIdentifier API +implementation 'com.adobe.marketing.mobile:edgeidentity:2.0.0' +implementation 'com.adobe.marketing.mobile:edge:2.0.0' +implementation 'com.adobe.marketing.mobile:core:2.0.0' +implementation 'com.adobe.marketing.mobile:edgeconsent:2.0.0' // Recommended when using the setAdvertisingIdentifier API ``` +See the [current version list](https://developer.adobe.com/client-sdks/documentation/current-sdk-versions) for the latest extension versions to use. + ### Development **Open the project** @@ -89,12 +91,6 @@ To enable the Git pre-commit hook to apply code formatting on each commit, run t make init ``` -## Related Projects - -| Project | Description | -| ------------------------------------------------------------ | ------------------------------------------------------------ | -| [AEP SDK Sample App for Android](https://github.com/adobe/aepsdk-sample-app-android) | Contains Android sample app for the AEP SDK. | - ## Documentation Additional documentation for usage and SDK architecture can be found under the [Documentation](Documentation) directory. From dba1d57fa4389f116779a223dd772ca95fb0e6e2 Mon Sep 17 00:00:00 2001 From: Calise Cheung Date: Tue, 24 Jan 2023 01:17:03 -0800 Subject: [PATCH 31/50] Update Readme Update Readme --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0140814e..df7b5e87 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ To open and run the project, open the `code/settings.gradle` file in Android Stu The test app needs to be configured with the following edge extensions before it can be used: - Mobile Core (installed by default) -- [Edge](https://aep-sdks.gitbook.io/docs/foundation-extensions/experience-platform-extension) -- [Edge Identity](https://aep-sdks.gitbook.io/docs/foundation-extensions/identity-for-edge-network) -- [Edge Consent](https://aep-sdks.gitbook.io/docs/foundation-extensions/consent-for-edge-network) (recommended when using the setAdvertisingIdentifier API) +- [Edge](https://developer.adobe.com/client-sdks/documentation/edge-network) +- [Edge Identity](https://developer.adobe.com/client-sdks/documentation/identity-for-edge-network) +- [Edge Consent](https://developer.adobe.com/client-sdks/documentation/consent-for-edge-network) (recommended when using the setAdvertisingIdentifier API) **Run demo application** @@ -91,6 +91,12 @@ To enable the Git pre-commit hook to apply code formatting on each commit, run t make init ``` +## Related Projects + +| Project | Description | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| [AEP SDK Sample App for Android](https://github.com/adobe/aepsdk-sample-app-android) | Contains Android sample app for the AEP SDK. | + ## Documentation Additional documentation for usage and SDK architecture can be found under the [Documentation](Documentation) directory. From 47399651caa5f3a9b7938a9eab6a158e8aacdea6 Mon Sep 17 00:00:00 2001 From: Calise Cheung Date: Tue, 24 Jan 2023 11:15:35 -0800 Subject: [PATCH 32/50] Use Core MapUtils for isNullOrEmpty Use Core MapUtils for isNullOrEmpty --- .../mobile/edge/identity/IdentityMap.java | 3 ++- .../mobile/edge/identity/IdentityState.java | 3 ++- .../marketing/mobile/edge/identity/Utils.java | 11 ----------- .../mobile/edge/identity/UtilsTests.java | 16 ---------------- 4 files changed, 4 insertions(+), 29 deletions(-) diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java index 710bbc31..2a5b8c9b 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityMap.java @@ -15,6 +15,7 @@ import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.MapUtils; import com.adobe.marketing.mobile.util.StringUtils; import java.util.ArrayList; import java.util.HashMap; @@ -265,7 +266,7 @@ Map asXDMMap(final boolean allowEmpty) { * @return {@code Map} XDM format representation of IdentityMap */ static IdentityMap fromXDMMap(final Map map) { - if (Utils.isNullOrEmpty(map)) { + if (MapUtils.isNullOrEmpty(map)) { return null; } diff --git a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java index 2cc44e91..eeec0c41 100644 --- a/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java +++ b/code/edgeidentity/src/main/java/com/adobe/marketing/mobile/edge/identity/IdentityState.java @@ -24,6 +24,7 @@ import com.adobe.marketing.mobile.services.Log; import com.adobe.marketing.mobile.services.ServiceProvider; import com.adobe.marketing.mobile.util.DataReader; +import com.adobe.marketing.mobile.util.MapUtils; import java.util.HashMap; import java.util.Map; @@ -294,7 +295,7 @@ private boolean isIdentityDirectRegistered(final Map eventHubSha null ); - return !Utils.isNullOrEmpty(identityDirectInfo); + return !MapUtils.isNullOrEmpty(identityDirectInfo); } /** 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 ebdcfc95..c3d075c9 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 @@ -12,22 +12,11 @@ package com.adobe.marketing.mobile.edge.identity; import java.util.List; -import java.util.Map; class Utils { private Utils() {} - /** - * Checks if the {@code Map} provided is null or empty. - * - * @param map the {@code Map} to verify - * @return true if the {@code Map} provided is null or empty; false otherwise - */ - static boolean isNullOrEmpty(final Map map) { - return map == null || map.isEmpty(); - } - /** * Checks if the {@code List} provided is null or empty. * diff --git a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java index 3a5072b4..eb060e62 100644 --- a/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java +++ b/code/edgeidentity/src/test/java/com/adobe/marketing/mobile/edge/identity/UtilsTests.java @@ -17,27 +17,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import org.junit.Test; @SuppressWarnings("unchecked") public class UtilsTests { - @Test - public void test_isNullOrEmpty_nullMap() { - assertTrue(Utils.isNullOrEmpty((Map) null)); - } - - @Test - public void test_isNullOrEmpty_emptyMap() { - assertTrue(Utils.isNullOrEmpty(Collections.EMPTY_MAP)); - } - - @Test - public void test_isNullOrEmpty_nonEmptyNonNullMap() { - assertFalse(Utils.isNullOrEmpty(Collections.singletonMap("someKey", "someValue"))); - } - @Test public void test_isNullOrEmpty_nullList() { assertTrue(Utils.isNullOrEmpty((List) null)); From c6214e365774313ee82bb9463338386109139be4 Mon Sep 17 00:00:00 2001 From: Calise Cheung Date: Tue, 24 Jan 2023 16:39:37 -0800 Subject: [PATCH 33/50] Updatet documentation Updatet documentation, make toc for Readme.md. Move advertising-identifier content to its own file. --- Documentation/README.md | 57 ++----------------- Documentation/advertising-identifier.md | 53 +++++++++++++++++ Documentation/api-reference.md | 2 +- README.md | 2 +- .../identity/app/ui/CustomIdentityFragment.kt | 2 +- 5 files changed, 61 insertions(+), 55 deletions(-) create mode 100644 Documentation/advertising-identifier.md diff --git a/Documentation/README.md b/Documentation/README.md index 5967d96f..81afbe6b 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,53 +1,6 @@ -# Advertising identifier +# Edge Identity extension documentation -## Configuration -To enable advertising identifier features in the sample app, follow these steps: -1. Update the value for key `gms_ads_app_id` located in the `secrets.xml` at [aepsdk-edgeidentity-android/code/app/src/main/res/values](../code/app/src/main/res/values/secrets.xml) with a valid Google AdMob app ID. - - See Google's [quick start reference](https://developers.google.com/admob/android/quick-start) on how to get your AdMob app ID. See step 3 of the [Configure your app](https://developers.google.com/admob/android/quick-start#import_the_mobile_ads_sdk) section for a free public test app ID from Google. - - Any real key values in the `secrets.xml` file should **not** be committed to the repository. -2. By default, the ad ID features are commented out in the sample app. To enable these features, uncomment the implemention code using [find and replace all](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html#replace_search_string_in_project) to replace all instances of: -```java -/* Ad ID implementation -``` -with: -```java -//* Ad ID implementation -``` -Each code block has a pair of block comments wrapped around it to enable this behavior: -```java -/* Ad ID implementation (pt. 1/4) - -/* Ad ID implementation (pt. 1/4) */ -``` - -After replacement it will become: -```java -//* Ad ID implementation (pt. 1/4) - -//* Ad ID implementation (pt. 1/4) */ -``` - -For convenience, these are the default find and replace shortcuts in Android Studio: -[Default shortcuts for find and replace](./assets/find-and-replace-shortcuts.png) - -The shortcut should open a window that looks like the following: -[Example of find and replace](./assets/find-and-replace-all-example.png) -There should be 5 pairs of special comment blocks (10 total matches) across two files: -`app/build.gradle`, `CustomIdentityFragment.kt`, and `SharedViewModel.kt` - -3. With the implementation code and gradle files uncommented with new dependencies, sync the project with the Gradle file changes using: File -> Sync Project with Gradle Files - -[Example of find and replace](./assets/sync-project-gradle-example.png) - -The app should now be properly configured to use advertising identifier features. - -To **disable** these features, follow these steps: -1. [Find and replace](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html#replace_search_string_in_project) all instances of: -```java -//* Ad ID implementation -``` -with: -```java -/* Ad ID implementation -``` -2. Sync Project with Gradle files using: File -> Sync Project with Gradle Files \ No newline at end of file +## Contents +- [Getting started](getting-started.md) +- [API reference](api-reference.md) +- [Getting started test app](getting-started-test-app.md) \ No newline at end of file diff --git a/Documentation/advertising-identifier.md b/Documentation/advertising-identifier.md new file mode 100644 index 00000000..5967d96f --- /dev/null +++ b/Documentation/advertising-identifier.md @@ -0,0 +1,53 @@ +# Advertising identifier + +## Configuration +To enable advertising identifier features in the sample app, follow these steps: +1. Update the value for key `gms_ads_app_id` located in the `secrets.xml` at [aepsdk-edgeidentity-android/code/app/src/main/res/values](../code/app/src/main/res/values/secrets.xml) with a valid Google AdMob app ID. + - See Google's [quick start reference](https://developers.google.com/admob/android/quick-start) on how to get your AdMob app ID. See step 3 of the [Configure your app](https://developers.google.com/admob/android/quick-start#import_the_mobile_ads_sdk) section for a free public test app ID from Google. + - Any real key values in the `secrets.xml` file should **not** be committed to the repository. +2. By default, the ad ID features are commented out in the sample app. To enable these features, uncomment the implemention code using [find and replace all](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html#replace_search_string_in_project) to replace all instances of: +```java +/* Ad ID implementation +``` +with: +```java +//* Ad ID implementation +``` +Each code block has a pair of block comments wrapped around it to enable this behavior: +```java +/* Ad ID implementation (pt. 1/4) + +/* Ad ID implementation (pt. 1/4) */ +``` + +After replacement it will become: +```java +//* Ad ID implementation (pt. 1/4) + +//* Ad ID implementation (pt. 1/4) */ +``` + +For convenience, these are the default find and replace shortcuts in Android Studio: +[Default shortcuts for find and replace](./assets/find-and-replace-shortcuts.png) + +The shortcut should open a window that looks like the following: +[Example of find and replace](./assets/find-and-replace-all-example.png) +There should be 5 pairs of special comment blocks (10 total matches) across two files: +`app/build.gradle`, `CustomIdentityFragment.kt`, and `SharedViewModel.kt` + +3. With the implementation code and gradle files uncommented with new dependencies, sync the project with the Gradle file changes using: File -> Sync Project with Gradle Files + +[Example of find and replace](./assets/sync-project-gradle-example.png) + +The app should now be properly configured to use advertising identifier features. + +To **disable** these features, follow these steps: +1. [Find and replace](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html#replace_search_string_in_project) all instances of: +```java +//* Ad ID implementation +``` +with: +```java +/* Ad ID implementation +``` +2. Sync Project with Gradle files using: File -> Sync Project with Gradle Files \ No newline at end of file diff --git a/Documentation/api-reference.md b/Documentation/api-reference.md index d8f4c5d4..7fa6090b 100644 --- a/Documentation/api-reference.md +++ b/Documentation/api-reference.md @@ -135,7 +135,7 @@ Identity.getUrlVariables(new AdobeCallback() { Registers the Identity for Edge Network extension with the Mobile Core extension. > **Warning** -> Deprecated as of 2.0.0. Use [MobileCore.registerExtensions API](https://github.com/adobe/aepsdk-core-android/blob/main/docs/Usage/MobileCore.md#registering-extensions-and-starting-the-sdk) instead. +> Deprecated as of 2.0.0. Use the [MobileCore.registerExtensions API](https://developer.adobe.com/client-sdks/documentation/mobile-core/api-reference) instead. > **Note** > If your use-case covers both Edge Network and Adobe Experience Cloud Solutions extensions, you need to register Identity for Edge Network and Identity for Experience Cloud Identity Service from Mobile Core extensions. For more details, see the [frequently asked questions](https://aep-sdks.gitbook.io/docs/foundation-extensions/identity-for-edge-network/identity-faq#q-i-am-using-aep-edge-and-adobe-solutions-extensions-which-identity-extension-should-i-install-and-register). diff --git a/README.md b/README.md index df7b5e87..5dd127ff 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ The test app needs to be configured with the following edge extensions before it 2. Select the `app` runnable with the desired emulator and run the program. > **Note** -> To enable GAID related advertising identifier features, follow the [documentation](Documentation/README.md#advertising-identifier) for the required setup steps. +> To enable GAID related advertising identifier features, follow the [documentation](Documentation/advertising-identifier.md) for the required setup steps. **View the platform events with Assurance** diff --git a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt index b651095f..7091c831 100644 --- a/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt +++ b/code/app/src/main/java/com/adobe/marketing/edge/identity/app/ui/CustomIdentityFragment.kt @@ -123,7 +123,7 @@ class CustomIdentityFragment : Fragment() { // Default hint for how to enable ad ID features; overwritten by actual implementation when ad ID features are enabled. root.findViewById