Skip to content

Commit 419a140

Browse files
[SDK-707] Replace request AsyncTask with SDK executor
1 parent 9673361 commit 419a140

34 files changed

Lines changed: 1038 additions & 283 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
99
- Added `IterableConfig.Builder.setInAppColorScheme()` and `setInAppColorSchemeProvider()` to control the color scheme reported to HTML in-app messages. The provider is evaluated for each new in-app message, allowing apps that keep their theme in Jetpack Compose state to return the current `LIGHT` or `DARK` scheme.
1010

1111
### Fixed
12-
- Push token registration, disable operations, and Iterable deep-link redirects now run on an SDK-owned serial executor instead of Android's process-wide `AsyncTask` queue, preventing host app background work from delaying SDK operations while preserving operation order. Deep-link callbacks and attribution updates continue on the main thread.
12+
- Push token registration, disable operations, Iterable deep-link redirects, and API requests now run on an SDK-owned serial executor instead of Android's process-wide `AsyncTask` queues, preventing host app background work from delaying SDK operations while preserving operation order. Deep-link callbacks, attribution updates, and API response callbacks continue on the main thread.
1313
- HTML in-app messages now follow the host activity's Android light-dark configuration instead of always rendering as if the device were in dark mode. Previously the in-app container always reported `prefers-color-scheme: dark` to the message HTML, so a campaign with an `@media (prefers-color-scheme: dark)` block rendered its dark styles even in light mode. Those campaigns now render their light styles in light mode; campaigns that don't declare dark styles, and rendering in dark mode, are unaffected.
1414
- Fixed the keychain treating a transient crypto timeout as a permanent decryption failure. A slow AndroidKeyStore operation that exceeded the 500 ms timeout would wipe the stored email, userId, and auth token and disable encryption, forcing the user to re-authenticate (and request a new auth token) on the next launch. Crypto timeouts are now handled as transient without wiping credentials or disabling encryption for the device: a read that times out returns no value for that call (the stored ciphertext is left intact for the next attempt), and a write that times out stores that one value unencrypted (as the non-encrypted fallback already did) rather than clearing everything. The timed-out crypto operation is also cancelled so it no longer blocks subsequent reads/writes.
1515
- `setExpiringAuthTokenRefreshPeriod` now validates its input instead of silently producing a broken refresh schedule. Previously a negative value was converted to a negative millisecond period and then *subtracted* when computing the refresh time, scheduling the refresh after the token had already expired; a very large value overflowed to a negative period with the same effect; and `null` threw a `NullPointerException` on unboxing. Invalid values (`null`, `NaN`, negatives) are now logged and ignored, leaving the period at whatever it was before the call — the 60 second default unless an earlier call set something else. Values above ~10 years are clamped to that ceiling rather than ignored. Zero remains valid and means the token is refreshed only once it has expired.

‎iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,10 @@ static void initializeForPush(@Nullable Context context) {
986986
this.embeddedManager = embeddedManager;
987987
this.pushRegistration = Objects.requireNonNull(pushRegistration);
988988
}
989+
void setRequestDispatcher(IterableRequestDispatcher requestDispatcher) {
990+
apiClient = new IterableApiClient(new IterableApiAuthProvider(),
991+
Objects.requireNonNull(requestDispatcher));
992+
}
989993

990994
//endregion
991995

‎iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
class IterableApiClient {
2121
private static final String TAG = "IterableApiClient";
2222
private final @NonNull AuthProvider authProvider;
23+
private final @NonNull IterableRequestDispatcher requestDispatcher;
2324
private RequestProcessor requestProcessor;
2425

2526
interface AuthProvider {
@@ -41,12 +42,20 @@ interface AuthProvider {
4142
}
4243

4344
IterableApiClient(@NonNull AuthProvider authProvider) {
45+
this(authProvider, IterableRequestDispatcher.sdk());
46+
}
47+
48+
IterableApiClient(
49+
@NonNull AuthProvider authProvider,
50+
@NonNull IterableRequestDispatcher requestDispatcher
51+
) {
4452
this.authProvider = authProvider;
53+
this.requestDispatcher = requestDispatcher;
4554
}
4655

4756
private RequestProcessor getRequestProcessor() {
4857
if (requestProcessor == null) {
49-
requestProcessor = new OnlineRequestProcessor();
58+
requestProcessor = new OnlineRequestProcessor(requestDispatcher);
5059
}
5160
return requestProcessor;
5261
}
@@ -64,8 +73,8 @@ void setOfflineProcessingEnabled(boolean offlineMode) {
6473
}
6574

6675
this.requestProcessor = offlineMode
67-
? new OfflineRequestProcessor(authProvider.getContext())
68-
: new OnlineRequestProcessor();
76+
? new OfflineRequestProcessor(authProvider.getContext(), requestDispatcher)
77+
: new OnlineRequestProcessor(requestDispatcher);
6978
}
7079

7180
void getRemoteConfiguration(IterableHelper.IterableActionHandler actionHandler) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.iterable.iterableapi;
2+
3+
import android.os.Handler;
4+
import android.os.Looper;
5+
6+
import java.util.concurrent.Executor;
7+
8+
final class IterableRequestDispatcher {
9+
interface RetryScheduler {
10+
void schedule(Runnable runnable, long delayMs);
11+
}
12+
13+
private static final IterableRequestDispatcher SDK_DISPATCHER =
14+
new IterableRequestDispatcher(
15+
IterableExecutors.serial(),
16+
IterableExecutors.main(),
17+
(runnable, delayMs) ->
18+
new Handler(Looper.getMainLooper()).postDelayed(runnable, delayMs)
19+
);
20+
21+
private final Executor requestExecutor;
22+
private final Executor callbackExecutor;
23+
private final RetryScheduler retryScheduler;
24+
25+
static IterableRequestDispatcher sdk() {
26+
return SDK_DISPATCHER;
27+
}
28+
29+
IterableRequestDispatcher(
30+
Executor requestExecutor,
31+
Executor callbackExecutor,
32+
RetryScheduler retryScheduler
33+
) {
34+
this.requestExecutor = requestExecutor;
35+
this.callbackExecutor = callbackExecutor;
36+
this.retryScheduler = retryScheduler;
37+
}
38+
39+
void execute(IterableApiRequest request) {
40+
execute(request, 0);
41+
}
42+
43+
void execute(IterableApiRequest request, int retryCount) {
44+
requestExecutor.execute(new IterableRequestTask(request, retryCount, this));
45+
}
46+
47+
void deliverResult(Runnable runnable) {
48+
callbackExecutor.execute(runnable);
49+
}
50+
51+
void retry(IterableApiRequest request, int retryCount, long delayMs) {
52+
retryScheduler.schedule(() -> execute(request, retryCount), delayMs);
53+
}
54+
}

‎iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java‎

Lines changed: 56 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
import static com.iterable.iterableapi.IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION;
55

66
import android.net.Uri;
7-
import android.os.AsyncTask;
8-
import android.os.Handler;
9-
import android.os.Looper;
107

118
import androidx.annotation.NonNull;
129
import androidx.annotation.Nullable;
@@ -29,10 +26,10 @@
2926
import java.util.Objects;
3027

3128
/**
32-
* Async task to handle sending data to the Iterable server
29+
* Runnable task to handle sending data to the Iterable server
3330
* Created by David Truong dt@iterable.com
3431
*/
35-
class IterableRequestTask extends AsyncTask<IterableApiRequest, Void, IterableApiResponse> {
32+
class IterableRequestTask implements Runnable {
3633
static final String TAG = "IterableRequest";
3734

3835
static String overrideUrl;
@@ -45,37 +42,56 @@ class IterableRequestTask extends AsyncTask<IterableApiRequest, Void, IterableAp
4542
static final String ERROR_CODE_INVALID_JWT_PAYLOAD = "InvalidJwtPayload";
4643
static final String ERROR_CODE_MISSING_JWT_PAYLOAD = "BadAuthorizationHeader";
4744
static final String ERROR_CODE_JWT_USER_IDENTIFIERS_MISMATCHED = "JwtUserIdentifiersMismatched";
48-
int retryCount = 0;
49-
IterableApiRequest iterableApiRequest;
45+
private final int retryCount;
46+
private final IterableApiRequest iterableApiRequest;
47+
private final IterableRequestDispatcher requestDispatcher;
48+
49+
IterableRequestTask(
50+
IterableApiRequest iterableApiRequest,
51+
int retryCount,
52+
IterableRequestDispatcher requestDispatcher
53+
) {
54+
this.iterableApiRequest = iterableApiRequest;
55+
this.retryCount = retryCount;
56+
this.requestDispatcher = requestDispatcher;
57+
}
5058

51-
/**
52-
* Sends the given request to Iterable using a HttpUserConnection
53-
* Reference - http://developer.android.com/reference/java/net/HttpURLConnection.html
54-
*
55-
* @param params
56-
* @return
57-
*/
58-
protected IterableApiResponse doInBackground(IterableApiRequest... params) {
59-
if (params != null && params.length > 0) {
60-
iterableApiRequest = params[0];
61-
}
62-
return executeApiRequest(iterableApiRequest);
59+
@Override
60+
public void run() {
61+
IterableApiResponse response = executeApiRequest(iterableApiRequest, requestDispatcher);
62+
requestDispatcher.deliverResult(() -> handleResponse(response));
6363
}
6464

65-
private static void retryRequestWithNewAuthToken(String newAuthToken, IterableApiRequest iterableApiRequest) {
65+
static void retryRequestWithNewAuthToken(
66+
String newAuthToken,
67+
IterableApiRequest iterableApiRequest,
68+
IterableRequestDispatcher requestDispatcher
69+
) {
6670
IterableApiRequest request = new IterableApiRequest(
6771
iterableApiRequest.apiKey,
6872
iterableApiRequest.resourcePath,
6973
iterableApiRequest.json,
7074
iterableApiRequest.requestType,
7175
newAuthToken,
72-
iterableApiRequest.legacyCallback);
73-
IterableRequestTask requestTask = new IterableRequestTask();
74-
requestTask.execute(request);
76+
iterableApiRequest.legacyCallback
77+
);
78+
requestDispatcher.execute(request);
7579
}
7680

7781
@WorkerThread
7882
static IterableApiResponse executeApiRequest(IterableApiRequest iterableApiRequest) {
83+
return executeApiRequest(iterableApiRequest, IterableRequestDispatcher.sdk());
84+
}
85+
86+
/**
87+
* Sends the given request to Iterable using a HttpURLConnection.
88+
* Reference - http://developer.android.com/reference/java/net/HttpURLConnection.html
89+
*/
90+
@WorkerThread
91+
private static IterableApiResponse executeApiRequest(
92+
IterableApiRequest iterableApiRequest,
93+
IterableRequestDispatcher requestDispatcher
94+
) {
7995
IterableApiResponse apiResponse = null;
8096
String requestResult = null;
8197

@@ -204,7 +220,7 @@ static IterableApiResponse executeApiRequest(IterableApiRequest iterableApiReque
204220
apiResponse = IterableApiResponse.failure(responseCode, requestResult, jsonResponse, "JWT Authorization header error");
205221
IterableApi.getInstance().getAuthManager().handleAuthFailure(iterableApiRequest.authToken, getMappedErrorCodeForMessage(jsonResponse));
206222

207-
handleJwtAuthRetry(iterableApiRequest);
223+
handleJwtAuthRetry(iterableApiRequest, requestDispatcher);
208224
} else {
209225
apiResponse = IterableApiResponse.failure(responseCode, requestResult, jsonResponse, "Invalid API Key");
210226
}
@@ -264,7 +280,10 @@ static IterableApiResponse executeApiRequest(IterableApiRequest iterableApiReque
264280
* is obtained via the AuthTokenReadyListener callback.
265281
* For online requests or when autoRetry is disabled, use the existing inline retry.
266282
*/
267-
private static void handleJwtAuthRetry(IterableApiRequest iterableApiRequest) {
283+
private static void handleJwtAuthRetry(
284+
IterableApiRequest iterableApiRequest,
285+
IterableRequestDispatcher requestDispatcher
286+
) {
268287
boolean autoRetry = IterableApi.getInstance().isAutoRetryOnJwtFailure();
269288
if (autoRetry && iterableApiRequest.getProcessorType() == IterableApiRequest.ProcessorType.OFFLINE) {
270289
IterableAuthManager authManager = IterableApi.getInstance().getAuthManager();
@@ -276,7 +295,7 @@ private static void handleJwtAuthRetry(IterableApiRequest iterableApiRequest) {
276295
null
277296
);
278297
} else {
279-
requestNewAuthTokenAndRetry(iterableApiRequest);
298+
requestNewAuthTokenAndRetry(iterableApiRequest, requestDispatcher);
280299
}
281300
}
282301

@@ -363,11 +382,7 @@ private static boolean isSensitive(String key) {
363382
return (key.equals(IterableConstants.HEADER_API_KEY)) || key.equals(IterableConstants.HEADER_SDK_AUTHORIZATION);
364383
}
365384

366-
private static final Handler handler = new Handler(Looper.getMainLooper());
367-
368-
@Override
369-
protected void onPostExecute(IterableApiResponse response) {
370-
385+
void handleResponse(IterableApiResponse response) {
371386
if (shouldRetry(response)) {
372387
retryRequestWithDelay();
373388
return;
@@ -380,25 +395,15 @@ protected void onPostExecute(IterableApiResponse response) {
380395
if (iterableApiRequest.legacyCallback != null) {
381396
iterableApiRequest.legacyCallback.execute(response.responseBody);
382397
}
383-
super.onPostExecute(response);
384398
}
385399

386400
private boolean shouldRetry(IterableApiResponse response) {
387401
return !response.success && response.responseCode >= 500 && retryCount <= MAX_RETRY_COUNT;
388402
}
389403

390404
private void retryRequestWithDelay() {
391-
final IterableRequestTask requestTask = new IterableRequestTask();
392-
requestTask.setRetryCount(retryCount + 1);
393-
394405
long delay = (retryCount > 2) ? RETRY_DELAY_MS * retryCount : 0;
395-
396-
handler.postDelayed(new Runnable() {
397-
@Override
398-
public void run() {
399-
requestTask.execute(iterableApiRequest);
400-
}
401-
}, delay);
406+
requestDispatcher.retry(iterableApiRequest, retryCount + 1, delay);
402407
}
403408

404409
private void handleSuccessResponse(IterableApiResponse response) {
@@ -425,7 +430,10 @@ private void handleErrorResponse(IterableApiResponse response) {
425430
}
426431
}
427432

428-
private static void requestNewAuthTokenAndRetry(IterableApiRequest iterableApiRequest) {
433+
private static void requestNewAuthTokenAndRetry(
434+
IterableApiRequest iterableApiRequest,
435+
IterableRequestDispatcher requestDispatcher
436+
) {
429437
IterableApi.getInstance().getAuthManager().setIsLastAuthTokenValid(false);
430438
long retryInterval = IterableApi.getInstance().getAuthManager().getNextRetryInterval();
431439
IterableApi.getInstance().getAuthManager().scheduleAuthTokenRefresh(
@@ -434,17 +442,17 @@ private static void requestNewAuthTokenAndRetry(IterableApiRequest iterableApiRe
434442
data -> {
435443
try {
436444
String newAuthToken = data.getString("newAuthToken");
437-
retryRequestWithNewAuthToken(newAuthToken, iterableApiRequest);
445+
retryRequestWithNewAuthToken(
446+
newAuthToken,
447+
iterableApiRequest,
448+
requestDispatcher
449+
);
438450
} catch (JSONException e) {
439451
e.printStackTrace();
440452
}
441453
}
442454
);
443455
}
444-
445-
protected void setRetryCount(int count) {
446-
retryCount = count;
447-
}
448456
}
449457

450458
/**

0 commit comments

Comments
 (0)