diff --git a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java index 2332868d..8c3a6265 100644 --- a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java +++ b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java @@ -80,7 +80,7 @@ public class OptimizelyClient { So, we start with an empty map of default attributes until the manager is initialized. */ - if (isValid()) { + if (isValid() && vuid != null) { // identifiers are empty here since vuid will be inserted by java-sdk core sendODPEvent(null, "client_initialized", null, null); } diff --git a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java index 57e4cdaf..ac43c8e6 100644 --- a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java +++ b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java @@ -48,11 +48,7 @@ import com.optimizely.ab.event.BatchEventProcessor; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; -import com.optimizely.ab.event.internal.BuildVersionInfo; -import com.optimizely.ab.event.internal.ClientEngineInfo; -import com.optimizely.ab.event.internal.payload.EventBatch; import com.optimizely.ab.notification.NotificationCenter; -import com.optimizely.ab.notification.UpdateConfigNotification; import com.optimizely.ab.odp.ODPApiManager; import com.optimizely.ab.odp.ODPEventManager; import com.optimizely.ab.odp.ODPManager; @@ -65,7 +61,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -792,6 +787,7 @@ public static class Builder { private int timeoutForODPSegmentFetchInSecs = 10; private int timeoutForODPEventDispatchInSecs = 10; private boolean odpEnabled = true; + private boolean vuidEnabled = false; private String vuid = null; private String customSdkName = null; @@ -1031,6 +1027,15 @@ public Builder withODPDisabled() { return this; } + /** + * Enable Vuid + * @return this {@link Builder} instance + */ + public Builder withVuidEnabled() { + this.vuidEnabled = true; + return this; + } + /** * Override the default (SDK-generated and persistent) vuid. * @param vuid a user-defined vuid value @@ -1120,8 +1125,11 @@ public OptimizelyManager build(Context context) { } - if (vuid == null) { - vuid = VuidManager.Companion.getShared(context).getVuid(); + VuidManager vuidManager = VuidManager.Companion.getInstance(); + vuidManager.configure(vuidEnabled, context); + + if (vuid == null && vuidEnabled) { + vuid = vuidManager.getVuid(); } ODPManager odpManager = null; diff --git a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java index b9f5f276..6753c327 100644 --- a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java +++ b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java @@ -17,9 +17,7 @@ package com.optimizely.ab.android.sdk; import android.content.Context; -import android.graphics.Path; -import com.optimizely.ab.Optimizely; import com.optimizely.ab.android.datafile_handler.DatafileHandler; import com.optimizely.ab.android.datafile_handler.DefaultDatafileHandler; import com.optimizely.ab.android.event_handler.DefaultEventHandler; @@ -28,7 +26,6 @@ import com.optimizely.ab.android.odp.ODPSegmentClient; import com.optimizely.ab.android.odp.VuidManager; import com.optimizely.ab.android.shared.DatafileConfig; -import com.optimizely.ab.android.shared.WorkerScheduler; import com.optimizely.ab.android.user_profile.DefaultUserProfileService; import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.error.ErrorHandler; @@ -36,22 +33,24 @@ import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; import com.optimizely.ab.notification.NotificationCenter; -import com.optimizely.ab.odp.ODPApiManager; import com.optimizely.ab.odp.ODPEventManager; import com.optimizely.ab.odp.ODPManager; import com.optimizely.ab.odp.ODPSegmentManager; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.runners.MockitoJUnitRunner; +import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import org.slf4j.Logger; import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyLong; @@ -63,25 +62,27 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyNew; import static org.powermock.api.mockito.PowerMockito.whenNew; -import java.sql.Time; import java.util.Map; import java.util.concurrent.TimeUnit; @RunWith(PowerMockRunner.class) @PowerMockIgnore("jdk.internal.reflect.*") -@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class, ODPManager.class, ODPSegmentManager.class, ODPEventManager.class}) +@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class, ODPManager.class, ODPSegmentManager.class, ODPEventManager.class, VuidManager.class}) public class OptimizelyManagerBuilderTest { private String testProjectId = "7595190003"; private String testSdkKey = "1234"; private Logger logger; + private VuidManager mockVuidManager; + private String minDatafile = "{\n" + "experiments: [ ],\n" + "version: \"2\",\n" + @@ -101,6 +102,15 @@ public class OptimizelyManagerBuilderTest { public void setup() throws Exception { mockContext = mock(Context.class); mockDatafileHandler = mock(DefaultDatafileHandler.class); + + mockStatic(VuidManager.class); + VuidManager.Companion mockCompanion = PowerMockito.mock(VuidManager.Companion.class); + mockVuidManager = PowerMockito.mock(VuidManager.class); + PowerMockito.doReturn(mockVuidManager).when(mockCompanion).getInstance(); + Whitebox.setInternalState( + VuidManager.class, "Companion", + mockCompanion + ); } /** @@ -400,4 +410,60 @@ public void testBuildWithODP_defaultCommonDataAndIdentifiers() throws Exception assertEquals(identifiers.size(), 1); } + ODPManager.Builder getMockODPManagerBuilder() { + ODPManager.Builder mockBuilder = PowerMockito.mock(ODPManager.Builder.class); + when(mockBuilder.withApiManager(any())).thenReturn(mockBuilder); + when(mockBuilder.withSegmentCacheSize(any())).thenReturn(mockBuilder); + when(mockBuilder.withSegmentCacheTimeout(any())).thenReturn(mockBuilder); + when(mockBuilder.withSegmentManager(any())).thenReturn(mockBuilder); + when(mockBuilder.withEventManager(any())).thenReturn(mockBuilder); + when(mockBuilder.withUserCommonData(any())).thenReturn(mockBuilder); + when(mockBuilder.withUserCommonIdentifiers(any())).thenReturn(mockBuilder); + return mockBuilder; + } + + @Test + public void testBuildWithVuidDisabled() throws Exception { + mockStatic(ODPManager.class); + ODPManager.Builder mockBuilder = getMockODPManagerBuilder(); + when(mockBuilder.build()).thenReturn(mock(ODPManager.class)); + when(ODPManager.builder()).thenReturn(mockBuilder); + + OptimizelyManager manager = OptimizelyManager.builder() + .withSDKKey(testSdkKey) + .build(mockContext); + + verify(mockVuidManager, times(1)).configure(eq(false), any(Context.class)); + + ArgumentCaptor> identifiersCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockBuilder).withUserCommonIdentifiers(identifiersCaptor.capture()); + Map identifiers = identifiersCaptor.getValue(); + assertFalse(identifiers.containsKey("vuid")); + + when(ODPManager.builder()).thenCallRealMethod(); + } + + @Test + public void testBuildWithVuidEnabled() throws Exception { + mockStatic(ODPManager.class); + ODPManager.Builder mockBuilder = getMockODPManagerBuilder(); + when(mockBuilder.build()).thenReturn(mock(ODPManager.class)); + when(ODPManager.builder()).thenReturn(mockBuilder); + + when(mockVuidManager.getVuid()).thenReturn("vuid_test"); + + OptimizelyManager manager = OptimizelyManager.builder() + .withSDKKey(testSdkKey) + .withVuidEnabled() + .build(mockContext); + + verify(mockVuidManager, times(1)).configure(eq(true), any(Context.class)); + + ArgumentCaptor> identifiersCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockBuilder).withUserCommonIdentifiers(identifiersCaptor.capture()); + Map identifiers = identifiersCaptor.getValue(); + assertEquals(identifiers.get("vuid"), "vuid_test"); + + when(ODPManager.builder()).thenCallRealMethod(); + } } diff --git a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java index 45fceb29..f707c61a 100644 --- a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java +++ b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java @@ -18,13 +18,14 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.doReturn; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyNew; import static org.powermock.api.mockito.PowerMockito.whenNew; @@ -33,9 +34,9 @@ import com.optimizely.ab.android.datafile_handler.DatafileHandler; import com.optimizely.ab.android.event_handler.DefaultEventHandler; +import com.optimizely.ab.android.odp.VuidManager; import com.optimizely.ab.android.shared.DatafileConfig; import com.optimizely.ab.bucketing.UserProfileService; -import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.event.BatchEventProcessor; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; @@ -45,11 +46,12 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.powermock.core.PowerMockUtils; +import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import org.slf4j.Logger; import java.util.concurrent.BlockingQueue; @@ -59,7 +61,7 @@ @RunWith(PowerMockRunner.class) @PowerMockIgnore("jdk.internal.reflect.*") -@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class}) +@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class, VuidManager.class}) public class OptimizelyManagerIntervalTest { private Logger logger; @@ -76,6 +78,15 @@ public void setup() throws Exception { mockEventHandler = mock(DefaultEventHandler.class); mockStatic(DefaultEventHandler.class); when(DefaultEventHandler.getInstance(any())).thenReturn(mockEventHandler); + + mockStatic(VuidManager.class); + VuidManager.Companion mockCompanion = PowerMockito.mock(VuidManager.Companion.class); + VuidManager mockVuidManager = PowerMockito.mock(VuidManager.class); + doReturn(mockVuidManager).when(mockCompanion).getInstance(); + Whitebox.setInternalState( + VuidManager.class, "Companion", + mockCompanion + ); } // DatafileDownloadInterval diff --git a/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt b/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt index cbbbfcfe..3151bf2c 100644 --- a/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt +++ b/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt @@ -22,6 +22,8 @@ import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -40,7 +42,8 @@ class VuidManagerTest { // remove a singleton instance VuidManager.removeSharedForTesting() - vuidManager = VuidManager.getShared(context) + vuidManager = VuidManager.getInstance() + vuidManager.configure(true, context) } @After @@ -51,6 +54,16 @@ class VuidManagerTest { editor.commit() } + fun saveInSharedPrefs(key: String, value: String) { + val sharedPreferences = context.getSharedPreferences("optly", Context.MODE_PRIVATE).edit() + sharedPreferences.putString(key, value) + sharedPreferences.apply() + } + + fun getFromSharedPrefs(key: String): String? { + return context.getSharedPreferences("optly", Context.MODE_PRIVATE).getString(key, null) + } + @Test fun makeVuid() { val vuid = vuidManager.makeVuid() @@ -90,16 +103,22 @@ class VuidManagerTest { @Test fun autoLoaded() { - val vuid1 = VuidManager.getShared(context).vuid + val vuidManager1 = VuidManager.getInstance() + vuidManager1.configure(true, context) + val vuid1 = vuidManager1.vuid assertTrue("vuid should be auto loaded when constructed", vuid1.startsWith("vuid_")) - val vuid2 = VuidManager.getShared(context).vuid + val vuidManager2 = VuidManager.getInstance() + vuidManager2.configure(true, context) + val vuid2 = vuidManager2.vuid assertEquals("the same vuid should be returned when getting a singleton", vuid1, vuid2) // remove shared instance, so will be re-instantiated VuidManager.removeSharedForTesting() - val vuid3 = VuidManager.getShared(context).vuid + val vuidManager3 = VuidManager.getInstance() + vuidManager3.configure(true, context) + val vuid3 = vuidManager3.vuid assertEquals("the saved vuid should be returned when instantiated again", vuid2, vuid3) // remove saved vuid @@ -107,8 +126,50 @@ class VuidManagerTest { // remove shared instance, so will be re-instantiated VuidManager.removeSharedForTesting() - val vuid4 = VuidManager.getShared(context).vuid + val vuidManager4 = VuidManager.getInstance() + vuidManager4.configure(true, context) + val vuid4 = vuidManager4.vuid assertNotEquals("a new vuid should be returned when storage cleared and re-instantiated", vuid3, vuid4) assertTrue(vuid4.startsWith("vuid_")) } + + @Test + fun configureWithVuidDisabled() { + cleanSharedPrefs() + saveInSharedPrefs("vuid", "vuid_test") + VuidManager.removeSharedForTesting() + + vuidManager = VuidManager.getInstance() + vuidManager.configure(false, context) + + assertNull(getFromSharedPrefs("vuid")) + assertEquals(vuidManager.vuid, "") + } + + @Test + fun configureWithVuidEnabledWhenVuidAlreadyExists() { + cleanSharedPrefs() + saveInSharedPrefs("vuid", "vuid_test") + VuidManager.removeSharedForTesting() + + vuidManager = VuidManager.getInstance() + vuidManager.configure(true, context) + + assertEquals(vuidManager.vuid, "vuid_test") + } + + @Test + fun configureWithVuidEnabledWhenVuidDoesNotExist() { + cleanSharedPrefs() + VuidManager.removeSharedForTesting() + assertNull(getFromSharedPrefs("vuid")) + + vuidManager = VuidManager.getInstance() + vuidManager.configure(true, context) + + assertTrue(vuidManager.vuid.startsWith("vuid_")) + assertNotNull(getFromSharedPrefs("vuid")) + getFromSharedPrefs("vuid")?.let { assertTrue(it.startsWith("vuid_")) } + assertEquals(getFromSharedPrefs("vuid"), vuidManager.vuid) + } } diff --git a/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt b/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt index 5077141b..391b413c 100644 --- a/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt +++ b/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt @@ -19,20 +19,16 @@ import androidx.annotation.VisibleForTesting import com.optimizely.ab.android.shared.OptlyStorage import java.util.UUID -class VuidManager private constructor(context: Context) { +class VuidManager private constructor() { var vuid = "" private val keyForVuid = "vuid" // stored in the private "optly" storage - init { - this.vuid = load(context) - } - companion object { @Volatile private var INSTANCE: VuidManager? = null @Synchronized - fun getShared(context: Context): VuidManager = INSTANCE ?: VuidManager(context).also { INSTANCE = it } + fun getInstance(): VuidManager = INSTANCE ?: VuidManager().also { INSTANCE = it } fun isVuid(visitorId: String): Boolean { return visitorId.startsWith("vuid_", ignoreCase = true) @@ -44,6 +40,16 @@ class VuidManager private constructor(context: Context) { } } + @Synchronized + fun configure(enableVuid: Boolean, context: Context) { + if (!enableVuid) { + removeVuid(context) + this.vuid = "" + } else { + this.vuid = load(context) + } + } + @VisibleForTesting fun makeVuid(): String { val maxLength = 32 // required by ODP server @@ -57,7 +63,9 @@ class VuidManager private constructor(context: Context) { fun load(context: Context): String { val storage = OptlyStorage(context) val oldVuid = storage.getString(keyForVuid, null) - if (oldVuid != null) return oldVuid + if (oldVuid != null) { + return oldVuid + } val vuid = makeVuid() save(context, vuid) @@ -69,4 +77,9 @@ class VuidManager private constructor(context: Context) { val storage = OptlyStorage(context) storage.saveString(keyForVuid, vuid) } + + fun removeVuid(context: Context) { + val storage = OptlyStorage(context) + storage.remove(keyForVuid) + } } diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java b/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java index 260acbb7..83e22d09 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java @@ -91,4 +91,8 @@ private SharedPreferences.Editor getWritablePrefs() { private SharedPreferences getReadablePrefs() { return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); } + + public void remove(String key) { + getWritablePrefs().remove(key).apply(); + } }