Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 62 additions & 83 deletions CHANGELOG.md

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions android-core/src/main/java/com/mparticle/MParticle.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import androidx.annotation.RequiresApi;

import com.mparticle.commerce.CommerceEvent;
import com.mparticle.consent.ConsentState;
import com.mparticle.identity.IdentityApi;
import com.mparticle.identity.IdentityApiRequest;
import com.mparticle.identity.IdentityApiResult;
Expand Down Expand Up @@ -911,6 +912,48 @@ public void setOptOut(@NonNull Boolean optOutStatus) {
}
}

/**
* Query the device-level consent state.
* <p>
* Device-level consent, when set, overrides MPID-based consent when applying consent forwarding
* rules and uploading events.
*
* @return the device-level consent state, or an empty state if none has been set
*/
@NonNull
public ConsentState getDeviceConsentState() {
return mConfigManager.getDeviceConsentState();
}

/**
* Set the device-level consent state.
* <p>
* Device-level consent overrides MPID-based consent when applying consent forwarding rules
* and uploading events. Pass {@code null} to clear the device-level override and fall back to
* MPID-based consent.
*
* @param state the device-level consent state, or {@code null} to clear the override
*/
public void setDeviceConsentState(@Nullable ConsentState state) {
ConsentState oldState = mConfigManager.getEffectiveConsentState(mConfigManager.getMpid());
mConfigManager.setDeviceConsentState(state);
ConsentState newState = mConfigManager.getEffectiveConsentState(mConfigManager.getMpid());
mKitManager.onConsentStateUpdated(oldState, newState, mConfigManager.getMpid());
}

/**
* Query whether device-based consent is enabled.
* <p>
* When enabled, {@link com.mparticle.identity.MParticleUser#setConsentState(ConsentState)}
* also persists consent at the device level.
*
* @return true if device-based consent is enabled
*/
@NonNull
public Boolean isDeviceBasedConsentEnabled() {
return mConfigManager.isDeviceBasedConsentEnabled();
}

/**
* Retrieve a URL to be loaded within a {@link WebView} to show the user a survey
* or feedback form.
Expand Down
36 changes: 36 additions & 0 deletions android-core/src/main/java/com/mparticle/MParticleOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class MParticleOptions {
private String mApiSecret;
private IdentityApiRequest mIdentifyRequest;
private Boolean mDevicePerformanceMetricsDisabled = false;
private Boolean mDeviceBasedConsentEnabled = false;
private Boolean mAndroidIdEnabled = false;
private Integer mUploadInterval = ConfigManager.DEFAULT_UPLOAD_INTERVAL; //seconds
private Integer mSessionTimeout = ConfigManager.DEFAULT_SESSION_TIMEOUT_SECONDS; //seconds
Expand Down Expand Up @@ -93,6 +94,9 @@ public MParticleOptions(@NonNull Builder builder) {
if (builder.devicePerformanceMetricsDisabled != null) {
this.mDevicePerformanceMetricsDisabled = builder.devicePerformanceMetricsDisabled;
}
if (builder.deviceBasedConsentEnabled != null) {
this.mDeviceBasedConsentEnabled = builder.deviceBasedConsentEnabled;
}
if (builder.androidIdEnabled != null) {
this.mAndroidIdEnabled = builder.androidIdEnabled;
}
Expand Down Expand Up @@ -254,6 +258,20 @@ public Boolean isDevicePerformanceMetricsDisabled() {
return mDevicePerformanceMetricsDisabled;
}

/**
* Query whether device-based consent is enabled.
* <p>
* When enabled, {@link com.mparticle.identity.MParticleUser#setConsentState(ConsentState)}
* will persist consent at the device level in addition to the current MPID. Device-level
* consent overrides MPID-based consent when applying consent forwarding rules and uploading events.
*
* @return true if device-based consent is enabled
*/
@NonNull
public Boolean isDeviceBasedConsentEnabled() {
return mDeviceBasedConsentEnabled;
}

/**
* @return true if collection is disabled, false if it is enabled
* @deprecated This method has been replaced as the behavior has been inverted - Android ID collection is now disabled by default.
Expand Down Expand Up @@ -423,6 +441,7 @@ public static class Builder {
private MParticle.Environment environment;
private IdentityApiRequest identifyRequest;
private Boolean devicePerformanceMetricsDisabled = null;
private Boolean deviceBasedConsentEnabled = null;
private Boolean androidIdEnabled = null;
private Integer uploadInterval = null;
private Integer sessionTimeout = null;
Expand Down Expand Up @@ -570,6 +589,23 @@ public Builder devicePerformanceMetricsDisabled(boolean disabled) {
return this;
}

/**
* Enable device-based consent.
* <p>
* When enabled, consent set via {@link com.mparticle.identity.MParticleUser#setConsentState(ConsentState)}
* is stored at the device level and overrides MPID-based consent when applying consent forwarding
* rules and uploading events. This is useful when consent is collected before the user's MPID is known
* or when the MPID changes during a flow such as checkout.
*
* @param enabled true to enable device-based consent
* @return the instance of the builder, for chaining calls
*/
@NonNull
public Builder deviceBasedConsentEnabled(boolean enabled) {
this.deviceBasedConsentEnabled = enabled;
return this;
}

/**
* @param disabled false to enable collection (true by default)
* @return the instance of the builder, for chaining calls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,17 @@ boolean setUser(Context context, long previousMpid, long newMpid, Map<MParticle.
}

public ConsentState getConsentState(long mpid) {
return mConfigManager.getConsentState(mpid);
return mConfigManager.getEffectiveConsentState(mpid);
}

public void setConsentState(ConsentState state, long mpid) {
ConsentState oldState = getConsentState(mpid);
mConfigManager.setConsentState(state, mpid);
mKitManager.onConsentStateUpdated(oldState, state, mpid);
if (mConfigManager.isDeviceBasedConsentEnabled()) {
mConfigManager.setDeviceConsentState(state);
}
ConsentState newState = getConsentState(mpid);
mKitManager.onConsentStateUpdated(oldState, newState, mpid);
}

public boolean isLoggedIn(Long mpid) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public class ConfigManager {
private UserStorage mUserStorage;
private String mLogUnhandledExceptions = VALUE_APP_DEFINED;
private boolean audienceAPIFlag = false;
private boolean mDeviceBasedConsentEnabled = false;

private boolean mSendOoEvents;
private JSONObject mProviderPersistence;
Expand Down Expand Up @@ -134,11 +135,13 @@ public static ConfigManager getInstance(Context context) {
public ConfigManager(Context context) {
mContext = context;
sPreferences = getPreferences(mContext);
mDeviceBasedConsentEnabled = sPreferences.getBoolean(Constants.PrefKeys.DEVICE_BASED_CONSENT_ENABLED, false);
}

public ConfigManager(@NonNull MParticleOptions options) {
this(options.getContext(), options.getEnvironment(), options.getApiKey(), options.getApiSecret(), options.getDataplanOptions(), options.getDataplanId(), options.getDataplanVersion(), options.getConfigMaxAge(), options.getConfigurationsForTarget(ConfigManager.class), options.getSideloadedKits());
mPersistenceMaxAgeSeconds = options.getPersistenceMaxAgeSeconds();
setDeviceBasedConsentEnabled(options.isDeviceBasedConsentEnabled());
}

/**
Expand Down Expand Up @@ -177,6 +180,7 @@ public ConfigManager(@NonNull Context context, @Nullable MParticle.Environment e
configuration.apply(this);
}
}
mDeviceBasedConsentEnabled = sPreferences.getBoolean(Constants.PrefKeys.DEVICE_BASED_CONSENT_ENABLED, false);
}

public void onMParticleStarted() {
Expand Down Expand Up @@ -1335,6 +1339,50 @@ public ConsentState getConsentState(long mpid) {
return ConsentState.withConsentState(serializedConsent).build();
}

public boolean isDeviceBasedConsentEnabled() {
return mDeviceBasedConsentEnabled;
}

public void setDeviceBasedConsentEnabled(boolean deviceBasedConsentEnabled) {
mDeviceBasedConsentEnabled = deviceBasedConsentEnabled;
sPreferences.edit()
.putBoolean(Constants.PrefKeys.DEVICE_BASED_CONSENT_ENABLED, deviceBasedConsentEnabled)
.apply();
}

public boolean hasDeviceConsentOverride() {
return sPreferences.contains(Constants.PrefKeys.DEVICE_CONSENT_STATE);
}

@NonNull
public ConsentState getDeviceConsentState() {
if (!hasDeviceConsentOverride()) {
return ConsentState.withConsentState((String) null).build();
}
String serializedConsent = sPreferences.getString(Constants.PrefKeys.DEVICE_CONSENT_STATE, null);
return ConsentState.withConsentState(serializedConsent).build();
}

public void setDeviceConsentState(@Nullable ConsentState state) {
if (state != null) {
sPreferences.edit()
.putString(Constants.PrefKeys.DEVICE_CONSENT_STATE, state.toString())
.apply();
} else {
sPreferences.edit()
.remove(Constants.PrefKeys.DEVICE_CONSENT_STATE)
.apply();
}
}

@NonNull
public ConsentState getEffectiveConsentState(long mpid) {
if (hasDeviceConsentOverride()) {
return getDeviceConsentState();
}
return getConsentState(mpid);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale device consent without flag

Medium Severity

After deviceBasedConsentEnabled is off at startup, a device consent value previously written by mirrored setConsentState can remain in preferences. getEffectiveConsentState still prefers that stored device consent, but setConsentState no longer updates device storage, so MPID consent changes may not reach kit forwarding or uploads.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2e277cd. Configure here.


public boolean isDirectUrlRoutingEnabled() {
return directUrlRouting;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static MessageBatch create(boolean history, ConfigManager configManager,
uploadMessage.put(Constants.MessageKey.COOKIES, cookies);
uploadMessage.put(Constants.MessageKey.PROVIDER_PERSISTENCE, configManager.getProviderPersistence());
uploadMessage.put(Constants.MessageKey.INTEGRATION_ATTRIBUTES, configManager.getIntegrationAttributes());
uploadMessage.addConsentState(configManager.getConsentState(batchId.getMpid()));
uploadMessage.addConsentState(configManager.getEffectiveConsentState(batchId.getMpid()));
uploadMessage.addDataplanContext(batchId.getDataplanId(), batchId.getDataplanVersion());
return uploadMessage;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,8 @@ object Constants {
const val IF_MODIFIED: String = "mp::ifmodified"
const val IDENTITY_API_CONTEXT: String = "mp::identity::api::context"
const val DEVICE_APPLICATION_STAMP: String = "mp::device-app-stamp"
const val DEVICE_CONSENT_STATE: String = "mp::device::consent"
const val DEVICE_BASED_CONSENT_ENABLED: String = "mp::device::consent::enabled"
const val PREVIOUS_ANDROID_ID: String = "mp::previous::android::id"
const val DISPLAY_PUSH_NOTIFICATIONS: String = "mp::displaypushnotifications"
const val IDENTITY_CONNECTION_TIMEOUT: String = "mp::connection:timeout:identity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ApiVisibilityTest {
publicMethodCount++
}
}
Assert.assertEquals(66, publicMethodCount)
Assert.assertEquals(69, publicMethodCount)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.mparticle.internal

import com.mparticle.MParticle
import com.mparticle.MockMParticle
import com.mparticle.consent.ConsentState
import com.mparticle.consent.GDPRConsent
import com.mparticle.internal.KitManager.KitStatus
import com.mparticle.internal.PushRegistrationHelper.PushRegistration
import com.mparticle.internal.messages.BaseMPMessage
Expand Down Expand Up @@ -729,6 +731,40 @@ class ConfigManagerTest {
Assert.assertNotNull(manager.configTimestamp)
}

@Test
fun testDeviceConsentOverridesMpidConsent() {
val mpid = ran.nextLong()
val mpidConsent = ConsentState.builder()
.addGDPRConsentState("mpid-purpose", GDPRConsent.builder(false).build())
.build()
val deviceConsent = ConsentState.builder()
.addGDPRConsentState("device-purpose", GDPRConsent.builder(true).build())
.build()

manager.setConsentState(mpidConsent, mpid)
Assert.assertFalse(manager.hasDeviceConsentOverride())
Assert.assertTrue(manager.getEffectiveConsentState(mpid).gdprConsentState.containsKey("mpid-purpose"))

manager.setDeviceConsentState(deviceConsent)
Assert.assertTrue(manager.hasDeviceConsentOverride())
Assert.assertTrue(manager.getEffectiveConsentState(mpid).gdprConsentState.containsKey("device-purpose"))
Assert.assertFalse(manager.getEffectiveConsentState(mpid).gdprConsentState.containsKey("mpid-purpose"))

manager.setDeviceConsentState(null)
Assert.assertFalse(manager.hasDeviceConsentOverride())
Assert.assertTrue(manager.getEffectiveConsentState(mpid).gdprConsentState.containsKey("mpid-purpose"))
}

@Test
fun testDeviceBasedConsentEnabledPersists() {
Assert.assertFalse(manager.isDeviceBasedConsentEnabled())
manager.setDeviceBasedConsentEnabled(true)
Assert.assertTrue(manager.isDeviceBasedConsentEnabled())

val reloadedManager = ConfigManager(context)
Assert.assertTrue(reloadedManager.isDeviceBasedConsentEnabled())
}

companion object {
private const val SAMPLE_CONFIG =
"{ \"dt\":\"ac\", \"id\":\"5b7b8073-852b-47c2-9b89-c4bc66e3bd55\", \"ct\":1428030730685, \"dbg\":false, \"cue\":\"appdefined\", \"pmk\":[ \"mp_message\", \"com.urbanairship.push.ALERT\", \"alert\", \"a\", \"message\" ], \"cnp\":\"appdefined\", \"soc\":0, \"oo\":false, \"tri\" : { \"mm\" : [{ \"dt\" : \"x\", \"eh\" : true } ], \"evts\" : [1217787541, 2, 3] }, \"eks\":[ { \"id\":64, \"as\":{ \"clientId\":\"8FMBElARYl9ZtgwYIN5sZA==\", \"surveyId\":\"android_app\", \"sendAppVersion\":\"True\", \"rootUrl\":\"http://survey.foreseeresults.com/survey/display\" }, \"hs\":{ \"et\":{ \"57\":0, \"49\":0, \"55\":0, \"52\":0, \"53\":0, \"50\":0, \"56\":0, \"51\":0, \"54\":0, \"48\":0 }, \"ec\":{ \"609391310\":0, \"-1282670145\":0, \"2138942058\":0, \"-1262630649\":0, \"-877324321\":0, \"1700497048\":0, \"1611158813\":0, \"1900204162\":0, \"-998867355\":0, \"-1758179958\":0, \"-994832826\":0, \"1598473606\":0, \"-2106320589\":0 }, \"ea\":{ \"343635109\":0, \"1162787110\":0, \"-427055400\":0, \"-1285822129\":0, \"1699530232\":0 }, \"svec\":{ \"-725356351\":0, \"-1992427723\":0, \"751512662\":0, \"-118381281\":0, \"-171137512\":0, \"-2036479142\":0, \"-1338304551\":0, \"1003167705\":0, \"1046650497\":0, \"1919407518\":0, \"-1326325184\":0, \"480870493\":0, \"-1087232483\":0, \"-725540438\":0, \"-461793000\":0, \"1935019626\":0, \"76381608\":0, \"273797382\":0, \"-948909976\":0, \"-348193740\":0, \"-685370074\":0, \"-849874419\":0, \"2074021738\":0, \"-767572488\":0, \"-1091433459\":0, \"1671688881\":0, \"1304651793\":0, \"1299738196\":0, \"326063875\":0, \"296835202\":0, \"268236000\":0, \"1708308839\":0, \"101093345\":0, \"-652558691\":0, \"-1613021771\":0, \"1106318256\":0, \"-473874363\":0, \"-1267780435\":0, \"486732621\":0, \"1855792002\":0, \"-881258627\":0, \"698731249\":0, \"1510155838\":0, \"1119638805\":0, \"479337352\":0, \"1312099430\":0, \"1712783405\":0, \"-459721027\":0, \"-214402990\":0, \"617910950\":0, \"428901717\":0, \"-201124647\":0, \"940674176\":0, \"1632668193\":0, \"338835860\":0, \"879890181\":0, \"1667730064\":0 } } } ], \"lsv\":\"2.1.4\", \"pio\":30 }"
Expand Down
Loading