Skip to content

Commit 02bc421

Browse files
authored
feature(Reachability): add connection checking for datafile-fetch and event-dispatch (#389)
- Add connection constraints to WorkManager requests for datafile-fetch and event-dispatch - Fix default event-dispatch retry to disabled (configurable)
1 parent 4f31d45 commit 02bc421

File tree

23 files changed

+733
-111
lines changed

23 files changed

+733
-111
lines changed

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import android.annotation.TargetApi;
2020
import android.app.Activity;
2121
import android.app.Application;
22+
import android.app.job.JobInfo;
2223
import android.content.Context;
2324
import android.content.res.Resources;
2425
import android.os.Build;
@@ -832,7 +833,6 @@ public Builder withEventDispatchRetryInterval(long interval, TimeUnit timeUnit)
832833
@Deprecated
833834
public Builder withEventDispatchInterval(long interval) {
834835
this.eventFlushInterval = interval;
835-
this.eventDispatchRetryInterval = interval;
836836
return this;
837837
}
838838

@@ -898,10 +898,15 @@ public OptimizelyManager build(Context context) {
898898

899899
if (datafileDownloadInterval > 0) {
900900
// JobScheduler API doesn't allow intervals less than 15 minutes
901-
// if (datafileDownloadInterval < 900) {
902-
// datafileDownloadInterval = 900;
903-
// logger.warn("Minimum datafile polling interval is 15 minutes. Defaulting to 15 minutes.");
904-
// }
901+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
902+
long minIntervalSecs = TimeUnit.MILLISECONDS.toSeconds(JobInfo.getMinPeriodMillis());
903+
long minIntervalMins = TimeUnit.SECONDS.toMinutes(minIntervalSecs);
904+
905+
if (datafileDownloadInterval < minIntervalSecs) {
906+
datafileDownloadInterval = minIntervalSecs;
907+
logger.warn("Minimum datafile polling interval is {} minutes. Defaulting to the minimum.", minIntervalMins);
908+
}
909+
}
905910
}
906911

907912
if (datafileConfig == null) {
@@ -922,7 +927,9 @@ public OptimizelyManager build(Context context) {
922927
}
923928

924929
if (eventHandler == null) {
925-
eventHandler = DefaultEventHandler.getInstance(context);
930+
DefaultEventHandler defaultHandler = DefaultEventHandler.getInstance(context);
931+
defaultHandler.setDispatchInterval(eventDispatchRetryInterval);
932+
eventHandler = defaultHandler;
926933
}
927934

928935
if(notificationCenter == null) {

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

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import android.content.Context;
2020

2121
import com.optimizely.ab.android.datafile_handler.DatafileHandler;
22+
import com.optimizely.ab.android.event_handler.DefaultEventHandler;
2223
import com.optimizely.ab.android.shared.DatafileConfig;
2324
import com.optimizely.ab.android.user_profile.DefaultUserProfileService;
2425
import com.optimizely.ab.bucketing.UserProfileService;
@@ -28,9 +29,12 @@
2829
import com.optimizely.ab.event.EventProcessor;
2930
import com.optimizely.ab.notification.NotificationCenter;
3031

32+
import org.junit.Before;
3133
import org.junit.Test;
3234
import org.junit.runner.RunWith;
3335
import org.mockito.runners.MockitoJUnitRunner;
36+
import org.powermock.api.mockito.PowerMockito;
37+
import org.powermock.core.classloader.annotations.PowerMockIgnore;
3438
import org.powermock.core.classloader.annotations.PrepareForTest;
3539
import org.powermock.modules.junit4.PowerMockRunner;
3640
import org.slf4j.Logger;
@@ -54,30 +58,42 @@
5458
import static org.mockito.Mockito.never;
5559
import static org.mockito.Mockito.verify;
5660
import static org.mockito.Mockito.when;
61+
import static org.powermock.api.mockito.PowerMockito.mockStatic;
5762
import static org.powermock.api.mockito.PowerMockito.verifyNew;
5863
import static org.powermock.api.mockito.PowerMockito.whenNew;
5964

6065

6166
@RunWith(PowerMockRunner.class)
62-
@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class})
67+
@PowerMockIgnore("jdk.internal.reflect.*")
68+
@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class})
6369
public class OptimizelyManagerIntervalTest {
6470

6571
private Logger logger;
72+
private Context mockContext;
73+
private DefaultEventHandler mockEventHandler;
6674

67-
// DatafileDownloadInterval
75+
@Before
76+
public void setup() throws Exception {
77+
mockContext = mock(Context.class);
78+
when(mockContext.getApplicationContext()).thenReturn(mockContext);
6879

69-
@Test
70-
public void testBuildWithDatafileDownloadInterval() throws Exception {
7180
whenNew(OptimizelyManager.class).withAnyArguments().thenReturn(mock(OptimizelyManager.class));
81+
whenNew(BatchEventProcessor.class).withAnyArguments().thenReturn(mock(BatchEventProcessor.class));
7282

73-
Context appContext = mock(Context.class);
74-
when(appContext.getApplicationContext()).thenReturn(appContext);
83+
mockEventHandler = mock(DefaultEventHandler.class);
84+
mockStatic(DefaultEventHandler.class);
85+
when(DefaultEventHandler.getInstance(any())).thenReturn(mockEventHandler);
86+
}
87+
88+
// DatafileDownloadInterval
7589

90+
@Test
91+
public void testBuildWithDatafileDownloadInterval() throws Exception {
7692
long goodNumber = 27;
7793
OptimizelyManager manager = OptimizelyManager.builder("1")
7894
.withLogger(logger)
7995
.withDatafileDownloadInterval(goodNumber, TimeUnit.MINUTES)
80-
.build(appContext);
96+
.build(mockContext);
8197

8298
verifyNew(OptimizelyManager.class).withArguments(anyString(),
8399
anyString(),
@@ -96,16 +112,11 @@ public void testBuildWithDatafileDownloadInterval() throws Exception {
96112

97113
@Test
98114
public void testBuildWithDatafileDownloadIntervalDeprecated() throws Exception {
99-
whenNew(OptimizelyManager.class).withAnyArguments().thenReturn(mock(OptimizelyManager.class));
100-
101-
Context appContext = mock(Context.class);
102-
when(appContext.getApplicationContext()).thenReturn(appContext);
103-
104115
long goodNumber = 1234L;
105116
OptimizelyManager manager = OptimizelyManager.builder("1")
106117
.withLogger(logger)
107118
.withDatafileDownloadInterval(goodNumber) // deprecated
108-
.build(appContext);
119+
.build(mockContext);
109120

110121
verifyNew(OptimizelyManager.class).withArguments(anyString(),
111122
anyString(),
@@ -124,17 +135,11 @@ public void testBuildWithDatafileDownloadIntervalDeprecated() throws Exception {
124135

125136
@Test
126137
public void testBuildWithEventDispatchInterval() throws Exception {
127-
whenNew(OptimizelyManager.class).withAnyArguments().thenReturn(mock(OptimizelyManager.class));
128-
whenNew(BatchEventProcessor.class).withAnyArguments().thenReturn(mock(BatchEventProcessor.class));
129-
130-
Context appContext = mock(Context.class);
131-
when(appContext.getApplicationContext()).thenReturn(appContext);
132-
133138
long goodNumber = 100L;
134139
OptimizelyManager manager = OptimizelyManager.builder("1")
135140
.withLogger(logger)
136141
.withEventDispatchInterval(goodNumber, TimeUnit.SECONDS)
137-
.build(appContext);
142+
.build(mockContext);
138143

139144
verifyNew(BatchEventProcessor.class).withArguments(any(BlockingQueue.class),
140145
any(EventHandler.class),
@@ -145,14 +150,16 @@ public void testBuildWithEventDispatchInterval() throws Exception {
145150
any(NotificationCenter.class),
146151
any(Object.class));
147152

153+
verify(mockEventHandler).setDispatchInterval(-1L); // default
154+
148155
verifyNew(OptimizelyManager.class).withArguments(anyString(),
149156
anyString(),
150157
any(DatafileConfig.class),
151158
any(Logger.class),
152159
anyLong(),
153160
any(DatafileHandler.class),
154161
any(ErrorHandler.class),
155-
eq(-1L), // milliseconds
162+
eq(-1L), // default
156163
any(EventHandler.class),
157164
any(EventProcessor.class),
158165
any(UserProfileService.class),
@@ -162,19 +169,14 @@ public void testBuildWithEventDispatchInterval() throws Exception {
162169

163170
@Test
164171
public void testBuildWithEventDispatchRetryInterval() throws Exception {
165-
whenNew(OptimizelyManager.class).withAnyArguments().thenReturn(mock(OptimizelyManager.class));
166-
whenNew(BatchEventProcessor.class).withAnyArguments().thenReturn(mock(BatchEventProcessor.class));
167-
168-
Context appContext = mock(Context.class);
169-
when(appContext.getApplicationContext()).thenReturn(appContext);
170-
171172
long goodNumber = 100L;
172-
long defaultEventFlushInterval = 30L;
173+
TimeUnit timeUnit = TimeUnit.MINUTES;
174+
long defaultEventFlushInterval = 30L; // seconds
173175

174176
OptimizelyManager manager = OptimizelyManager.builder("1")
175177
.withLogger(logger)
176-
.withEventDispatchRetryInterval(goodNumber, TimeUnit.MINUTES)
177-
.build(appContext);
178+
.withEventDispatchRetryInterval(goodNumber, timeUnit)
179+
.build(mockContext);
178180

179181
verifyNew(BatchEventProcessor.class).withArguments(any(BlockingQueue.class),
180182
any(EventHandler.class),
@@ -185,6 +187,8 @@ public void testBuildWithEventDispatchRetryInterval() throws Exception {
185187
any(NotificationCenter.class),
186188
any(Object.class));
187189

190+
verify(mockEventHandler).setDispatchInterval(timeUnit.toMillis(goodNumber)); // milli-seconds
191+
188192
verifyNew(OptimizelyManager.class).withArguments(anyString(),
189193
anyString(),
190194
any(DatafileConfig.class),
@@ -202,17 +206,11 @@ public void testBuildWithEventDispatchRetryInterval() throws Exception {
202206

203207
@Test
204208
public void testBuildWithEventDispatchIntervalDeprecated() throws Exception {
205-
whenNew(OptimizelyManager.class).withAnyArguments().thenReturn(mock(OptimizelyManager.class));
206-
whenNew(BatchEventProcessor.class).withAnyArguments().thenReturn(mock(BatchEventProcessor.class));
207-
208-
Context appContext = mock(Context.class);
209-
when(appContext.getApplicationContext()).thenReturn(appContext);
210-
211209
long goodNumber = 1234L;
212210
OptimizelyManager manager = OptimizelyManager.builder("1")
213211
.withLogger(logger)
214212
.withEventDispatchInterval(goodNumber) // deprecated
215-
.build(appContext);
213+
.build(mockContext);
216214

217215
verifyNew(BatchEventProcessor.class).withArguments(any(BlockingQueue.class),
218216
any(EventHandler.class),
@@ -223,14 +221,16 @@ public void testBuildWithEventDispatchIntervalDeprecated() throws Exception {
223221
any(NotificationCenter.class),
224222
any(Object.class));
225223

224+
verify(mockEventHandler).setDispatchInterval(-1L); // deprecated api not change default retryInterval
225+
226226
verifyNew(OptimizelyManager.class).withArguments(anyString(),
227227
anyString(),
228228
any(DatafileConfig.class),
229229
any(Logger.class),
230230
anyLong(),
231231
any(DatafileHandler.class),
232232
any(ErrorHandler.class),
233-
eq(goodNumber), // milliseconds
233+
eq(-1L), // deprecated api not change default retryInterval
234234
any(EventHandler.class),
235235
any(EventProcessor.class),
236236
any(UserProfileService.class),

datafile-handler/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dependencies {
5757
testImplementation "org.mockito:mockito-core:$mockito_ver"
5858
testImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
5959

60+
androidTestImplementation "androidx.work:work-testing:$work_runtime"
6061
androidTestImplementation "androidx.test.ext:junit:$androidx_test"
6162
// Set this dependency to use JUnit 4 rules
6263
androidTestImplementation "androidx.test:rules:$androidx_test"

datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,33 @@ public void noCacheAndLoadFromCDNFails() {
146146
verify(datafileLoadedListener, atMost(1)).onDatafileLoaded(null);
147147
}
148148

149+
@Test
150+
public void getDatafile_datafileCacheInjected() {
151+
final ExecutorService executor = Executors.newSingleThreadExecutor();
152+
153+
// datafileCache initially null, and passed with getDatafile()
154+
DatafileLoader datafileLoader = new DatafileLoader(context, datafileClient, null, logger);
155+
datafileCache.save("{}");
156+
when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn("");
157+
158+
// skip if datafileCache is not provided
159+
datafileLoader.getDatafile("1", datafileLoadedListener);
160+
try {
161+
executor.awaitTermination(5, TimeUnit.SECONDS);
162+
} catch (InterruptedException e) {
163+
fail();
164+
}
165+
verify(datafileLoadedListener, never()).onDatafileLoaded("{}");
166+
167+
datafileLoader.getDatafile("1", datafileCache, datafileLoadedListener);
168+
try {
169+
executor.awaitTermination(5, TimeUnit.SECONDS);
170+
} catch (InterruptedException e) {
171+
fail();
172+
}
173+
verify(datafileLoadedListener, atLeast(1)).onDatafileLoaded("{}");
174+
}
175+
149176
@Test
150177
// flacky with lower API
151178
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/****************************************************************************
2+
* Copyright 2021, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package com.optimizely.ab.android.datafile_handler;
18+
19+
import static org.hamcrest.CoreMatchers.is;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertThat;
22+
import static org.mockito.Matchers.any;
23+
import static org.mockito.Matchers.anyObject;
24+
import static org.mockito.Matchers.anyString;
25+
import static org.mockito.Matchers.eq;
26+
import static org.mockito.Mockito.mock;
27+
import static org.mockito.Mockito.verify;
28+
import static org.mockito.Mockito.when;
29+
30+
import android.content.Context;
31+
32+
import androidx.test.core.app.ApplicationProvider;
33+
import androidx.test.ext.junit.runners.AndroidJUnit4;
34+
import androidx.work.Data;
35+
import androidx.work.ListenableWorker;
36+
import androidx.work.testing.TestWorkerBuilder;
37+
38+
import com.optimizely.ab.android.shared.Cache;
39+
import com.optimizely.ab.android.shared.DatafileConfig;
40+
import com.optimizely.ab.event.LogEvent;
41+
42+
import org.junit.Before;
43+
import org.junit.Test;
44+
import org.junit.runner.RunWith;
45+
import org.slf4j.Logger;
46+
import org.slf4j.LoggerFactory;
47+
48+
import java.util.concurrent.Executor;
49+
import java.util.concurrent.Executors;
50+
51+
/**
52+
* Tests {@link DatafileWorker}
53+
*/
54+
@RunWith(AndroidJUnit4.class)
55+
public class DatafileWorkerTest {
56+
private Context context;
57+
private Executor executor;
58+
private String sdkKey = "sdkKey";
59+
private Logger logger = LoggerFactory.getLogger("test");
60+
61+
@Before
62+
public void setUp() {
63+
context = ApplicationProvider.getApplicationContext();
64+
executor = Executors.newSingleThreadExecutor();
65+
}
66+
67+
@Test
68+
public void testInputData() {
69+
DatafileConfig datafileConfig1 = new DatafileConfig(null, sdkKey);
70+
Data data = DatafileWorker.getData(datafileConfig1);
71+
72+
DatafileConfig datafileConfig2 = DatafileWorker.getDataConfig(data);
73+
assertEquals(datafileConfig2.getKey(), sdkKey);
74+
}
75+
76+
@Test
77+
public void testDatafileFetch() {
78+
DatafileWorker worker = mockDatafileWorker(sdkKey);
79+
worker.datafileLoader = mock(DatafileLoader.class);
80+
81+
ListenableWorker.Result result = worker.doWork();
82+
83+
DatafileConfig datafileConfig = new DatafileConfig(null, sdkKey);
84+
String datafileUrl = datafileConfig.getUrl();
85+
DatafileCache datafileCache = new DatafileCache(datafileConfig.getKey(), new Cache(context, logger), logger);
86+
87+
verify(worker.datafileLoader).getDatafile(eq(datafileUrl), eq(datafileCache), eq(null));
88+
assertThat(result, is(ListenableWorker.Result.success())); // success
89+
}
90+
91+
// Helpers
92+
93+
DatafileWorker mockDatafileWorker(String sdkKey) {
94+
DatafileConfig datafileConfig = new DatafileConfig(null, sdkKey);
95+
Data inputData = DatafileWorker.getData(datafileConfig);
96+
97+
return (DatafileWorker) TestWorkerBuilder.from(context, DatafileWorker.class, executor)
98+
.setInputData(inputData)
99+
.build();
100+
}
101+
102+
}

0 commit comments

Comments
 (0)