Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ To learn more about various SDK features, read:
- [Embedded Messages with Iterable's Android SDK](https://support.iterable.com/hc/articles/23061877893652)
- [Unknown User Activation: Developer Docs](https://support.iterable.com/hc/sections/40078809116180)

## Background Initialization (Recommended)

To prevent ANRs during app startup, use background initialization instead of the standard `initialize()` method:

```java
// In Application.onCreate()
IterableApi.initializeInBackground(this, "your-api-key", config, new AsyncInitializationCallback() {
@Override
public void onInitializationComplete() {
// SDK is ready
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can this method be useful for developer?
Is it necessary to implement something here? Like implement a flag to understand SDK's state?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah if they want to do something after initialization is done

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the sdk is deemed to be initialized at this point

}

@Override
public void onInitializationFailed(Exception e) {
// Handle initialization failure
}
});
```

This initializes the SDK on a background thread and automatically queues API calls until initialization completes, preventing ANRs and ensuring no data is lost.

## Sample projects

For sample code, take a look at:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.iterable.iterableapi;

import androidx.annotation.NonNull;

/**
* Callback interface for background initialization completion.
* All callbacks are executed on the main thread.
*/
public interface AsyncInitializationCallback {
/**
* Called on the main thread when initialization completes successfully.
* At this point, all queued operations have been processed and the SDK is ready for use.
*/
void onInitializationComplete();

/**
* Called on the main thread if initialization fails.
* Any queued operations will be cleared when initialization fails.
*
* @param exception The exception that caused initialization failure
*/
void onInitializationFailed(@NonNull Exception exception);
}
139 changes: 117 additions & 22 deletions iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ public class IterableApi {
static volatile IterableApi sharedInstance = new IterableApi();

private static final String TAG = "IterableApi";
private Context _applicationContext;
Context _applicationContext; // Package-private for background initializer access
IterableConfig config;
private String _apiKey;
String _apiKey; // Package-private for background initializer access
private String _email;
private String _userId;
String _userIdUnknown;
Expand All @@ -56,6 +56,16 @@ public class IterableApi {
private HashMap<String, String> deviceAttributes = new HashMap<>();
private IterableKeychain keychain;

//region Background Initialization - Delegated to IterableBackgroundInitializer
//---------------------------------------------------------------------------------------

/**
* Helper method to queue operations if initialization is in progress
*/
private void queueOrExecute(Runnable operation, String description) {
IterableBackgroundInitializer.queueOrExecute(operation, description);
}

void fetchRemoteConfiguration() {
apiClient.getRemoteConfiguration(new IterableHelper.IterableActionHandler() {
@Override
Expand Down Expand Up @@ -347,11 +357,20 @@ private void logoutPreviousUser() {
disablePush();
}

getInAppManager().reset();
getEmbeddedManager().reset();
getAuthManager().reset();
// Only reset managers if they're initialized
if (inAppManager != null) {
inAppManager.reset();
}
if (embeddedManager != null) {
embeddedManager.reset();
}
if (authManager != null) {
authManager.reset();
}

apiClient.onLogout();
if (apiClient != null) {
apiClient.onLogout();
}
}

private void onLogin(
Expand Down Expand Up @@ -726,6 +745,80 @@ public static void initialize(@NonNull Context context, @NonNull String apiKey,
}
}

/**
* Initialize the Iterable SDK in the background to avoid ANRs.
* This method returns immediately and performs all initialization work on a background thread.
* Any API calls made before initialization completes will be queued and executed after initialization.
*
* @param context Application context
* @param apiKey Iterable API key
* @param callback Optional callback for initialization completion (can be null)
*/
public static void initializeInBackground(@NonNull Context context,
@NonNull String apiKey,
@Nullable AsyncInitializationCallback callback) {
IterableBackgroundInitializer.initializeInBackground(context, apiKey, null, callback);
}

/**
* Initialize the Iterable SDK in the background to avoid ANRs.
* This method returns immediately and performs all initialization work on a background thread.
* Any API calls made before initialization completes will be queued and executed after initialization.
*
* @param context Application context
* @param apiKey Iterable API key
* @param config Optional configuration (can be null)
* @param callback Optional callback for initialization completion (can be null)
*/
public static void initializeInBackground(@NonNull Context context,
@NonNull String apiKey,
@Nullable IterableConfig config,
@Nullable AsyncInitializationCallback callback) {
IterableBackgroundInitializer.initializeInBackground(context, apiKey, config, callback);
}

/**
* Check if background initialization is in progress
* @return true if initialization is currently running in background
*/
public static boolean isInitializingInBackground() {
return IterableBackgroundInitializer.isInitializingInBackground();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What different states do we want to support?
Initializing, Initialized and Not initialized?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah if both of them return false then its not initalized

}

/**
* Check if background initialization has completed
* @return true if background initialization completed successfully
*/
public static boolean isBackgroundInitializationComplete() {
return IterableBackgroundInitializer.isBackgroundInitializationComplete();
}

/**
* Get the number of operations currently queued
* @return number of queued operations
*/
@VisibleForTesting
static int getQueuedOperationCount() {
return IterableBackgroundInitializer.getQueuedOperationCount();
}

/**
* Shutdown the background executor for proper cleanup
* Should be called during application shutdown or for testing
*/
@VisibleForTesting
static void shutdownBackgroundExecutor() {
IterableBackgroundInitializer.shutdownBackgroundExecutor();
}

/**
* Reset background initialization state - for testing only
*/
@VisibleForTesting
static void resetBackgroundInitializationState() {
IterableBackgroundInitializer.resetBackgroundInitializationState();
}

public static void setContext(Context context) {
IterableActivityMonitor.getInstance().registerLifecycleCallbacks(context);
}
Expand Down Expand Up @@ -807,7 +900,7 @@ public void pauseAuthRetries(boolean pauseRetry) {
}

public void setEmail(@Nullable String email) {
setEmail(email, null, null, null, null);
queueOrExecute(() -> setEmail(email, null, null, null, null), "setEmail(" + email + ")");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove the actual email from description

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix (1)

}

public void setEmail(@Nullable String email, IterableIdentityResolution identityResolution) {
Expand Down Expand Up @@ -876,7 +969,7 @@ public void setUnknownUser(@Nullable String userId) {
}

public void setUserId(@Nullable String userId) {
setUserId(userId, null, null, null, null, false);
queueOrExecute(() -> setUserId(userId, null, null, null, null, false), "setUserId(" + userId + ")");
}

public void setUserId(@Nullable String userId, IterableIdentityResolution identityResolution) {
Expand Down Expand Up @@ -1026,11 +1119,11 @@ public void removeDeviceAttribute(String key) {
* @param deviceToken Push token obtained from GCM or FCM
*/
public void registerDeviceToken(@NonNull String deviceToken) {
registerDeviceToken(_email, _userId, _authToken, getPushIntegrationName(), deviceToken, deviceAttributes);
queueOrExecute(() -> registerDeviceToken(_email, _userId, _authToken, getPushIntegrationName(), deviceToken, deviceAttributes), "registerDeviceToken");
}

public void trackPushOpen(int campaignId, int templateId, @NonNull String messageId) {
trackPushOpen(campaignId, templateId, messageId, null);
queueOrExecute(() -> trackPushOpen(campaignId, templateId, messageId, null), "trackPushOpen(" + campaignId + ", " + templateId + ", " + messageId + ")");
}

/**
Expand Down Expand Up @@ -1202,7 +1295,7 @@ public boolean isIterableIntent(@Nullable Intent intent) {
* @param eventName
*/
public void track(@NonNull String eventName) {
track(eventName, 0, 0, null);
queueOrExecute(() -> track(eventName, 0, 0, null), "track(" + eventName + ")");
}

/**
Expand All @@ -1211,7 +1304,7 @@ public void track(@NonNull String eventName) {
* @param dataFields
*/
public void track(@NonNull String eventName, @Nullable JSONObject dataFields) {
track(eventName, 0, 0, dataFields);
queueOrExecute(() -> track(eventName, 0, 0, dataFields), "track(" + eventName + ", dataFields)");
}

/**
Expand All @@ -1221,7 +1314,7 @@ public void track(@NonNull String eventName, @Nullable JSONObject dataFields) {
* @param templateId
*/
public void track(@NonNull String eventName, int campaignId, int templateId) {
track(eventName, campaignId, templateId, null);
queueOrExecute(() -> track(eventName, campaignId, templateId, null), "track(" + eventName + ", " + campaignId + ", " + templateId + ")");
}

/**
Expand All @@ -1248,14 +1341,16 @@ public void track(@NonNull String eventName, int campaignId, int templateId, @Nu
* @param items
*/
public void updateCart(@NonNull List<CommerceItem> items) {
if (!checkSDKInitialization() && _userIdUnknown == null) {
if (sharedInstance.config.enableUnknownUserActivation) {
unknownUserManager.trackUnknownUpdateCart(items);
queueOrExecute(() -> {
if (!checkSDKInitialization() && _userIdUnknown == null) {
if (sharedInstance.config.enableUnknownUserActivation) {
Comment on lines +1369 to +1370
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do configuration check need to get into queue as well? If so, we will have to make it consistent accross other method calls as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the whole method goes into the queue so we make sure everything is done after initialization

unknownUserManager.trackUnknownUpdateCart(items);
}
return;
}
return;
}

apiClient.updateCart(items);
apiClient.updateCart(items);
}, "updateCart(" + items.size() + " items)");
}

/**
Expand All @@ -1264,7 +1359,7 @@ public void updateCart(@NonNull List<CommerceItem> items) {
* @param items list of purchased items
*/
public void trackPurchase(double total, @NonNull List<CommerceItem> items) {
trackPurchase(total, items, null, null);
queueOrExecute(() -> trackPurchase(total, items, null, null), "trackPurchase(" + total + ", " + items.size() + " items)");
}

/**
Expand All @@ -1274,7 +1369,7 @@ public void trackPurchase(double total, @NonNull List<CommerceItem> items) {
* @param dataFields a `JSONObject` containing any additional information to save along with the event
*/
public void trackPurchase(double total, @NonNull List<CommerceItem> items, @Nullable JSONObject dataFields) {
trackPurchase(total, items, dataFields, null);
queueOrExecute(() -> trackPurchase(total, items, dataFields, null), "trackPurchase(" + total + ", " + items.size() + " items, dataFields)");
}


Expand Down Expand Up @@ -1302,7 +1397,7 @@ public void trackPurchase(double total, @NonNull List<CommerceItem> items, @Null
* @param newEmail New email
*/
public void updateEmail(final @NonNull String newEmail) {
updateEmail(newEmail, null, null, null);
queueOrExecute(() -> updateEmail(newEmail, null, null, null), "updateEmail(" + newEmail + ")");
}

public void updateEmail(final @NonNull String newEmail, final @NonNull String authToken) {
Expand Down
Loading
Loading