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
4 changes: 4 additions & 0 deletions android-core/api/android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,14 @@ public class com/mparticle/MParticleOptions {
public fun getApiSecret ()Ljava/lang/String;
public fun getAttributionListener ()Lcom/mparticle/AttributionListener;
public fun getBatchCreationListener ()Lcom/mparticle/MParticleOptions$BatchCreationListener;
public fun getCollectCustomModulePreferences ()Z
public fun getConfigMaxAge ()Ljava/lang/Integer;
public fun getConfiguration (Ljava/lang/Class;)Lcom/mparticle/Configuration;
public fun getConfigurations ()Ljava/util/List;
public fun getConfigurationsForTarget (Ljava/lang/Class;)Ljava/util/List;
public fun getConnectionTimeout ()I
public fun getContext ()Landroid/content/Context;
public fun getCustomModulePreferenceKeys ()Ljava/util/List;
public fun getDataplanId ()Ljava/lang/String;
public fun getDataplanOptions ()Lcom/mparticle/MParticleOptions$DataplanOptions;
public fun getDataplanVersion ()Ljava/lang/Integer;
Expand Down Expand Up @@ -424,9 +426,11 @@ public class com/mparticle/MParticleOptions$Builder {
public fun attributionListener (Lcom/mparticle/AttributionListener;)Lcom/mparticle/MParticleOptions$Builder;
public fun batchCreationListener (Lcom/mparticle/MParticleOptions$BatchCreationListener;)Lcom/mparticle/MParticleOptions$Builder;
public fun build ()Lcom/mparticle/MParticleOptions;
public fun collectCustomModulePreferences (Z)Lcom/mparticle/MParticleOptions$Builder;
public fun configMaxAgeSeconds (I)Lcom/mparticle/MParticleOptions$Builder;
public fun configuration (Lcom/mparticle/Configuration;)Lcom/mparticle/MParticleOptions$Builder;
public fun credentials (Ljava/lang/String;Ljava/lang/String;)Lcom/mparticle/MParticleOptions$Builder;
public fun customModulePreferenceKeys (Ljava/util/List;)Lcom/mparticle/MParticleOptions$Builder;
public fun dataplan (Ljava/lang/String;Ljava/lang/Integer;)Lcom/mparticle/MParticleOptions$Builder;
public fun dataplanOptions (Lcom/mparticle/MParticleOptions$DataplanOptions;)Lcom/mparticle/MParticleOptions$Builder;
public fun deviceBasedConsentEnabled (Z)Lcom/mparticle/MParticleOptions$Builder;
Expand Down
66 changes: 66 additions & 0 deletions android-core/src/main/java/com/mparticle/MParticleOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public class MParticleOptions {
private Map<Class, List<Configuration>> mConfigurations = new HashMap();
private List<SideloadedKit> sideloadedKits = new ArrayList<>();
private List<Integer> disabledKits = new ArrayList<>();
private boolean collectCustomModulePreferences = true;
private List<String> customModulePreferenceKeys = new ArrayList<>();

private MParticleOptions() {
}
Expand Down Expand Up @@ -164,6 +166,8 @@ public MParticleOptions(@NonNull Builder builder) {
this.mConfigurations = builder.configurations;
this.sideloadedKits = builder.sideloadedKits;
this.disabledKits = builder.disabledKits;
this.collectCustomModulePreferences = builder.collectCustomModulePreferences;
this.customModulePreferenceKeys = builder.customModulePreferenceKeys;
}

/**
Expand Down Expand Up @@ -218,6 +222,25 @@ public List<Integer> getDisabledKits() {
return disabledKits;
}

/**
* Whether the SDK may read custom module preferences out of the host app's SharedPreferences.
*
* @see Builder#collectCustomModulePreferences(boolean)
*/
public boolean getCollectCustomModulePreferences() {
return collectCustomModulePreferences;
}

/**
* The SharedPreferences keys the SDK may read for custom modules in addition to its default set.
*
* @see Builder#customModulePreferenceKeys(List)
*/
@NonNull
public List<String> getCustomModulePreferenceKeys() {
return customModulePreferenceKeys;
}

/**
* Query the API Key.
*
Expand Down Expand Up @@ -453,6 +476,8 @@ public static class Builder {
private boolean isAppDebuggable;
private List<SideloadedKit> sideloadedKits = new ArrayList<>();
private List<Integer> disabledKits = new ArrayList<>();
private boolean collectCustomModulePreferences = true;
private List<String> customModulePreferenceKeys = new ArrayList<>();

private Builder(Context context) {
this.context = context;
Expand Down Expand Up @@ -532,6 +557,45 @@ public Builder disabledKits(@NonNull List<Integer> kits) {
return this;
}

/**
* Whether the SDK may read custom module preferences out of the host app's SharedPreferences.
*
* Custom modules let the configuration forward a small, fixed set of Adobe Mobile Services
* values the Adobe SDK stores in SharedPreferences. Set this to false to stop the SDK
* reading SharedPreferences for custom modules entirely, whatever the configuration asks for.
*
* Defaults to true.
*
* @param collect whether custom module preferences may be read
* @return the instance of the builder, for chaining calls
* @see #customModulePreferenceKeys(List)
*/
@NonNull
public Builder collectCustomModulePreferences(boolean collect) {
this.collectCustomModulePreferences = collect;
return this;
}

/**
* Additional SharedPreferences keys the SDK may read for custom modules.
*
* The SDK reads only the Adobe Mobile Services values custom modules were built to carry,
* and ignores any other key the configuration names. List a key here to allow it as well; keys
* outside the default set and this list are never read, and each one ignored is logged. The
* configuration still chooses which SharedPreferences file a listed key is read from.
*
* Has no effect when {@link #collectCustomModulePreferences(boolean)} is false.
*
* @param keys the additional keys the SDK may read
* @return the instance of the builder, for chaining calls
* @see #collectCustomModulePreferences(boolean)
*/
@NonNull
public Builder customModulePreferenceKeys(@NonNull List<String> keys) {
this.customModulePreferenceKeys = new ArrayList<>(keys);
return this;
}

/**
* Register an IdentityApiRequest which will be passed to an {@link com.mparticle.identity.IdentityApi#identify(IdentityApiRequest)}
* request when the SDK starts in order to interact with the results of this call, without registering
Expand Down Expand Up @@ -873,6 +937,8 @@ public MParticleOptions build() {
}

MParticleOptions buildForInternalRestart() {
// The host's own options are unknown here, so fail closed rather than read its preferences.
collectCustomModulePreferences = false;
return new MParticleOptions(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public class ConfigManager {
public static final int DEFAULT_UPLOAD_INTERVAL = 600;
private List<ConfigLoadedListener> configUpdatedListeners = new ArrayList<>();
private List<SideloadedKit> sideloadedKits = new ArrayList<>();
private boolean mCollectCustomModulePreferences = true;
private List<String> mCustomModulePreferenceKeys = new ArrayList<>();

private ConfigManager() {
super();
Expand Down Expand Up @@ -144,6 +146,8 @@ 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());
mCollectCustomModulePreferences = options.getCollectCustomModulePreferences();
mCustomModulePreferenceKeys = new ArrayList<>(options.getCustomModulePreferenceKeys());
}

/**
Expand Down Expand Up @@ -452,8 +456,8 @@ private synchronized void updateCoreConfig(JSONObject responseJSON, boolean newC
mSendOoEvents = false;
}

if (responseJSON.has(ProviderPersistence.KEY_PERSISTENCE)) {
setProviderPersistence(new ProviderPersistence(responseJSON, mContext));
if (mCollectCustomModulePreferences && responseJSON.has(ProviderPersistence.KEY_PERSISTENCE)) {
setProviderPersistence(new ProviderPersistence(responseJSON, mContext, mCustomModulePreferenceKeys));
} else {
setProviderPersistence(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import org.json.JSONObject;

import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -24,7 +28,6 @@ class ProviderPersistence extends JSONObject {
private static final String KEY_PERSISTENCE_ID = "id";
private static final String KEY_PERSISTENCE_ANDROID = "pr";
private static final String KEY_PERSISTENCE_FILE = "f";
private static final String KEY_PERSISTENCE_MODE = "m";
private static final String KEY_PERSISTENCE_KEY_LIST = "ps";
private static final String KEY_PERSISTENCE_KEY = "k";
private static final String KEY_PERSISTENCE_TYPE = "t";
Expand All @@ -38,8 +41,26 @@ class ProviderPersistence extends JSONObject {
private static final int PERSISTENCE_TYPE_FLOAT = 4;
private static final int PERSISTENCE_TYPE_LONG = 5;


ProviderPersistence(JSONObject config, Context context) throws JSONException {
/**
* The Adobe Mobile Services values custom modules were built to carry. The config names the keys
* to read, so without this bound it could name any preference the host app keeps. The host app
* widens it with {@link com.mparticle.MParticleOptions.Builder#customModulePreferenceKeys}.
* The Adobe Mobile Services (v4) SDK stores install date, last-used date and launches as ADMS_*
* keys on Android and as OMCK* keys on iOS; both are listed so a configuration shared with iOS
* keeps working.
*/
private static final Set<String> DEFAULT_ALLOWED_KEYS = new HashSet<>(Arrays.asList(
"ADB_LIFETIME_VALUE",
"ADOBEMOBILE_STOREDDEFAULTS_AID",
"APP_MEASUREMENT_VISITOR_ID",
"ADMS_InstallDate",
"ADMS_LastDateUsed",
"ADMS_Launches",
"OMCK1",
"OMCK5",
"OMCK6"));

ProviderPersistence(JSONObject config, Context context, Collection<String> allowedKeys) throws JSONException {
super();
JSONArray configPersistence = config.getJSONArray(KEY_PERSISTENCE);
for (int i = 0; i < configPersistence.length(); i++) {
Expand All @@ -55,12 +76,17 @@ class ProviderPersistence extends JSONObject {
Logger.warning("Ignoring provider persistence config for the SDK's own preferences file: " + fileName);
continue;
}
SharedPreferences preferences = context.getSharedPreferences(fileName, fileObject.getInt(KEY_PERSISTENCE_MODE));
SharedPreferences preferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
JSONArray fileObjects = fileObject.getJSONArray(KEY_PERSISTENCE_KEY_LIST);
SharedPreferences.Editor editor = preferences.edit();
for (int keyIndex = 0; keyIndex < fileObjects.length(); keyIndex++) {
final int type = fileObjects.getJSONObject(keyIndex).getInt(KEY_PERSISTENCE_TYPE);
final String key = fileObjects.getJSONObject(keyIndex).getString(KEY_PERSISTENCE_KEY);
if (!DEFAULT_ALLOWED_KEYS.contains(key) && !allowedKeys.contains(key)) {
Logger.warning("Ignoring a custom module preference for '" + key + "': not a key this SDK reads. "
+ "Add it to MParticleOptions.Builder.customModulePreferenceKeys() to share it deliberately.");
continue;
}
final String mpKey = fileObjects.getJSONObject(keyIndex).getString(KEY_PERSISTENCE_MPVAR);
final String mpPersistenceKey = MPPREFIX + mpKey;
if (preferences.contains(mpPersistenceKey)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mparticle

import com.mparticle.mock.MockContext
import org.junit.Assert
import org.junit.Test

class MParticleOptionsTest {
@Test
fun testInternalRestartDoesNotCollectCustomModulePreferences() {
Assert.assertTrue(MParticleOptions.builder(MockContext()).build().collectCustomModulePreferences)
Assert.assertFalse(MParticleOptions.builder(MockContext()).buildForInternalRestart().collectCustomModulePreferences)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mparticle.internal

import com.mparticle.MParticle
import com.mparticle.MParticleOptions
import com.mparticle.MockMParticle
import com.mparticle.consent.ConsentState
import com.mparticle.consent.GDPRConsent
Expand Down Expand Up @@ -769,7 +770,7 @@ class ConfigManagerTest {
@Throws(Exception::class)
fun testProviderPersistenceIgnoresSdkPreferencesFiles() {
val prefs = context.getSharedPreferences(null, 0)
prefs.edit().putString(Constants.PrefKeys.API_KEY, "stored api key").remove(Constants.PrefKeys.OPTOUT).apply()
prefs.edit().putString(ADOBE_AID_KEY, "stored aid").remove(Constants.PrefKeys.OPTOUT).apply()
val sdkFiles =
listOf(
Constants.PREFS_FILE,
Expand All @@ -779,23 +780,76 @@ class ConfigManagerTest {
"mp::kit::80",
)
for (fileName in sdkFiles) {
manager.updateConfig(JSONObject(providerPersistenceConfig(fileName, Constants.PrefKeys.API_KEY, "optout::")))
manager.updateConfig(JSONObject(providerPersistenceConfig(fileName, ADOBE_AID_KEY, "optout::")))
val persistence = manager.providerPersistence
Assert.assertNotNull(persistence)
Assert.assertEquals(0, persistence?.getJSONObject("1")?.length())
Assert.assertFalse(prefs.contains(Constants.PrefKeys.OPTOUT))
}

// a preferences file that belongs to another SDK is still migrated
prefs.edit().putString("other_sdk_key", "other sdk value").apply()
manager.updateConfig(JSONObject(providerPersistenceConfig("other_sdk_prefs", "other_sdk_key", "migrated")))
Assert.assertEquals("other sdk value", manager.providerPersistence?.getJSONObject("1")?.getString("migrated"))
Assert.assertEquals("other sdk value", prefs.getString("mp::migrated", null))
manager.updateConfig(JSONObject(providerPersistenceConfig("other_sdk_prefs", ADOBE_AID_KEY, "migrated")))
Assert.assertEquals("stored aid", manager.providerPersistence?.getJSONObject("1")?.getString("migrated"))
Assert.assertEquals("stored aid", prefs.getString("mp::migrated", null))
}

@Test
@Throws(Exception::class)
fun testProviderPersistenceReadsOnlyAllowedKeys() {
val prefs = context.getSharedPreferences(null, 0)
prefs.edit().putString("auth_token", "secret").apply()
val config = JSONObject(providerPersistenceConfig("host_prefs", "auth_token", "token"))

manager.updateConfig(config)
Assert.assertEquals(0, manager.providerPersistence?.getJSONObject("1")?.length())
Assert.assertFalse(prefs.contains("mp::token"))

val allowingManager =
ConfigManager(
MParticleOptions
.builder(context)
.credentials("some api key", "some api secret")
.customModulePreferenceKeys(listOf("auth_token"))
.build(),
)
allowingManager.updateConfig(config)
Assert.assertEquals("secret", allowingManager.providerPersistence?.getJSONObject("1")?.getString("token"))
}

@Test
@Throws(Exception::class)
fun testProviderPersistenceReadsAdobeLifecycleKeys() {
val prefs = context.getSharedPreferences(null, 0)
for (key in listOf("ADMS_InstallDate", "ADMS_LastDateUsed", "ADMS_Launches", "OMCK1", "OMCK5", "OMCK6")) {
prefs.edit().putString(key, "value of $key").apply()
manager.updateConfig(JSONObject(providerPersistenceConfig("APP_MEASUREMENT_CACHE", key, "out")))
Assert.assertEquals("value of $key", manager.providerPersistence?.getJSONObject("1")?.getString("out"))
prefs.edit().remove("mp::out").apply()
}
}

@Test
@Throws(Exception::class)
fun testProviderPersistenceIgnoredWhenCollectionDisabled() {
val prefs = context.getSharedPreferences(null, 0)
prefs.edit().putString(ADOBE_AID_KEY, "stored aid").apply()
val disabledManager =
ConfigManager(
MParticleOptions
.builder(context)
.credentials("some api key", "some api secret")
.collectCustomModulePreferences(false)
.build(),
)
disabledManager.updateConfig(JSONObject(providerPersistenceConfig("other_sdk_prefs", ADOBE_AID_KEY, "aid")))
Assert.assertNull(disabledManager.providerPersistence)
Assert.assertFalse(prefs.contains("mp::aid"))
}

private fun providerPersistenceConfig(fileName: String, key: String, mpVar: String): String = """{ "cms":[ { "id":1, "pr":[ { "f":"$fileName", "m":0, "ps":[ { "k":"$key", "t":1, "n":"$mpVar", "d":"default" } ] } ] } ] }"""

companion object {
private const val ADOBE_AID_KEY = "ADOBEMOBILE_STOREDDEFAULTS_AID"
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 }"
var attributes = "mp::integrationattributes"
Expand Down
Loading