Skip to content

Commit 8bea0f1

Browse files
authored
[FSSDK-9918] add returnInMainThread flag for async init (#470)
This PR adds a flag (returnInMainThread) to async initialization for controlling to return on completion in main thread or background thread. - by default, returnInMainThread = true for backward compatibility. - set returnInMainThread = false to return in a background thread.
1 parent f117429 commit 8bea0f1

File tree

7 files changed

+198
-50
lines changed

7 files changed

+198
-50
lines changed

android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import android.content.pm.PackageInfo;
2222
import android.content.pm.PackageManager;
2323
import android.os.Build;
24+
import android.util.Log;
2425

2526
import androidx.test.ext.junit.runners.AndroidJUnit4;
2627
import androidx.test.filters.SdkSuppress;
@@ -48,6 +49,8 @@
4849
import org.mockito.stubbing.Answer;
4950
import org.slf4j.Logger;
5051

52+
import java.util.Map;
53+
import java.util.concurrent.CountDownLatch;
5154
import java.util.concurrent.ExecutorService;
5255
import java.util.concurrent.Executors;
5356
import java.util.concurrent.TimeUnit;
@@ -58,6 +61,7 @@
5861
import static junit.framework.Assert.assertNull;
5962
import static junit.framework.Assert.assertTrue;
6063
import static junit.framework.Assert.fail;
64+
import static org.junit.Assert.assertNotEquals;
6165
import static org.mockito.Matchers.any;
6266
import static org.mockito.Matchers.eq;
6367
import static org.mockito.Mockito.doAnswer;
@@ -359,7 +363,7 @@ public void injectOptimizely() {
359363
UserProfileService userProfileService = mock(UserProfileService.class);
360364
OptimizelyStartListener startListener = mock(OptimizelyStartListener.class);
361365

362-
optimizelyManager.setOptimizelyStartListener(startListener);
366+
optimizelyManager.setOptimizelyStartListener(startListener, true);
363367
optimizelyManager.injectOptimizely(context, userProfileService, minDatafile);
364368
try {
365369
executor.awaitTermination(5, TimeUnit.SECONDS);
@@ -750,6 +754,72 @@ public void initializeSyncWithResourceDatafileNoCacheWithDefaultParams() {
750754
verify(manager).initialize(eq(context), eq(defaultDatafile), eq(true), eq(false));
751755
}
752756

757+
@Test
758+
public void initializeAsyncCallbackInBackgroundThread() throws InterruptedException {
759+
OptimizelyManager optimizelyManager = OptimizelyManager.builder(testProjectId)
760+
.build(InstrumentationRegistry.getInstrumentation().getTargetContext());
761+
762+
CountDownLatch latch = new CountDownLatch(1);
763+
764+
// by default, async init returns in main thread.
765+
// this parameter should be set to false to overrule it.
766+
boolean returnInMainThread = false;
767+
768+
optimizelyManager.initialize(
769+
InstrumentationRegistry.getInstrumentation().getContext(),
770+
null,
771+
returnInMainThread,
772+
(client) -> {
773+
Log.d("Optly", "[TESTING] " + Thread.currentThread().getName());
774+
try {
775+
assertNotEquals(
776+
"OptimizelyStartListener should be called in a background thread",
777+
"main", Thread.currentThread().getName()
778+
);
779+
latch.countDown();
780+
} catch (AssertionError e) {
781+
// we need catch and silence this assertion error, otherwise it will be caught in OptimizeManager,
782+
// and give a wrong error message. The failure will be detected with the latch timeout below.
783+
}
784+
}
785+
);
786+
787+
boolean completed = latch.await(1, TimeUnit.SECONDS);
788+
if (!completed) {
789+
fail("OptimizelyStartListener thread checking failed");
790+
}
791+
}
792+
793+
@Test
794+
public void initializeAsyncCallbackInMainThread() throws InterruptedException {
795+
OptimizelyManager optimizelyManager = OptimizelyManager.builder(testProjectId)
796+
.build(InstrumentationRegistry.getInstrumentation().getTargetContext());
797+
798+
CountDownLatch latch = new CountDownLatch(1);
799+
800+
optimizelyManager.initialize(
801+
InstrumentationRegistry.getInstrumentation().getContext(),
802+
null,
803+
(client) -> {
804+
Log.d("Optly", "[TESTING] " + Thread.currentThread().getName());
805+
try {
806+
assertEquals(
807+
"OptimizelyStartListener should be called in a background thread",
808+
"main", Thread.currentThread().getName()
809+
);
810+
latch.countDown();
811+
} catch (AssertionError e) {
812+
// we need catch and silence this assertion error, otherwise it will be caught in OptimizeManager,
813+
// and give a wrong error message. The failure will be detected with the latch timeout below.
814+
}
815+
}
816+
);
817+
818+
boolean completed = latch.await(1, TimeUnit.SECONDS);
819+
if (!completed) {
820+
fail("OptimizelyStartListener thread checking failed");
821+
}
822+
}
753823

754824
// Utils
755825

android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public class OptimizelyManager {
9797
@Nullable private final String vuid;
9898

9999
@Nullable private OptimizelyStartListener optimizelyStartListener;
100+
private boolean returnInMainThreadFromAsyncInit = true;
100101

101102
@Nullable private final List<OptimizelyDecideOption> defaultDecideOptions;
102103
private String sdkVersion = null;
@@ -175,8 +176,14 @@ OptimizelyStartListener getOptimizelyStartListener() {
175176
return optimizelyStartListener;
176177
}
177178

178-
void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStartListener) {
179+
void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStartListener, boolean returnInMainThread) {
179180
this.optimizelyStartListener = optimizelyStartListener;
181+
this.returnInMainThreadFromAsyncInit = returnInMainThread;
182+
}
183+
184+
void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStartListener) {
185+
boolean returnInMainThread = true;
186+
setOptimizelyStartListener(optimizelyStartListener, returnInMainThread);
180187
}
181188

182189
private void notifyStartListener() {
@@ -398,11 +405,27 @@ public void initialize(@NonNull final Context context, @NonNull OptimizelyStartL
398405
* @see #initialize(Context, Integer, OptimizelyStartListener)
399406
*/
400407
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
401-
public void initialize(@NonNull final Context context, @RawRes final Integer datafileRes, @NonNull OptimizelyStartListener optimizelyStartListener) {
408+
public void initialize(
409+
@NonNull final Context context,
410+
@RawRes final Integer datafileRes,
411+
@NonNull OptimizelyStartListener optimizelyStartListener)
412+
{
413+
// return in main thread after async completed (backward compatible)
414+
boolean returnInMainThread = true;
415+
initialize(context, datafileRes, returnInMainThread, optimizelyStartListener);
416+
}
417+
418+
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
419+
public void initialize(
420+
@NonNull final Context context,
421+
@RawRes final Integer datafileRes,
422+
final boolean returnInMainThread,
423+
@NonNull OptimizelyStartListener optimizelyStartListener)
424+
{
402425
if (!isAndroidVersionSupported()) {
403426
return;
404427
}
405-
setOptimizelyStartListener(optimizelyStartListener);
428+
setOptimizelyStartListener(optimizelyStartListener, returnInMainThread);
406429
datafileHandler.downloadDatafile(context, datafileConfig, getDatafileLoadedListener(context,datafileRes));
407430
}
408431

@@ -553,7 +576,7 @@ public void onStartComplete(UserProfileService userProfileService) {
553576
logger.info("No listener to send Optimizely to");
554577
}
555578
}
556-
});
579+
}, returnInMainThreadFromAsyncInit);
557580
}
558581
else {
559582
if (optimizelyStartListener != null) {

test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInJava.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ static public void samplesAll(Context context) {
9090
samplesForDoc_NotificatonListener(context);
9191
samplesForDoc_OlderVersions(context);
9292
samplesForDoc_ForcedDecision(context);
93-
samplesForDoc_ODP(context);
93+
samplesForDoc_ODP_async(context);
94+
samplesForDoc_ODP_sync(context);
9495
}
9596

9697
static public void samplesForDecide(Context context) {
@@ -859,7 +860,7 @@ static public void samplesForDoc_ForcedDecision(Context context) {
859860
success = user.removeAllForcedDecisions();
860861
}
861862

862-
static public void samplesForDoc_ODP(Context context) {
863+
static public void samplesForDoc_ODP_async(Context context) {
863864
OptimizelyManager optimizelyManager = OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context);
864865
optimizelyManager.initialize(context, null, (OptimizelyClient client) -> {
865866
OptimizelyUserContext userContext = client.createUserContext("user_123");
@@ -871,4 +872,19 @@ static public void samplesForDoc_ODP(Context context) {
871872
});
872873
}
873874

875+
static public void samplesForDoc_ODP_sync(Context context) {
876+
OptimizelyManager optimizelyManager = OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context);
877+
878+
boolean returnInMainThread = false;
879+
880+
optimizelyManager.initialize(context, null, returnInMainThread, (OptimizelyClient client) -> {
881+
OptimizelyUserContext userContext = client.createUserContext("user_123");
882+
userContext.fetchQualifiedSegments();
883+
884+
Log.d("Optimizely", "[ODP] segments = " + userContext.getQualifiedSegments());
885+
OptimizelyDecision optDecision = userContext.decide("odp-flag-1");
886+
Log.d("Optimizely", "[ODP] decision = " + optDecision.toString());
887+
});
888+
}
889+
874890
}

test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInKotlin.kt

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ package com.optimizely.ab.android.test_app
1818
import android.content.Context
1919
import android.content.IntentFilter
2020
import android.net.wifi.WifiManager
21-
import android.os.Parcel
22-
import android.os.Parcelable
2321
import android.util.Log
2422
import com.optimizely.ab.OptimizelyDecisionContext
2523
import com.optimizely.ab.OptimizelyForcedDecision
@@ -29,7 +27,6 @@ import com.optimizely.ab.android.event_handler.EventRescheduler
2927
import com.optimizely.ab.android.sdk.OptimizelyClient
3028
import com.optimizely.ab.android.sdk.OptimizelyManager
3129
import com.optimizely.ab.bucketing.UserProfileService
32-
import com.optimizely.ab.config.Variation
3330
import com.optimizely.ab.config.parser.JsonParseException
3431
import com.optimizely.ab.error.ErrorHandler
3532
import com.optimizely.ab.error.RaiseExceptionErrorHandler
@@ -40,12 +37,8 @@ import com.optimizely.ab.notification.DecisionNotification
4037
import com.optimizely.ab.notification.NotificationHandler
4138
import com.optimizely.ab.notification.TrackNotification
4239
import com.optimizely.ab.notification.UpdateConfigNotification
43-
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig
4440
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption
4541
import com.optimizely.ab.optimizelydecision.OptimizelyDecision
46-
import com.optimizely.ab.optimizelyjson.OptimizelyJSON
47-
import org.slf4j.LoggerFactory
48-
import java.lang.Exception
4942
import java.util.*
5043
import java.util.concurrent.TimeUnit
5144

@@ -76,7 +69,8 @@ object APISamplesInKotlin {
7669
samplesForDoc_NotificatonListener(context)
7770
samplesForDoc_OlderVersions(context)
7871
samplesForDoc_ForcedDecision(context)
79-
samplesForDoc_ODP(context)
72+
samplesForDoc_ODP_async(context)
73+
samplesForDoc_ODP_sync(context)
8074
}
8175

8276
fun samplesForDecide(context: Context) {
@@ -829,7 +823,7 @@ object APISamplesInKotlin {
829823
success = user.removeAllForcedDecisions()
830824
}
831825

832-
fun samplesForDoc_ODP(context: Context?) {
826+
fun samplesForDoc_ODP_async(context: Context?) {
833827
val optimizelyManager =
834828
OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context)
835829
optimizelyManager.initialize(context!!, null) { client: OptimizelyClient ->
@@ -842,6 +836,22 @@ object APISamplesInKotlin {
842836
}
843837
}
844838

839+
fun samplesForDoc_ODP_sync(context: Context?) {
840+
val optimizelyManager =
841+
OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context)
842+
843+
val returnInMainThread = false;
844+
845+
optimizelyManager.initialize(context!!, null, returnInMainThread) { client: OptimizelyClient ->
846+
val userContext = client.createUserContext("user_123")
847+
userContext!!.fetchQualifiedSegments()
848+
849+
Log.d("Optimizely", "[ODP] segments = " + userContext.qualifiedSegments)
850+
val optDecision = userContext.decide("odp-flag-1")
851+
Log.d("Optimizely", "[ODP] decision = $optDecision")
852+
}
853+
}
854+
845855
}
846856

847857

test-app/src/main/java/com/optimizely/ab/android/test_app/SplashScreenActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.optimizely.ab.android.event_handler.EventRescheduler
2525
import com.optimizely.ab.android.sdk.OptimizelyClient
2626
import com.optimizely.ab.android.sdk.OptimizelyManager
2727
import com.optimizely.ab.android.shared.CountingIdlingResourceManager
28+
import com.optimizely.ab.android.test_app.Samples.APISamplesInJava
2829
import com.optimizely.ab.notification.DecisionNotification
2930
import com.optimizely.ab.notification.TrackNotification
3031
import com.optimizely.ab.notification.UpdateConfigNotification
@@ -131,4 +132,4 @@ class SplashScreenActivity : AppCompatActivity() {
131132
// The Idling Resource which will be null in production.
132133
private val countingIdlingResourceManager: CountingIdlingResourceManager? = null
133134
}
134-
}
135+
}

user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DefaultUserProfileServiceTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import java.util.Map;
3131
import java.util.concurrent.ConcurrentHashMap;
32+
import java.util.concurrent.CountDownLatch;
3233
import java.util.concurrent.ExecutorService;
3334
import java.util.concurrent.Executors;
3435
import java.util.concurrent.TimeUnit;
@@ -39,6 +40,8 @@
3940
import static junit.framework.Assert.assertTrue;
4041
import static junit.framework.Assert.fail;
4142
import static org.mockito.Mockito.mock;
43+
import static org.mockito.Mockito.spy;
44+
import static org.mockito.Mockito.verify;
4245

4346
/**
4447
* Tests for {@link DefaultUserProfileService}
@@ -101,6 +104,20 @@ public void teardown() {
101104
cache.delete(diskCache.getFileName());
102105
}
103106

107+
@Test
108+
public void startInBackground() throws InterruptedException {
109+
DefaultUserProfileService ups = spy(DefaultUserProfileService.class);
110+
111+
CountDownLatch latch = new CountDownLatch(1);
112+
ups.startInBackground((u) -> {
113+
latch.countDown();
114+
});
115+
116+
latch.await(3, TimeUnit.SECONDS);
117+
118+
verify(ups).start();
119+
}
120+
104121
@Test
105122
public void saveAndStartAndLookup() {
106123
userProfileService.save(userProfileMap1);

0 commit comments

Comments
 (0)