From 91bec834660e485147ed2ee40b683b5c575f6447 Mon Sep 17 00:00:00 2001 From: Tim Hull Date: Fri, 5 Nov 2021 16:33:35 +0000 Subject: [PATCH] 5.0.0 --- CHANGELOG.md | 7 ++ README-CN.md | 2 +- README.md | 22 ++++- .../sdk/example/ExampleApplication.java | 18 ++++ .../sdk/example/ExampleApplication.java | 18 ++++ .../example/ExampleApplication.java | 18 ++++ .../example/ExampleApplication.java | 18 ++++ gradle.properties | 2 +- library-notifications/README-CN.md | 4 +- library-notifications/README.md | 4 +- .../java/com/deltadna/android/sdk/DDNA.java | 57 +++++++++++- .../deltadna/android/sdk/DDNADelegate.java | 6 +- .../com/deltadna/android/sdk/DDNAImpl.java | 15 +++- .../deltadna/android/sdk/DDNANonTracking.java | 4 +- .../deltadna/android/sdk/EventHandler.java | 6 ++ .../android/sdk/consent/ConsentStatus.java | 5 ++ .../android/sdk/consent/ConsentTracker.java | 78 +++++++++++++++++ .../sdk/consent/GeoIpNetworkClient.java | 46 ++++++++++ .../android/sdk/net/NetworkManager.java | 76 ++++++++++------ .../deltadna/android/sdk/DDNADelegateTest.kt | 42 +++++++-- .../com/deltadna/android/sdk/DDNAImplTest.kt | 10 +++ .../android/sdk/DDNANonTrackingTest.kt | 9 ++ .../android/sdk/net/NetworkManagerTest.kt | 86 +++++++++++++++++-- 23 files changed, 506 insertions(+), 47 deletions(-) create mode 100644 library/src/main/java/com/deltadna/android/sdk/consent/ConsentStatus.java create mode 100644 library/src/main/java/com/deltadna/android/sdk/consent/ConsentTracker.java create mode 100644 library/src/main/java/com/deltadna/android/sdk/consent/GeoIpNetworkClient.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 031d1216..b4fbc8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [5.0.0](https://github.com/deltaDNA/android-sdk/releases/tag/5.0.0) + +## New +- **Breaking Change**: Provided APIs for checking if PIPL consent is required, and for registering a user's PIPL consent if so. +Note it is now a requirement to call the API to check for consent, and register any consents if necessary, before collect and engage +responses will be successfully sent to the deltaDNA service. + ## [4.13.6](https://github.com/deltaDNA/android-sdk/releases/tag/4.13.6) ### Fixed diff --git a/README-CN.md b/README-CN.md index b6c7dd18..5fd34d74 100755 --- a/README-CN.md +++ b/README-CN.md @@ -43,7 +43,7 @@ allprojects { ``` 在你APP的构建脚本 ```groovy -compile 'com.deltadna.android:deltadna-sdk:4.13.6' +compile 'com.deltadna.android:deltadna-sdk:5.0.0' ``` ## 初始化 diff --git a/README.md b/README.md index 7e08a50c..e54a795d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ allprojects { In your app's build script: ```groovy dependencies { - implementation 'com.deltadna.android:deltadna-sdk:4.13.6' + implementation 'com.deltadna.android:deltadna-sdk:5.0.0' } ``` The Java source and target compatibility needs to be set to 1.8 in you app's build script: @@ -68,6 +68,10 @@ The SDK needs to be initialised with the following parameters in an `Application * `environmentKey`, a unique 32 character string assigned to your application. You will be assigned separate application keys for development and production builds of your game. You will need to change the environment key that you initialise the SDK with as you move from development and testing to production. * `collectUrl`, this is the address of the server that will be collecting your events. * `engageUrl`, this is the address of the server that will provide real-time A/B Testing and Targeting. + +It is a requirement in versions 5.0.0 and above to check if a user is in a location where PIPL consent is required, and to provide that consent if so. This must +be done before the SDK will send any events or make any engage requests. + ```java public class MyApplication extends Application { @@ -80,6 +84,22 @@ public class MyApplication extends Application { "environmentKey", "collectUrl", "engageUrl")); + + DDNA.instance().isPiplConsentRequired(new ConsentTracker.Callback() { + @Override + public void onSuccess(boolean requiresConsent) { + if (requiresConsent) { + // Put in a consent flow here to check that the user has given their consent, then update the booleans below to match. + DDNA.instance().setPiplConsent(true, true); + } + } + + @Override + public void onFailure(Throwable exception) { + Log.e("EXAMPLE", "Failed to check for PIPL consent", exception); + // Try again later. + } + }); } } ``` diff --git a/examples/demo-forget-me/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java b/examples/demo-forget-me/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java index 3205ac91..31936c4c 100644 --- a/examples/demo-forget-me/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java +++ b/examples/demo-forget-me/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java @@ -17,9 +17,11 @@ package com.deltadna.android.sdk.example; import android.app.Application; +import android.util.Log; import com.deltadna.android.sdk.BuildConfig; import com.deltadna.android.sdk.DDNA; +import com.deltadna.android.sdk.consent.ConsentTracker; public class ExampleApplication extends Application { @@ -39,5 +41,21 @@ public void onCreate() { "http://collect3347ndrds.deltadna.net/collect/api", "http://engage3347ndrds.deltadna.net") .clientVersion(BuildConfig.VERSION_NAME)); + + DDNA.instance().isPiplConsentRequired(new ConsentTracker.Callback() { + @Override + public void onSuccess(boolean requiresConsent) { + if (requiresConsent) { + // In our example, we assume we have consent, but you should check to make sure this is the case! + DDNA.instance().setPiplConsent(true, true); + } + } + + @Override + public void onFailure(Throwable exception) { + Log.e("EXAMPLE", "Failed to check for PIPL consent", exception); + // Try again later. + } + }); } } diff --git a/examples/demo/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java b/examples/demo/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java index 9ce1b329..f79948fb 100644 --- a/examples/demo/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java +++ b/examples/demo/src/main/java/com/deltadna/android/sdk/example/ExampleApplication.java @@ -17,9 +17,11 @@ package com.deltadna.android.sdk.example; import android.app.Application; +import android.util.Log; import com.deltadna.android.sdk.BuildConfig; import com.deltadna.android.sdk.DDNA; +import com.deltadna.android.sdk.consent.ConsentTracker; public class ExampleApplication extends Application { @@ -39,5 +41,21 @@ public void onCreate() { "http://collect3347ndrds.deltadna.net/collect/api", "http://engage3347ndrds.deltadna.net") .clientVersion(BuildConfig.VERSION_NAME)); + + DDNA.instance().isPiplConsentRequired(new ConsentTracker.Callback() { + @Override + public void onSuccess(boolean requiresConsent) { + if (requiresConsent) { + // In our example, we assume we have consent, but you should check to make sure this is the case! + DDNA.instance().setPiplConsent(true, true); + } + } + + @Override + public void onFailure(Throwable exception) { + Log.e("EXAMPLE", "Failed to check for PIPL consent", exception); + // Try again later. + } + }); } } diff --git a/examples/notifications-style/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java b/examples/notifications-style/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java index add144e4..cad84603 100644 --- a/examples/notifications-style/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java +++ b/examples/notifications-style/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java @@ -17,8 +17,10 @@ package com.deltadna.android.sdk.notifications.example; import android.app.Application; +import android.util.Log; import com.deltadna.android.sdk.DDNA; +import com.deltadna.android.sdk.consent.ConsentTracker; public class ExampleApplication extends Application { @@ -31,5 +33,21 @@ public void onCreate() { "07575004106474324897044893014183", "http://collect3347ndrds.deltadna.net/collect/api", "http://engage3347ndrds.deltadna.net")); + + DDNA.instance().isPiplConsentRequired(new ConsentTracker.Callback() { + @Override + public void onSuccess(boolean requiresConsent) { + if (requiresConsent) { + // In our example, we assume we have consent, but you should check to make sure this is the case! + DDNA.instance().setPiplConsent(true, true); + } + } + + @Override + public void onFailure(Throwable exception) { + Log.e("EXAMPLE", "Failed to check for PIPL consent", exception); + // Try again later. + } + }); } } diff --git a/examples/notifications/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java b/examples/notifications/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java index a97873d7..acb5628b 100644 --- a/examples/notifications/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java +++ b/examples/notifications/src/main/java/com/deltadna/android/sdk/notifications/example/ExampleApplication.java @@ -17,8 +17,10 @@ package com.deltadna.android.sdk.notifications.example; import android.app.Application; +import android.util.Log; import com.deltadna.android.sdk.DDNA; +import com.deltadna.android.sdk.consent.ConsentTracker; import com.deltadna.android.sdk.notifications.DDNANotifications; public class ExampleApplication extends Application { @@ -32,6 +34,22 @@ public void onCreate() { "07575004106474324897044893014183", "http://collect3347ndrds.deltadna.net/collect/api", "http://engage3347ndrds.deltadna.net")); + + DDNA.instance().isPiplConsentRequired(new ConsentTracker.Callback() { + @Override + public void onSuccess(boolean requiresConsent) { + if (requiresConsent) { + // In our example, we assume we have consent, but you should check to make sure this is the case! + DDNA.instance().setPiplConsent(true, true); + } + } + + @Override + public void onFailure(Throwable exception) { + Log.e("EXAMPLE", "Failed to check for PIPL consent", exception); + // Try again later. + } + }); // only needs to be called if targeting API 26 or higher DDNANotifications.setReceiver(ExampleReceiver.class); diff --git a/gradle.properties b/gradle.properties index e2d5894e..c3076218 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.deltadna.android -VERSION_NAME=4.13.6 +VERSION_NAME=5.0.0 POM_DESCRIPTION=deltaDNA SDK for Android POM_URL=https://github.com/deltaDNA/android-sdk diff --git a/library-notifications/README-CN.md b/library-notifications/README-CN.md index 40c5700b..9c36574f 100755 --- a/library-notifications/README-CN.md +++ b/library-notifications/README-CN.md @@ -41,8 +41,8 @@ allprojects { ``` 在你APP的构建脚本 ```groovy -compile 'com.deltadna.android:deltadna-sdk:4.13.6' -compile 'com.deltadna.android:deltadna-sdk-notifications:4.13.6' +compile 'com.deltadna.android:deltadna-sdk:5.0.0' +compile 'com.deltadna.android:deltadna-sdk-notifications:5.0.0' ``` ## 整合 diff --git a/library-notifications/README.md b/library-notifications/README.md index d5ae31fc..8828e392 100644 --- a/library-notifications/README.md +++ b/library-notifications/README.md @@ -46,8 +46,8 @@ allprojects { In your app's build script: ```groovy dependencies { - implementation 'com.deltadna.android:deltadna-sdk:4.13.6' - implementation 'com.deltadna.android:deltadna-sdk-notifications:4.13.6' + implementation 'com.deltadna.android:deltadna-sdk:5.0.0' + implementation 'com.deltadna.android:deltadna-sdk-notifications:5.0.0' } ``` diff --git a/library/src/main/java/com/deltadna/android/sdk/DDNA.java b/library/src/main/java/com/deltadna/android/sdk/DDNA.java index d7d014d2..f498d92a 100644 --- a/library/src/main/java/com/deltadna/android/sdk/DDNA.java +++ b/library/src/main/java/com/deltadna/android/sdk/DDNA.java @@ -17,10 +17,14 @@ package com.deltadna.android.sdk; import android.app.Application; +import android.content.Context; import android.os.Bundle; import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Log; + +import com.deltadna.android.sdk.consent.ConsentTracker; +import com.deltadna.android.sdk.consent.GeoIpNetworkClient; import com.deltadna.android.sdk.exceptions.NotInitialisedException; import com.deltadna.android.sdk.helpers.ClientInfo; import com.deltadna.android.sdk.helpers.Preconditions; @@ -131,6 +135,12 @@ public static synchronized DDNA instance() { final Preferences preferences; final NetworkManager network; private final EngageFactory engageFactory; + + /** + * INTERNAL USE ONLY. Do not access this property directly. + * Please use isPiplConsentRequired and setPiplConsent on the main DDNA instance to check for and provide consent. + */ + public final ConsentTracker consentTracker; String sessionId = UUID.randomUUID().toString(); @@ -156,6 +166,10 @@ public static synchronized DDNA instance() { engageUrl, settings, hashSecret); + consentTracker = new ConsentTracker( + application.getSharedPreferences("com.deltadna.android.sdk.prefs", Context.MODE_PRIVATE), + new GeoIpNetworkClient(network) + ); engageFactory = new EngageFactory(this); } @@ -409,7 +423,48 @@ public abstract DDNA requestEngagement( abstract ImageMessageStore getImageMessageStore(); abstract Map getIso4217(); - + + /** + * Checks if any PIPL user consents are required before sending data from the device. + * This method must be called before starting the SDK. + * + * The callback parameter will be true if consent needs to be checked, and false if either + * consent has previously been gathered, or if consent is not required in the user's current + * location. + */ + public void isPiplConsentRequired(ConsentTracker.Callback callback) { + consentTracker.isPiplConsentRequired(new ConsentTracker.Callback() { + @Override + public void onSuccess(boolean requiresConsent) { + if (!requiresConsent && instance != null && isStarted()) { + requestSessionConfiguration(); + } + callback.onSuccess(requiresConsent); + } + + @Override + public void onFailure(Throwable exception) { + callback.onFailure(exception); + } + }); + } + + /** + * Registers a user's consent (or lack of consent) to have their data used and / or exported + * under PIPL legislation. Call isPiplConsentRequired first to check if this method is needed + * or not. + */ + public void setPiplConsent(boolean consentForUse, boolean consentForExport) { + if (!consentForUse) { + forgetMe(); + } + if (consentForUse && consentForExport && instance != null && isStarted()) { + // If we've already set up the SDK, refresh the SDK to allow relevant configurations to be fetched + requestSessionConfiguration(); + } + consentTracker.setConsents(consentForUse, consentForExport); + } + /** * Changes the session id. * diff --git a/library/src/main/java/com/deltadna/android/sdk/DDNADelegate.java b/library/src/main/java/com/deltadna/android/sdk/DDNADelegate.java index 6bc37b61..dfdd9722 100644 --- a/library/src/main/java/com/deltadna/android/sdk/DDNADelegate.java +++ b/library/src/main/java/com/deltadna/android/sdk/DDNADelegate.java @@ -19,6 +19,8 @@ import android.os.Bundle; import androidx.annotation.Nullable; import android.text.TextUtils; + +import com.deltadna.android.sdk.consent.ConsentTracker; import com.deltadna.android.sdk.listeners.EngageListener; import com.deltadna.android.sdk.listeners.EventListener; import com.deltadna.android.sdk.listeners.internal.IEventListener; @@ -183,7 +185,7 @@ public DDNA stopTrackingMe() { return nonTracking; } - + @Override ImageMessageStore getImageMessageStore() { return getDelegate().getImageMessageStore(); @@ -195,7 +197,7 @@ Map getIso4217() { } private DDNA getDelegate() { - if (preferences.isForgetMe() || preferences.isForgotten() || preferences.isStopTrackingMe()) { + if (preferences.isForgetMe() || preferences.isForgotten() || preferences.isStopTrackingMe() || DDNA.instance().consentTracker.isConsentDenied()) { return nonTracking; } else { return tracking; diff --git a/library/src/main/java/com/deltadna/android/sdk/DDNAImpl.java b/library/src/main/java/com/deltadna/android/sdk/DDNAImpl.java index 80b6d352..2b8e6b02 100644 --- a/library/src/main/java/com/deltadna/android/sdk/DDNAImpl.java +++ b/library/src/main/java/com/deltadna/android/sdk/DDNAImpl.java @@ -22,6 +22,7 @@ import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Log; + import com.deltadna.android.sdk.exceptions.NotStartedException; import com.deltadna.android.sdk.exceptions.SessionConfigurationException; import com.deltadna.android.sdk.helpers.ClientInfo; @@ -90,6 +91,11 @@ public DDNA startSdk() { @Override public DDNA startSdk(@Nullable String userId) { Log.d(TAG, "Starting SDK"); + + if (!consentTracker.hasCheckedForConsent()) { + Log.w(TAG, "In version 5.0.0 and above, it is a requirement to check if user consent is required before events will be sent. " + + "Events will not be sent until this check is performed and any required consents are provided."); + } if (started) { Log.w(TAG, "SDK already started"); @@ -243,6 +249,13 @@ public DDNA requestEngagement(String decisionPoint, EngageListener l public DDNA requestEngagement(E engagement, EngageListener listener) { Preconditions.checkArg(engagement != null, "engagement cannot be null"); Preconditions.checkArg(listener != null, "listener cannot be null"); + + if (!DDNA.instance().consentTracker.hasCheckedForConsent()) { + Log.w(TAG, "You need to check for user consent before making engagement requests."); + listener.onCompleted((E) engagement.setResponse(new Response<>( + 200, false, new byte[] {}, new JSONObject(), null))); + return this; + } if (!started) { Log.w(TAG, "SDK has not been started, aborting engagement " + engagement); @@ -389,7 +402,7 @@ public DDNA forgetMe() { public DDNA stopTrackingMe() { return stopSdk(); } - + @Override ImageMessageStore getImageMessageStore() { return imageMessageStore; diff --git a/library/src/main/java/com/deltadna/android/sdk/DDNANonTracking.java b/library/src/main/java/com/deltadna/android/sdk/DDNANonTracking.java index ffeaf48a..f8856693 100644 --- a/library/src/main/java/com/deltadna/android/sdk/DDNANonTracking.java +++ b/library/src/main/java/com/deltadna/android/sdk/DDNANonTracking.java @@ -23,6 +23,8 @@ import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Log; + +import com.deltadna.android.sdk.consent.ConsentTracker; import com.deltadna.android.sdk.helpers.Settings; import com.deltadna.android.sdk.listeners.EngageListener; import com.deltadna.android.sdk.listeners.EventListener; @@ -205,7 +207,7 @@ public synchronized DDNA stopTrackingMe() { return this; } - + @Override ImageMessageStore getImageMessageStore() { // ok as we should never get far enough to use the store diff --git a/library/src/main/java/com/deltadna/android/sdk/EventHandler.java b/library/src/main/java/com/deltadna/android/sdk/EventHandler.java index be37e51d..5a9c35b8 100644 --- a/library/src/main/java/com/deltadna/android/sdk/EventHandler.java +++ b/library/src/main/java/com/deltadna/android/sdk/EventHandler.java @@ -281,6 +281,12 @@ private final class Upload implements Runnable { @Override public void run() { Log.v(TAG, "Starting event upload"); + + if (!DDNA.instance().consentTracker.allConsentsAreMet()) { + Log.w(TAG, "PIPL consents have not been checked or have been denied, cannot send Collect request."); + return; + } + final CloseableIterator items = events.items(); final AtomicReference clearEvents = new AtomicReference<>(CloseableIterator.Mode.ALL); diff --git a/library/src/main/java/com/deltadna/android/sdk/consent/ConsentStatus.java b/library/src/main/java/com/deltadna/android/sdk/consent/ConsentStatus.java new file mode 100644 index 00000000..94335be1 --- /dev/null +++ b/library/src/main/java/com/deltadna/android/sdk/consent/ConsentStatus.java @@ -0,0 +1,5 @@ +package com.deltadna.android.sdk.consent; + +public enum ConsentStatus { + unknown, notRequired, requiredButUnchecked, consentGiven, consentDenied +} diff --git a/library/src/main/java/com/deltadna/android/sdk/consent/ConsentTracker.java b/library/src/main/java/com/deltadna/android/sdk/consent/ConsentTracker.java new file mode 100644 index 00000000..f95eaa6e --- /dev/null +++ b/library/src/main/java/com/deltadna/android/sdk/consent/ConsentTracker.java @@ -0,0 +1,78 @@ +package com.deltadna.android.sdk.consent; + +import android.content.SharedPreferences; + +public class ConsentTracker { + private final SharedPreferences sharedPreferencesInstance; + private final GeoIpNetworkClient geoIpNetworkClient; + + private final static String useKey = "ddnaPiplUseConsent"; + private final static String exportKey = "ddnaPiplExportConsent"; + + public ConsentStatus useConsentStatus; + public ConsentStatus exportConsentStatus; + + public interface Callback { + void onSuccess(boolean requiresConsent); + void onFailure(Throwable exception); + } + + public ConsentTracker(SharedPreferences sharedPreferences, GeoIpNetworkClient geoIpNetworkClient) { + this.sharedPreferencesInstance = sharedPreferences; + this.geoIpNetworkClient = geoIpNetworkClient; + + String useConsent = sharedPreferences.getString(useKey, ConsentStatus.unknown.name()); + useConsentStatus = ConsentStatus.valueOf(useConsent); + String exportConsent = sharedPreferences.getString(exportKey, ConsentStatus.unknown.name()); + exportConsentStatus = ConsentStatus.valueOf(exportConsent); + } + + public boolean hasCheckedForConsent() { + return isACheckedStatus(useConsentStatus) && isACheckedStatus(exportConsentStatus); + } + + public boolean isConsentDenied() { + return exportConsentStatus == ConsentStatus.consentDenied || useConsentStatus == ConsentStatus.consentDenied; + } + + public void isPiplConsentRequired(Callback callback) { + if (hasCheckedForConsent()) { + callback.onSuccess(false); + return; + } + geoIpNetworkClient.fetchGeoIpResponse(new GeoIpNetworkClient.Callback() { + @Override + public void OnSuccess(String consentIdentifier) { + boolean isConsentNeeded = consentIdentifier.equals("pipl"); + ConsentStatus status = isConsentNeeded ? ConsentStatus.requiredButUnchecked : ConsentStatus.notRequired; + useConsentStatus = status; + exportConsentStatus = status; + callback.onSuccess(isConsentNeeded); + } + + @Override + public void OnFailure(Throwable exception) { + callback.onFailure(exception); + } + }); + } + + public void setConsents(boolean useConsent, boolean exportConsent) { + useConsentStatus = useConsent ? ConsentStatus.consentGiven : ConsentStatus.consentDenied; + exportConsentStatus = exportConsent ? ConsentStatus.consentGiven : ConsentStatus.consentDenied; + + sharedPreferencesInstance.edit() + .putString(useConsentStatus.name(), useKey) + .putString(exportConsentStatus.name(), exportKey) + .apply(); + } + + public boolean allConsentsAreMet() { + return (useConsentStatus == ConsentStatus.consentGiven || useConsentStatus == ConsentStatus.notRequired) + && (exportConsentStatus == ConsentStatus.consentGiven || exportConsentStatus == ConsentStatus.notRequired); + } + + private boolean isACheckedStatus(ConsentStatus status) { + return status == ConsentStatus.consentDenied || status == ConsentStatus.consentGiven || status == ConsentStatus.notRequired; + } +} diff --git a/library/src/main/java/com/deltadna/android/sdk/consent/GeoIpNetworkClient.java b/library/src/main/java/com/deltadna/android/sdk/consent/GeoIpNetworkClient.java new file mode 100644 index 00000000..c4745cbc --- /dev/null +++ b/library/src/main/java/com/deltadna/android/sdk/consent/GeoIpNetworkClient.java @@ -0,0 +1,46 @@ +package com.deltadna.android.sdk.consent; + +import com.deltadna.android.sdk.listeners.RequestListener; +import com.deltadna.android.sdk.net.NetworkManager; +import com.deltadna.android.sdk.net.Response; + +import org.json.JSONException; +import org.json.JSONObject; + +public class GeoIpNetworkClient { + static final String geoIpUrl = "https://pls.prd.mz.internal.unity3d.com/api/v1/user-lookup"; + + private final NetworkManager networkManager; + + public GeoIpNetworkClient(NetworkManager networkManager) { + this.networkManager = networkManager; + } + + // Note: In the other SDKs, we deserialize the whole response and return it from the network client. + // However, due to the JSON library in use in this SDK, and the structure of our main network manager, + // it is not trivial to do this, so as we only use the identifier anyway and that is easy to extract, + // we just use that instead. + interface Callback { + void OnSuccess(String consentIdentifier); + void OnFailure(Throwable exception); + } + + public void fetchGeoIpResponse(Callback callback) { + networkManager.get(geoIpUrl, new RequestListener() { + @Override + public void onCompleted(Response response) { + try { + String identifier = response.body.getString("identifier"); + callback.OnSuccess(identifier); + } catch (JSONException e) { + callback.OnFailure(e); + } + } + + @Override + public void onError(Throwable t) { + callback.OnFailure(t); + } + }); + } +} diff --git a/library/src/main/java/com/deltadna/android/sdk/net/NetworkManager.java b/library/src/main/java/com/deltadna/android/sdk/net/NetworkManager.java index c155fb57..0f16e64a 100644 --- a/library/src/main/java/com/deltadna/android/sdk/net/NetworkManager.java +++ b/library/src/main/java/com/deltadna/android/sdk/net/NetworkManager.java @@ -19,6 +19,8 @@ import androidx.annotation.Nullable; import android.util.Log; import com.deltadna.android.sdk.BuildConfig; +import com.deltadna.android.sdk.DDNA; +import com.deltadna.android.sdk.consent.ConsentStatus; import com.deltadna.android.sdk.helpers.Settings; import com.deltadna.android.sdk.listeners.RequestListener; import org.json.JSONObject; @@ -75,23 +77,34 @@ public NetworkManager( dispatcher = new NetworkDispatcher(); } + + public CancelableRequest get(String url, @Nullable RequestListener listener) { + Request request = new Request.Builder() + .get() + .url(url) + .maxRetries(settings.getHttpRequestMaxRetries()) + .retryDelay(settings.getHttpRequestRetryDelay() * 1000) + .build(); + return dispatcher.enqueue(request, ResponseBodyConverter.JSON, listener); + } public CancelableRequest collect( JSONObject payload, @Nullable RequestListener listener) { + + Request.Builder builder = new Request.Builder() + .post(RequestBody.json(payload)) + .url(payload.has("eventList") + ? buildHashedEndpoint(collectUrl + "/bulk", payload.toString()) + : buildHashedEndpoint(collectUrl, payload.toString())) + .header("Accept", "application/json") + .maxRetries(settings.getHttpRequestMaxRetries()) + .retryDelay(settings.getHttpRequestRetryDelay() * 1000) + .connectionTimeout(settings.getHttpRequestCollectTimeout() * 1000); + + addPIPLHeadersToRequest(builder); - return dispatcher.enqueue( - new Request.Builder() - .post(RequestBody.json(payload)) - .url(payload.has("eventList") - ? buildHashedEndpoint(collectUrl + "/bulk", payload.toString()) - : buildHashedEndpoint(collectUrl, payload.toString())) - .header("Accept", "application/json") - .maxRetries(settings.getHttpRequestMaxRetries()) - .retryDelay(settings.getHttpRequestRetryDelay() * 1000) - .connectionTimeout(settings.getHttpRequestCollectTimeout() * 1000) - .build(), - listener); + return dispatcher.enqueue(builder.build(), listener); } public CancelableRequest engage( @@ -105,15 +118,18 @@ public CancelableRequest engage( public CancelableRequest engage(JSONObject payload, RequestListener listener, boolean isConfigurationRequest){ - int timeoutInSeconds = isConfigurationRequest ? settings.getHttpRequestConfigTimeout() : settings.getHttpRequestEngageTimeout(); + + Request.Builder builder = new Request.Builder() + .post(RequestBody.json(payload)) + .url(buildHashedEndpoint(engageUrl, payload.toString())) + .header("Accept", "application/json") + .connectionTimeout(timeoutInSeconds * 1000); + + addPIPLHeadersToRequest(builder); + return dispatcher.enqueue( - new Request.Builder() - .post(RequestBody.json(payload)) - .url(buildHashedEndpoint(engageUrl, payload.toString())) - .header("Accept", "application/json") - .connectionTimeout(timeoutInSeconds * 1000) - .build(), + builder.build(), ResponseBodyConverter.JSON, listener); } @@ -122,14 +138,17 @@ public CancelableRequest fetch( String url, final File dest, RequestListener listener) { + + Request.Builder builder = new Request.Builder() + .get() + .url(url) + .connectionTimeout(settings.getHttpRequestEngageTimeout() * 1000); + + addPIPLHeadersToRequest(builder); // TODO tweak timeouts as this should come back quickly as well return dispatcher.enqueue( - new Request.Builder() - .get() - .url(url) - .connectionTimeout(settings.getHttpRequestEngageTimeout() * 1000) - .build(), + builder.build(), new ResponseBodyConverter() { @Override public File convert(byte[] input) throws Exception { @@ -164,4 +183,13 @@ private String buildHashedEndpoint(String endpoint, String payload) { return builder.toString(); } + + private void addPIPLHeadersToRequest(Request.Builder requestBuilder) { + if (DDNA.instance().consentTracker.useConsentStatus == ConsentStatus.consentGiven) { + requestBuilder.header("PIPL_CONSENT", ""); + } + if (DDNA.instance().consentTracker.exportConsentStatus == ConsentStatus.consentGiven) { + requestBuilder.header("PIPL_EXPORT", ""); + } + } } diff --git a/library/src/test/java/com/deltadna/android/sdk/DDNADelegateTest.kt b/library/src/test/java/com/deltadna/android/sdk/DDNADelegateTest.kt index ca9313ec..b7866f6b 100644 --- a/library/src/test/java/com/deltadna/android/sdk/DDNADelegateTest.kt +++ b/library/src/test/java/com/deltadna/android/sdk/DDNADelegateTest.kt @@ -17,6 +17,7 @@ package com.deltadna.android.sdk import android.os.Bundle +import com.deltadna.android.sdk.consent.ConsentStatus import com.deltadna.android.sdk.listeners.EngageListener import com.google.common.truth.Truth.assertThat import com.nhaarman.mockito_kotlin.* @@ -42,13 +43,20 @@ class DDNADelegateTest { fun before() { tracking = mock() nonTracking = mock() + + val config = DDNA.Configuration( + application, + "environmentKey", + "collectUrl", + "engageUrl") + + DDNA.initialise(config) + + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.unknown + DDNA.instance().consentTracker.exportConsentStatus = ConsentStatus.unknown uut = DDNADelegate( - DDNA.Configuration( - application, - "environmentKey", - "collectUrl", - "engageUrl"), + config, mutableSetOf(), mutableSetOf(), tracking, @@ -351,6 +359,30 @@ class DDNADelegateTest { verifyZeroInteractions(tracking) } + + @Test + fun forwardsToNonTrackingAfterPIPLOptOut() { + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentDenied + + uut.startSdk() + verify(nonTracking).startSdk() + } + + @Test + fun forwardsToTrackingIfPIPLOptIn() { + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentGiven + + uut.startSdk() + verify(tracking).startSdk() + } + + @Test + fun forwardsToTrackingIfPIPLNotRequired() { + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.notRequired + + uut.startSdk() + verify(tracking).startSdk() + } private class KEvent(name: String) : Event(name) private class KEngagement(point: String) : Engagement(point) diff --git a/library/src/test/java/com/deltadna/android/sdk/DDNAImplTest.kt b/library/src/test/java/com/deltadna/android/sdk/DDNAImplTest.kt index ca269ef2..50f5552e 100644 --- a/library/src/test/java/com/deltadna/android/sdk/DDNAImplTest.kt +++ b/library/src/test/java/com/deltadna/android/sdk/DDNAImplTest.kt @@ -16,6 +16,8 @@ package com.deltadna.android.sdk +import com.deltadna.android.sdk.consent.ConsentStatus +import com.deltadna.android.sdk.consent.ConsentTracker import com.deltadna.android.sdk.exceptions.NotStartedException import com.deltadna.android.sdk.exceptions.SessionConfigurationException import com.deltadna.android.sdk.helpers.Settings @@ -56,6 +58,11 @@ class DDNAImplTest { server = MockWebServer() server.start() + DDNA.initialise(DDNA.Configuration(RuntimeEnvironment.application, + "environmentKey", + server.url("/collect").toString(), + server.url("/engage").toString())) + uut = DDNAImpl( RuntimeEnvironment.application, "environmentKey", @@ -68,6 +75,9 @@ class DDNAImplTest { null, mutableSetOf(), mutableSetOf()) + + DDNA.instance().consentTracker.exportConsentStatus = ConsentStatus.consentGiven + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentGiven } @After diff --git a/library/src/test/java/com/deltadna/android/sdk/DDNANonTrackingTest.kt b/library/src/test/java/com/deltadna/android/sdk/DDNANonTrackingTest.kt index 851862b6..2042a88d 100644 --- a/library/src/test/java/com/deltadna/android/sdk/DDNANonTrackingTest.kt +++ b/library/src/test/java/com/deltadna/android/sdk/DDNANonTrackingTest.kt @@ -17,6 +17,7 @@ package com.deltadna.android.sdk import android.os.Bundle +import com.deltadna.android.sdk.consent.ConsentStatus import com.deltadna.android.sdk.helpers.Settings import com.deltadna.android.sdk.listeners.EngageListener import com.deltadna.android.sdk.listeners.EventListener @@ -49,6 +50,11 @@ class DDNANonTrackingTest { fun before() { server = MockWebServer() server.start() + + DDNA.initialise(DDNA.Configuration(RuntimeEnvironment.application, + "environmentKey", + server.url("/collect").toString(), + server.url("/engage").toString())) uut = DDNANonTracking( application, @@ -60,6 +66,9 @@ class DDNANonTrackingTest { null, mutableSetOf(), mutableSetOf()) + + DDNA.instance().consentTracker.exportConsentStatus = ConsentStatus.consentGiven + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentGiven } @After diff --git a/library/src/test/java/com/deltadna/android/sdk/net/NetworkManagerTest.kt b/library/src/test/java/com/deltadna/android/sdk/net/NetworkManagerTest.kt index 2572a7aa..24da0c98 100644 --- a/library/src/test/java/com/deltadna/android/sdk/net/NetworkManagerTest.kt +++ b/library/src/test/java/com/deltadna/android/sdk/net/NetworkManagerTest.kt @@ -16,6 +16,9 @@ package com.deltadna.android.sdk.net +import android.app.Application +import com.deltadna.android.sdk.DDNA +import com.deltadna.android.sdk.consent.ConsentStatus import com.deltadna.android.sdk.helpers.Settings import com.google.common.truth.Truth.assertThat import com.nhaarman.mockito_kotlin.mock @@ -32,10 +35,12 @@ import org.junit.runner.Description import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.runners.model.Statement +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment import java.io.File import java.nio.charset.Charset -@RunWith(JUnit4::class) +@RunWith(RobolectricTestRunner::class) class NetworkManagerTest { @Suppress("unused") // accessed by test framework @@ -67,8 +72,8 @@ class NetworkManagerTest { } } - private var uut: NetworkManager? = null - private var server: MockWebServer? = null + private lateinit var uut: NetworkManager + private lateinit var server: MockWebServer @Before fun before() { @@ -77,7 +82,13 @@ class NetworkManagerTest { server = MockWebServer() server!!.start() - + + DDNA.initialise(DDNA.Configuration( + RuntimeEnvironment.application, + "envKey", + "collectUrl", + "engageUrl")) + uut = NetworkManager( ENV_KEY, server!!.url(COLLECT).toString(), @@ -88,8 +99,7 @@ class NetworkManagerTest { @After fun after() { - server!!.shutdown() - uut = null + server.shutdown() } @Test @@ -104,6 +114,36 @@ class NetworkManagerTest { assertThat(body.readUtf8()).isEqualTo("{\"field\":1}") } } + + @Test + fun collectPiplHeadersConsentProvided() { + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentGiven + DDNA.instance().consentTracker.exportConsentStatus = ConsentStatus.consentGiven + + server.enqueue(MockResponse().setResponseCode(200)) + + uut.collect(JSONObject().put("eventList", "[]"), null) + + with(server.takeRequest()) { + assertThat(headers["PIPL_CONSENT"]).isNotNull() + assertThat(headers["PIPL_EXPORT"]).isNotNull() + } + } + + @Test + fun collectPiplHeadersConsentNotProvided() { + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentDenied + DDNA.instance().consentTracker.exportConsentStatus = ConsentStatus.consentDenied + + server.enqueue(MockResponse().setResponseCode(200)) + + uut.collect(JSONObject().put("eventList", "[]"), null) + + with(server.takeRequest()) { + assertThat(headers["PIPL_CONSENT"]).isNull() + assertThat(headers["PIPL_EXPORT"]).isNull() + } + } @Test fun collectBulk() { @@ -179,6 +219,40 @@ class NetworkManagerTest { assertThat(server!!.takeRequest().path) .startsWith("$ENGAGE/$ENV_KEY/hash") } + + @Test + fun engagePiplHeadersConsentProvided() { + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentGiven + DDNA.instance().consentTracker.exportConsentStatus = ConsentStatus.consentGiven + + server.enqueue(MockResponse() + .setResponseCode(200) + .setBody("{\"result\":1}")) + + uut.engage(JSONObject().put("field", 1), mock()) + + with(server.takeRequest()) { + assertThat(headers["PIPL_CONSENT"]).isNotNull() + assertThat(headers["PIPL_EXPORT"]).isNotNull() + } + } + + @Test + fun engagePiplHeadersConsentNotProvided() { + DDNA.instance().consentTracker.useConsentStatus = ConsentStatus.consentDenied + DDNA.instance().consentTracker.exportConsentStatus = ConsentStatus.consentDenied + + server.enqueue(MockResponse() + .setResponseCode(200) + .setBody("{\"result\":1}")) + + uut.engage(JSONObject().put("field", 1), mock()) + + with(server!!.takeRequest()) { + assertThat(headers["PIPL_CONSENT"]).isNull() + assertThat(headers["PIPL_EXPORT"]).isNull() + } + } @Test fun fetch() {