Skip to content

Commit

Permalink
Merge pull request #403 from Countly/stop_views_consent
Browse files Browse the repository at this point in the history
[Android] end views on consent removal
  • Loading branch information
turtledreams authored Oct 28, 2024
2 parents 9aacfae + 3c96d6d commit d139e66
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
108 changes: 107 additions & 1 deletion sdk/src/androidTest/java/ly/count/android/sdk/ModuleViewsTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> customSegmentation, String id, String pvid) throws JSONException {
validateView(viewName, viewDuration, idx, size, start, visit, customSegmentation, id, pvid, null);
}
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/ModuleBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ void deviceIdChanged(boolean withoutMerge) {
void onConsentChanged(@NonNull final List<String> consentChangeDelta, final boolean newConsent, @NonNull final ModuleConsent.ConsentChangeSource changeSource) {
}

void consentWillChange(@NonNull List<String> consentThatWillChange, final boolean isConsentGiven) {
}

//notify the SDK modules that internal configuration was updated
void sdkConfigurationChanged() {

Expand Down
13 changes: 10 additions & 3 deletions sdk/src/main/java/ly/count/android/sdk/ModuleConsent.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -213,6 +214,7 @@ void setConsentInternal(@Nullable final String[] featureNames, final boolean isC
}

List<String> consentThatWillChange = new ArrayList<>(featureNames.length);
Map<String, Boolean> consentUpdateMap = new ConcurrentHashMap<>();

for (String featureName : featureNames) {
if (!isValidFeatureName(featureName)) {
Expand All @@ -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);
}
Expand Down
10 changes: 9 additions & 1 deletion sdk/src/main/java/ly/count/android/sdk/ModuleViews.java
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map<String, Objec
L.d("[ModuleViews] View [" + vd.viewName + "], id:[" + vd.viewID + "] is getting closed, reporting duration: [" + (UtilsTime.currentTimestampSeconds() - vd.viewStartTimeSeconds) + "] s, current timestamp: [" + UtilsTime.currentTimestampSeconds() + "]");

if (!consentProvider.getConsent(Countly.CountlyFeatureNames.views)) {
L.w("[ModuleViews] stopViewWithIDInternal, no consent given for views, ignoring call");
return;
}

Expand All @@ -295,7 +296,7 @@ void recordViewEndEvent(ViewData vd, @Nullable Map<String, Object> 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;
}

Expand Down Expand Up @@ -506,6 +507,13 @@ void onConfigurationChanged(Configuration newConfig) {
}
}

@Override
void consentWillChange(@NonNull List<String> consentThatWillChange, final boolean isConsentGiven) {
if (consentThatWillChange.contains(Countly.CountlyFeatureNames.views) && !isConsentGiven) {
stopAllViewsInternal(null);
}
}

@Override
void onActivityStopped(int updatedActivityCount) {
if (autoViewTracker) {
Expand Down

0 comments on commit d139e66

Please sign in to comment.