Skip to content

Commit e25d5f6

Browse files
authored
Refactor initialization API to conform to shared design with iOS SDK. (#44)
1 parent d5cd4f0 commit e25d5f6

File tree

8 files changed

+160
-57
lines changed

8 files changed

+160
-57
lines changed

.travis.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ android:
44
- tools
55
- platform-tools
66
- build-tools-24.0.3
7-
- android-23
8-
- doc-23
7+
- android-24
8+
- doc-24
99
- extra-android-m2repository
10-
- sys-img-armeabi-v7a-android-23
10+
- sys-img-armeabi-v7a-android-24
1111
jdk:
1212
- oraclejdk8
1313
before_cache:
@@ -19,11 +19,11 @@ cache:
1919
before_script:
2020
- echo $TRAVIS_BRANCH
2121
- echo $TRAVIS_TAG
22-
- echo no | android create avd --force -n test -t android-23 --abi default/armeabi-v7a
22+
- echo no | android create avd --force -n test -t android-24 --abi default/armeabi-v7a
2323
- emulator -avd test -no-audio -no-window &
2424
- android-wait-for-emulator
2525
- adb shell input keyevent 82 &
2626
script:
27+
- ./gradlew cleanAllModules
2728
- ./gradlew testAllModules
28-
- if [[ -n $TRAVIS_TAG ]]; then ./gradlew ship; fi
29-
29+
- if [[ -n $TRAVIS_TAG ]]; then ./gradlew ship; fi

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,27 @@
1919
import android.content.Intent;
2020
import android.os.Build;
2121
import android.support.annotation.RequiresApi;
22+
import android.support.test.InstrumentationRegistry;
2223
import android.support.test.espresso.core.deps.guava.util.concurrent.ListeningExecutorService;
2324
import android.support.test.espresso.core.deps.guava.util.concurrent.MoreExecutors;
2425
import android.support.test.runner.AndroidJUnit4;
2526

27+
import com.optimizely.ab.android.shared.Cache;
2628
import com.optimizely.ab.android.shared.ServiceScheduler;
2729
import com.optimizely.ab.android.user_experiment_record.AndroidUserExperimentRecord;
2830

31+
import org.junit.After;
2932
import org.junit.Before;
3033
import org.junit.Test;
3134
import org.junit.runner.RunWith;
3235
import org.mockito.ArgumentCaptor;
36+
import org.mockito.Mock;
3337
import org.slf4j.Logger;
3438

3539
import java.util.concurrent.TimeUnit;
3640

3741
import static junit.framework.Assert.assertEquals;
42+
import static junit.framework.Assert.assertFalse;
3843
import static junit.framework.Assert.assertNotNull;
3944
import static junit.framework.Assert.assertNull;
4045
import static junit.framework.Assert.assertTrue;
@@ -71,20 +76,21 @@ public class OptimizelyManagerTest {
7176
public void setup() {
7277
logger = mock(Logger.class);
7378
executor = MoreExecutors.newDirectExecutorService();
74-
optimizelyManager = new OptimizelyManager("1", 1L, TimeUnit.HOURS, 1L, TimeUnit.HOURS, executor, logger);
79+
optimizelyManager = new OptimizelyManager("7595190003", 1L, TimeUnit.HOURS, 1L, TimeUnit.HOURS, executor, logger);
7580
}
7681

82+
7783
@SuppressWarnings("WrongConstant")
7884
@Test
79-
public void start() {
85+
public void initialize() {
8086
OptimizelyStartListener startListener = mock(OptimizelyStartListener.class);
8187
Context context = mock(Context.class);
8288
Context appContext = mock(Context.class);
8389
when(context.getApplicationContext()).thenReturn(appContext);
8490
when(appContext.getPackageName()).thenReturn("com.optly");
8591
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
8692

87-
optimizelyManager.start(context, startListener);
93+
optimizelyManager.initialize(context, startListener);
8894

8995
assertNotNull(optimizelyManager.getOptimizelyStartListener());
9096
assertNotNull(optimizelyManager.getDataFileServiceConnection());

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import android.support.annotation.NonNull;
2020
import android.support.annotation.Nullable;
21+
import android.support.annotation.VisibleForTesting;
2122

2223
import com.optimizely.ab.android.shared.Cache;
2324

@@ -31,7 +32,7 @@
3132
/*
3233
* Abstracts the actual data "file" {@link java.io.File}
3334
*/
34-
class DataFileCache {
35+
public class DataFileCache {
3536

3637
private static final String OPTLY_DATA_FILE_NAME = "optly-data-file-%s.json";
3738

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ public class OptimizelyClient {
8888
}
8989
}
9090

91+
/**
92+
* Check that this is a valid instance
93+
* @return True if the OptimizelyClient instance was instantiated correctly
94+
*/
95+
public boolean isValid() {
96+
return optimizely != null;
97+
}
98+
9199
/**
92100
* Track an event for a user
93101
* @param eventName the name of the event

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

Lines changed: 112 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import com.optimizely.ab.event.internal.payload.Event;
4747
import com.optimizely.ab.android.user_experiment_record.AndroidUserExperimentRecord;
4848

49+
import org.json.JSONObject;
4950
import org.slf4j.Logger;
5051
import org.slf4j.LoggerFactory;
5152

@@ -117,6 +118,91 @@ void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStar
117118
this.optimizelyStartListener = optimizelyStartListener;
118119
}
119120

121+
/**
122+
* Initialize Optimizely Synchronously
123+
* <p>
124+
* Instantiates and returns an {@link OptimizelyClient} instance. Will also cache the instance
125+
* for future lookups via getClient
126+
* @param context any {@link Context} instance
127+
* @param datafile the datafile
128+
* @return an {@link OptimizelyClient} instance
129+
*/
130+
public OptimizelyClient initialize(@NonNull Context context, @NonNull String datafile) {
131+
if (!isAndroidVersionSupported()) {
132+
return optimizelyClient;
133+
}
134+
135+
AndroidUserExperimentRecord userExperimentRecord =
136+
(AndroidUserExperimentRecord) AndroidUserExperimentRecord.newInstance(getProjectId(), context);
137+
// The User Experiment Record is started on the main thread on an asynchronous start.
138+
// Starting simply creates the file if it doesn't exist so it's not
139+
// terribly expensive. Blocking the UI thread prevents touch input...
140+
userExperimentRecord.start();
141+
try {
142+
optimizelyClient = buildOptimizely(context, datafile, userExperimentRecord);
143+
} catch (ConfigParseException e) {
144+
logger.error("Unable to parse compiled data file", e);
145+
}
146+
147+
148+
// After instantiating the OptimizelyClient, we will begin the datafile sync so that next time
149+
// the user can instantiate with the latest datafile
150+
final Intent intent = new Intent(context.getApplicationContext(), DataFileService.class);
151+
if (dataFileServiceConnection == null) {
152+
this.dataFileServiceConnection = new DataFileServiceConnection(this);
153+
context.getApplicationContext().bindService(intent, dataFileServiceConnection, Context.BIND_AUTO_CREATE);
154+
}
155+
156+
return optimizelyClient;
157+
}
158+
159+
/**
160+
* Initialize Optimizely Synchronously
161+
* <p>
162+
* Instantiates and returns an {@link OptimizelyClient} instance. Will also cache the instance
163+
* for future lookups via getClient. The datafile should be stored in res/raw.
164+
*
165+
* @param context any {@link Context} instance
166+
* @param dataFileRes the R id that the data file is located under.
167+
* @return an {@link OptimizelyClient} instance
168+
*/
169+
@NonNull
170+
public OptimizelyClient initialize(@NonNull Context context, @RawRes int dataFileRes) {
171+
try {
172+
String datafile = loadRawResource(context, dataFileRes);
173+
return initialize(context, datafile);
174+
} catch (IOException e) {
175+
logger.error("Unable to load compiled data file", e);
176+
}
177+
178+
// return dummy client if not able to initialize a valid one
179+
return optimizelyClient;
180+
}
181+
182+
/**
183+
* Initialize Optimizely Synchronously
184+
* <p>
185+
* Instantiates and returns an {@link OptimizelyClient} instance using the datafile cached on disk
186+
* if not available then it will return a dummy instance.
187+
* @param context any {@link Context} instance
188+
* @return an {@link OptimizelyClient} instance
189+
*/
190+
public OptimizelyClient initialize(@NonNull Context context) {
191+
DataFileCache dataFileCache = new DataFileCache(
192+
projectId,
193+
new Cache(context, LoggerFactory.getLogger(Cache.class)),
194+
LoggerFactory.getLogger(DataFileCache.class)
195+
);
196+
197+
JSONObject datafile = dataFileCache.load();
198+
if (datafile != null) {
199+
return initialize(context, datafile.toString());
200+
}
201+
202+
// return dummy client if not able to initialize a valid one
203+
return optimizelyClient;
204+
}
205+
120206
/**
121207
* Starts Optimizely asynchronously
122208
* <p>
@@ -130,30 +216,32 @@ void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStar
130216
* @param optimizelyStartListener callback that {@link OptimizelyClient} instances are sent to.
131217
*/
132218
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
133-
public void start(@NonNull Activity activity, @NonNull OptimizelyStartListener optimizelyStartListener) {
219+
public void initialize(@NonNull Activity activity, @NonNull OptimizelyStartListener optimizelyStartListener) {
134220
if (!isAndroidVersionSupported()) {
135221
return;
136222
}
137223
activity.getApplication().registerActivityLifecycleCallbacks(new OptlyActivityLifecycleCallbacks(this));
138-
start(activity.getApplication(), optimizelyStartListener);
224+
initialize(activity.getApplicationContext(), optimizelyStartListener);
139225
}
140226

141227
/**
142228
* @param context any type of context instance
143229
* @param optimizelyStartListener callback that {@link OptimizelyClient} instances are sent to.
144-
* @see #start(Activity, OptimizelyStartListener)
230+
* @see #initialize(Activity, OptimizelyStartListener)
145231
* <p>
146232
* This method does the same thing except it can be used with a generic {@link Context}.
147233
* When using this method be sure to call {@link #stop(Context)} to unbind {@link DataFileService}.
148234
*/
149-
public void start(@NonNull Context context, @NonNull OptimizelyStartListener optimizelyStartListener) {
235+
public void initialize(@NonNull Context context, @NonNull OptimizelyStartListener optimizelyStartListener) {
150236
if (!isAndroidVersionSupported()) {
151237
return;
152238
}
153239
this.optimizelyStartListener = optimizelyStartListener;
154-
this.dataFileServiceConnection = new DataFileServiceConnection(this);
155240
final Intent intent = new Intent(context.getApplicationContext(), DataFileService.class);
156-
context.getApplicationContext().bindService(intent, dataFileServiceConnection, Context.BIND_AUTO_CREATE);
241+
if (dataFileServiceConnection == null) {
242+
this.dataFileServiceConnection = new DataFileServiceConnection(this);
243+
context.getApplicationContext().bindService(intent, dataFileServiceConnection, Context.BIND_AUTO_CREATE);
244+
}
157245
}
158246

159247
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@@ -165,7 +253,7 @@ void stop(@NonNull Activity activity, @NonNull OptlyActivityLifecycleCallbacks o
165253
/**
166254
* Unbinds {@link DataFileService}
167255
* <p>
168-
* Calling this is not necessary if using {@link #start(Activity, OptimizelyStartListener)} which
256+
* Calling this is not necessary if using {@link #initialize(Activity, OptimizelyStartListener)} which
169257
* handles unbinding implicitly.
170258
*
171259
* @param context any {@link Context} instance
@@ -185,12 +273,11 @@ public void stop(@NonNull Context context) {
185273
/**
186274
* Gets a cached Optimizely instance
187275
* <p>
188-
* If {@link #start(Activity, OptimizelyStartListener)} or {@link #start(Context, OptimizelyStartListener)}
276+
* If {@link #initialize(Activity, OptimizelyStartListener)} or {@link #initialize(Context, OptimizelyStartListener)}
189277
* has not been called yet the returned {@link OptimizelyClient} instance will be a dummy instance
190278
* that logs warnings in order to prevent {@link NullPointerException}.
191279
* <p>
192-
* If {@link #getOptimizely(Context, int)} was used the built {@link OptimizelyClient} instance
193-
* will be updated. Using {@link #start(Activity, OptimizelyStartListener)} or {@link #start(Context, OptimizelyStartListener)}
280+
* Using {@link #initialize(Activity, OptimizelyStartListener)} or {@link #initialize(Context, OptimizelyStartListener)}
194281
* will update the cached instance with a new {@link OptimizelyClient} built from a cached local
195282
* datafile on disk or a remote datafile on the CDN.
196283
*
@@ -202,41 +289,6 @@ public OptimizelyClient getOptimizely() {
202289
return optimizelyClient;
203290
}
204291

205-
/**
206-
* Create an instance of {@link OptimizelyClient} from a compiled in datafile
207-
* <p>
208-
* After using this method successfully {@link #getOptimizely()} will contain a
209-
* cached instance built from the compiled in datafile. The datafile should be
210-
* stored in res/raw.
211-
*
212-
* @param context any {@link Context} instance
213-
* @param dataFileRes the R id that the data file is located under.
214-
* @return an {@link OptimizelyClient} instance
215-
*/
216-
@NonNull
217-
public OptimizelyClient getOptimizely(@NonNull Context context, @RawRes int dataFileRes) {
218-
if (!isAndroidVersionSupported()) {
219-
return optimizelyClient;
220-
}
221-
222-
AndroidUserExperimentRecord userExperimentRecord =
223-
(AndroidUserExperimentRecord) AndroidUserExperimentRecord.newInstance(getProjectId(), context);
224-
// Blocking File I/O is necessary here in order to provide a synchronous API
225-
// The User Experiment Record is started off the of the main thread when starting
226-
// asynchronously. Starting simply creates the file if it doesn't exist so it's not
227-
// terribly expensive. Blocking the UI the thread prevents touch input...
228-
userExperimentRecord.start();
229-
try {
230-
optimizelyClient = buildOptimizely(context, loadRawResource(context, dataFileRes), userExperimentRecord);
231-
} catch (ConfigParseException e) {
232-
logger.error("Unable to parse compiled data file", e);
233-
} catch (IOException e) {
234-
logger.error("Unable to load compiled data file", e);
235-
}
236-
237-
return optimizelyClient;
238-
}
239-
240292
private String loadRawResource(Context context, @RawRes int rawRes) throws IOException {
241293
Resources res = context.getResources();
242294
InputStream in = res.openRawResource(rawRes);
@@ -249,6 +301,21 @@ private String loadRawResource(Context context, @RawRes int rawRes) throws IOExc
249301
}
250302
}
251303

304+
/**
305+
* Check if the datafile is cached on the disk
306+
* @param context any {@link Context} instance
307+
* @return True if the datafile is cached on the disk
308+
*/
309+
public boolean isDatafileCached(Context context) {
310+
DataFileCache dataFileCache = new DataFileCache(
311+
projectId,
312+
new Cache(context, LoggerFactory.getLogger(Cache.class)),
313+
LoggerFactory.getLogger(DataFileCache.class)
314+
);
315+
316+
return dataFileCache.exists();
317+
}
318+
252319
@NonNull
253320
String getProjectId() {
254321
return projectId;

android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
import java.util.HashMap;
2828

29+
import static junit.framework.Assert.assertTrue;
30+
import static junit.framework.Assert.assertFalse;
2931
import static org.mockito.Mockito.verify;
3032

3133
/**
@@ -163,4 +165,16 @@ public void testBadGetVariation3() {
163165
verify(logger).warn("Optimizely is not initialized, could not get variation for experiment {} " +
164166
"for user {} with attributes", "1", "1");
165167
}
168+
169+
@Test
170+
public void testIsValid() {
171+
OptimizelyClient optimizelyClient = new OptimizelyClient(optimizely, logger);
172+
assertTrue(optimizelyClient.isValid());
173+
}
174+
175+
@Test
176+
public void testIsInvalid() {
177+
OptimizelyClient optimizelyClient = new OptimizelyClient(null, logger);
178+
assertFalse(optimizelyClient.isValid());
179+
}
166180
}

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ task ship() {
7373
'event-handler:uploadArchives', 'user-experiment-record:uploadArchives')
7474
}
7575

76+
task cleanAllModules << {
77+
logger.info("Running clena for all modules")
78+
}
79+
80+
cleanAllModules.dependsOn(':android-sdk:clean', ':event-handler:clean',
81+
':user-experiment-record:clean', ':shared:clean')
82+
7683
task testAllModules << {
7784
logger.info("Running android tests for all modules")
7885
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected void onCreate(Bundle savedInstanceState) {
4747
optimizelyManager = myApplication.getOptimizelyManager();
4848

4949
// Load Optimizely from a compiled in data file
50-
final OptimizelyClient optimizely = optimizelyManager.getOptimizely(this, R.raw.data_file);
50+
final OptimizelyClient optimizely = optimizelyManager.initialize(this, R.raw.data_file);
5151
CountingIdlingResourceManager.increment(); // For impression event
5252
Variation variation = optimizely.activate("experiment_0", myApplication.getAnonUserId(), myApplication.getAttributes());
5353
if (variation != null) {
@@ -79,7 +79,7 @@ protected void onStart() {
7979
super.onStart();
8080

8181
CountingIdlingResourceManager.increment(); // For Optimizely starting
82-
optimizelyManager.start(this, new OptimizelyStartListener() {
82+
optimizelyManager.initialize(this, new OptimizelyStartListener() {
8383
@Override
8484
public void onStart(OptimizelyClient optimizely) {
8585
CountingIdlingResourceManager.decrement(); // For Optimizely starting

0 commit comments

Comments
 (0)