From 18980cf7176240397408c6f54f9d10e9b21a8945 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Wed, 14 Oct 2026 14:26:25 -0400 Subject: [PATCH 1/2] fix: bound the preferences custom modules may read from the host app The configuration's custom module settings name a SharedPreferences file and the keys to read from it, and the SDK uploads the values it finds. Nothing limited which keys could be named, so the configuration could read any preference the host app keeps, and it also chose the mode the file was opened with. The SDK now reads only the Adobe Mobile Services identifiers custom modules were built to carry, matching the Apple SDK. A host app can allow more keys with MParticleOptions.Builder.customModulePreferenceKeys or stop the SDK reading them with collectCustomModulePreferences(false). A key outside the allowed set is neither read nor cached, and is logged. Preference files are always opened in private mode. When the SDK starts itself to handle a push message, it does not know the host app's options, so it reads no custom module preferences in that process. Co-Authored-By: Claude Opus 5.5 (1M context) --- android-core/api/android-core.api | 4 ++ .../java/com/mparticle/MParticleOptions.java | 66 +++++++++++++++++++ .../com/mparticle/internal/ConfigManager.java | 8 ++- .../internal/ProviderPersistence.java | 28 ++++++-- .../com/mparticle/MParticleOptionsTest.kt | 13 ++++ .../mparticle/internal/ConfigManagerTest.kt | 54 +++++++++++++-- 6 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 android-core/src/test/kotlin/com/mparticle/MParticleOptionsTest.kt diff --git a/android-core/api/android-core.api b/android-core/api/android-core.api index da618c98f..1fca8eb80 100644 --- a/android-core/api/android-core.api +++ b/android-core/api/android-core.api @@ -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; @@ -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; diff --git a/android-core/src/main/java/com/mparticle/MParticleOptions.java b/android-core/src/main/java/com/mparticle/MParticleOptions.java index c5e421e8b..a11fe8ccf 100644 --- a/android-core/src/main/java/com/mparticle/MParticleOptions.java +++ b/android-core/src/main/java/com/mparticle/MParticleOptions.java @@ -60,6 +60,8 @@ public class MParticleOptions { private Map> mConfigurations = new HashMap(); private List sideloadedKits = new ArrayList<>(); private List disabledKits = new ArrayList<>(); + private boolean collectCustomModulePreferences = true; + private List customModulePreferenceKeys = new ArrayList<>(); private MParticleOptions() { } @@ -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; } /** @@ -218,6 +222,25 @@ public List 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 getCustomModulePreferenceKeys() { + return customModulePreferenceKeys; + } + /** * Query the API Key. * @@ -453,6 +476,8 @@ public static class Builder { private boolean isAppDebuggable; private List sideloadedKits = new ArrayList<>(); private List disabledKits = new ArrayList<>(); + private boolean collectCustomModulePreferences = true; + private List customModulePreferenceKeys = new ArrayList<>(); private Builder(Context context) { this.context = context; @@ -532,6 +557,45 @@ public Builder disabledKits(@NonNull List 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 + * identifiers 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 identifiers 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 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 @@ -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); } } diff --git a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java index 216fa2b5d..edb65e64c 100644 --- a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java +++ b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java @@ -117,6 +117,8 @@ public class ConfigManager { public static final int DEFAULT_UPLOAD_INTERVAL = 600; private List configUpdatedListeners = new ArrayList<>(); private List sideloadedKits = new ArrayList<>(); + private boolean mCollectCustomModulePreferences = true; + private List mCustomModulePreferenceKeys = new ArrayList<>(); private ConfigManager() { super(); @@ -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()); } /** @@ -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); } diff --git a/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java b/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java index c1f051c0f..788b8ca65 100644 --- a/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java +++ b/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java @@ -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; @@ -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"; @@ -38,8 +41,20 @@ 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 identifiers 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}. + */ + private static final Set DEFAULT_ALLOWED_KEYS = new HashSet<>(Arrays.asList( + "ADB_LIFETIME_VALUE", + "ADOBEMOBILE_STOREDDEFAULTS_AID", + "APP_MEASUREMENT_VISITOR_ID", + "OMCK1", + "OMCK5", + "OMCK6")); + + ProviderPersistence(JSONObject config, Context context, Collection allowedKeys) throws JSONException { super(); JSONArray configPersistence = config.getJSONArray(KEY_PERSISTENCE); for (int i = 0; i < configPersistence.length(); i++) { @@ -55,12 +70,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)) { diff --git a/android-core/src/test/kotlin/com/mparticle/MParticleOptionsTest.kt b/android-core/src/test/kotlin/com/mparticle/MParticleOptionsTest.kt new file mode 100644 index 000000000..b84a8c5d7 --- /dev/null +++ b/android-core/src/test/kotlin/com/mparticle/MParticleOptionsTest.kt @@ -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) + } +} diff --git a/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt index f84229af9..2f87bae9e 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt @@ -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 @@ -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, @@ -779,7 +780,7 @@ 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()) @@ -787,15 +788,56 @@ class ConfigManagerTest { } // 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 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" From 16cbf9f7232fe6b27c27f38eb18bd8e46e221888 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Wed, 14 Oct 2026 15:14:44 -0400 Subject: [PATCH 2/2] fix: allow the Android names of the Adobe lifecycle values The Adobe Mobile Services SDK keeps install date, last-used date and launches under ADMS_InstallDate, ADMS_LastDateUsed and ADMS_Launches on Android, and under OMCK1, OMCK5 and OMCK6 on iOS. The default set of custom module preference keys copied the Apple SDK's and so had only the iOS names. Add the Android names, and keep the iOS names so a configuration shared with iOS still resolves those keys as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../main/java/com/mparticle/MParticleOptions.java | 4 ++-- .../com/mparticle/internal/ProviderPersistence.java | 12 +++++++++--- .../com/mparticle/internal/ConfigManagerTest.kt | 12 ++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/android-core/src/main/java/com/mparticle/MParticleOptions.java b/android-core/src/main/java/com/mparticle/MParticleOptions.java index a11fe8ccf..1439c2071 100644 --- a/android-core/src/main/java/com/mparticle/MParticleOptions.java +++ b/android-core/src/main/java/com/mparticle/MParticleOptions.java @@ -561,7 +561,7 @@ public Builder disabledKits(@NonNull List kits) { * 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 - * identifiers the Adobe SDK stores in SharedPreferences. Set this to false to stop the SDK + * 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. @@ -579,7 +579,7 @@ public Builder collectCustomModulePreferences(boolean collect) { /** * Additional SharedPreferences keys the SDK may read for custom modules. * - * The SDK reads only the Adobe Mobile Services identifiers custom modules were built to carry, + * 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. diff --git a/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java b/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java index 788b8ca65..0176cdccc 100644 --- a/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java +++ b/android-core/src/main/java/com/mparticle/internal/ProviderPersistence.java @@ -42,14 +42,20 @@ class ProviderPersistence extends JSONObject { private static final int PERSISTENCE_TYPE_LONG = 5; /** - * The Adobe Mobile Services identifiers 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 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 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")); diff --git a/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt index 2f87bae9e..956cdbfc3 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/ConfigManagerTest.kt @@ -816,6 +816,18 @@ class ConfigManagerTest { 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() {