diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d500440..3646f838c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## XX.XX.XX +* ! Minor breaking change ! All active views will now automatically stop when consent for "views" is revoked. + * The Android SDK now supports Android 15 (API level 35) * The views will be stopped and restarted now while going to the background or foreground instead of resuming and pausing. diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java index acad0e10e..7e412a1e9 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java @@ -1855,6 +1855,113 @@ public void recordView_previousViewName() throws JSONException { validateView("test2", 0.0, 1, 2, false, true, TestUtils.map(), "_CLY_", "_CLY_", "test"); } + /** + * "startView" with consent removal + * Validate that all running views are stopped when the view consent is removed + * + * @throws JSONException if the JSON is not valid + */ + @Test + public void startView_consentRemoval() throws JSONException { + CountlyConfig countlyConfig = TestUtils.createBaseConfig(); + countlyConfig.setRequiresConsent(true); + countlyConfig.setLoggingEnabled(true); + countlyConfig.giveAllConsents(); + countlyConfig.setEventQueueSizeToSend(1); + + Countly countly = new Countly().init(countlyConfig); + + countly.views().startView("test"); + ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 0); + validateView("test", 0.0, 1, 2, true, true, TestUtils.map(), "_CLY_", "_CLY_", null); + + countly.views().startView("test2"); + validateView("test2", 0.0, 2, 3, false, true, TestUtils.map(), "_CLY_", "_CLY_", null); + + countly.consent().removeConsent(new String[] { Countly.CountlyFeatureNames.views }); + validateView("test", 0.0, 3, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null); + validateView("test2", 0.0, 4, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null); + ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 5, new boolean[] { true, true, true, true, true, true, true, true, true, true, true, true, false, true, true }); + + countly.consent().giveConsent(new String[] { Countly.CountlyFeatureNames.views }); + ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 6); + Assert.assertEquals(7, TestUtils.getCurrentRQ().length); + } + + /** + * "startAutoStoppedView" with consent removal + * Validate that running view is stopped when the view consent is removed + * + * @throws JSONException if the JSON is not valid + */ + @Test + public void startAutoStoppedView_consentRemoval() throws JSONException { + CountlyConfig countlyConfig = TestUtils.createBaseConfig(); + countlyConfig.setRequiresConsent(true); + countlyConfig.setLoggingEnabled(true); + countlyConfig.giveAllConsents(); + countlyConfig.setEventQueueSizeToSend(1); + + Countly countly = new Countly().init(countlyConfig); + + countly.views().startAutoStoppedView("test"); + ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 0); + validateView("test", 0.0, 1, 2, true, true, TestUtils.map(), "_CLY_", "_CLY_", null); + + countly.views().startAutoStoppedView("test2"); + validateView("test", 0.0, 2, 4, false, false, TestUtils.map(), "_CLY_", "_CLY_", null); + validateView("test2", 0.0, 3, 4, false, true, TestUtils.map(), "_CLY_", "_CLY_", null); + + countly.consent().removeConsent(new String[] { Countly.CountlyFeatureNames.views }); + validateView("test2", 0.0, 4, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null); + ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 5, new boolean[] { true, true, true, true, true, true, true, true, true, true, true, true, false, true, true }); + + countly.consent().giveConsent(new String[] { Countly.CountlyFeatureNames.views }); + ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 6); + Assert.assertEquals(7, TestUtils.getCurrentRQ().length); + } + + /** + * Auto view tracking with consent removal + * Validate that running view is stopped when the view consent is removed + * + * @throws JSONException if the JSON is not valid + */ + @Test + public void autoViewTracking_consentRemoval() throws JSONException { + CountlyConfig countlyConfig = TestUtils.createBaseConfig(TestUtils.getContext()); + countlyConfig.setRequiresConsent(true); + countlyConfig.setLoggingEnabled(true); + countlyConfig.enableAutomaticViewTracking(); + countlyConfig.giveAllConsents(); + countlyConfig.setEventQueueSizeToSend(1); + + Countly countly = new Countly().init(countlyConfig); + + Activity activity = Mockito.mock(Activity.class); + countly.onStart(activity); + + ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 0); + ModuleSessionsTests.validateSessionBeginRequest(1, TestUtils.commonDeviceId); + ModuleEventsTests.validateEventInRQ("[CLY]_orientation", TestUtils.map("mode", "portrait"), 1, 0, 0, 2, 4); + + validateView(activity.getClass().getName(), 0.0, 3, 4, true, true, TestUtils.map(), "_CLY_", "_CLY_", null); + + Activity activity2 = Mockito.mock(Activity.class); + countly.onStart(activity2); + validateView(activity.getClass().getName(), 0.0, 4, 6, false, false, TestUtils.map(), "_CLY_", "_CLY_", null); + validateView(activity2.getClass().getName(), 0.0, 5, 6, false, true, TestUtils.map(), "_CLY_", "_CLY_", null); + + countly.consent().removeConsent(new String[] { Countly.CountlyFeatureNames.views }); + validateView(activity2.getClass().getName(), 0.0, 6, 8, false, false, TestUtils.map(), "_CLY_", "_CLY_", null); + ModuleConsentTests.validateConsentRequest(TestUtils.commonDeviceId, 7, new boolean[] { true, true, true, true, true, true, true, true, true, true, true, true, false, true, true }); + + countly.consent().giveConsent(new String[] { Countly.CountlyFeatureNames.views }); + + ModuleConsentTests.validateAllConsentRequest(TestUtils.commonDeviceId, 8); + Assert.assertEquals(9, TestUtils.getCurrentRQ().length); + } + static void validateView(String viewName, Double viewDuration, int idx, int size, boolean start, boolean visit, Map customSegmentation, String id, String pvid) throws JSONException { validateView(viewName, viewDuration, idx, size, start, visit, customSegmentation, id, pvid, null); } @@ -1877,6 +1984,5 @@ static void validateView(String viewName, Double viewDuration, int idx, int size ModuleEventsTests.validateEventInRQ(TestUtils.commonDeviceId, ModuleViews.VIEW_EVENT_KEY, viewSegmentation, 1, 0.0, viewDuration, id, pvid, "_CLY_", "_CLY_", idx, size, 0, 1); } - //todo extract orientation tests } diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java index df4e1cf04..e24eb24b6 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/scSE_SessionsTests.java @@ -312,7 +312,7 @@ public void SE_206_CR_CNG_A_id_change() throws InterruptedException { flowAutomaticSessions(countly, new TestLifecycleObserver()); - Assert.assertEquals(5, TestUtils.getCurrentRQ().length); + Assert.assertEquals(6, TestUtils.getCurrentRQ().length); validateSessionConsentRequest(0, false, TestUtils.commonDeviceId); validateRequest(TestUtils.map("location", ""), 1); TestUtils.validateRequest("newID", TestUtils.map("old_device_id", TestUtils.commonDeviceId), 2); diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java index 0ad6655d4..65ef158f4 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java @@ -95,6 +95,9 @@ void deviceIdChanged(boolean withoutMerge) { void onConsentChanged(@NonNull final List consentChangeDelta, final boolean newConsent, @NonNull final ModuleConsent.ConsentChangeSource changeSource) { } + void consentWillChange(@NonNull List consentThatWillChange, final boolean isConsentGiven) { + } + //notify the SDK modules that internal configuration was updated void sdkConfigurationChanged() { diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java index be6265a27..68f410463 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java @@ -8,6 +8,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class ModuleConsent extends ModuleBase implements ConsentProvider { Consent consentInterface = null; @@ -213,6 +214,7 @@ void setConsentInternal(@Nullable final String[] featureNames, final boolean isC } List consentThatWillChange = new ArrayList<>(featureNames.length); + Map consentUpdateMap = new ConcurrentHashMap<>(); for (String featureName : featureNames) { if (!isValidFeatureName(featureName)) { @@ -223,12 +225,17 @@ void setConsentInternal(@Nullable final String[] featureNames, final boolean isC if (getConsentTrue(featureName) != isConsentGiven) { //if the current consent does not match the one give, add it to the list consentThatWillChange.add(featureName); - - //set new consent value - featureConsentValues.put(featureName, isConsentGiven); + //set new consent values later because some modules need to do operation before changing consent + consentUpdateMap.put(featureName, isConsentGiven); } } + for (ModuleBase module : _cly.modules) { + module.consentWillChange(consentThatWillChange, isConsentGiven); + } + + featureConsentValues.putAll(consentUpdateMap); + for (ModuleBase module : _cly.modules) { module.onConsentChanged(consentThatWillChange, isConsentGiven, changeSource); } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java b/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java index 176edeaf5..9902351c7 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleViews.java @@ -272,6 +272,7 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map customViewSeg //only record view if the view name is not null if (vd.viewName == null) { - L.e("[ModuleViews] stopViewWithIDInternal, view has no internal name, ignoring it"); + L.e("[ModuleViews] recordViewEndEvent, view has no internal name, ignoring it"); return; } @@ -506,6 +507,13 @@ void onConfigurationChanged(Configuration newConfig) { } } + @Override + void consentWillChange(@NonNull List consentThatWillChange, final boolean isConsentGiven) { + if (consentThatWillChange.contains(Countly.CountlyFeatureNames.views) && !isConsentGiven) { + stopAllViewsInternal(null); + } + } + @Override void onActivityStopped(int updatedActivityCount) { if (autoViewTracker) {