Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Notification small-icon resolution now falls back through standard conventions — the Firebase `com.google.firebase.messaging.default_notification_icon` meta-data, `@drawable/notification_icon` (Expo / React Native), and `@drawable/ic_notification` — before defaulting to the app launcher icon. This fixes white-square notification icons on Android 5.0+ for apps that configure their icon through these conventions but don't set `iterable_notification_icon`.
- Added a `DEFER` response to `IterableInAppHandler.InAppResponse`, returned from `onNewInApp(message)`. Unlike `SKIP` (which permanently drops the message), `DEFER` leaves the message pending so the SDK reconsiders it on a later display pass (next foreground, sync, or newly arrived message). Use it for temporary, per-message suppression — for example while a splash screen is showing. Existing handlers returning `SHOW`/`SKIP` are unaffected.
- Added `IterableInAppManager.resumeInAppDisplay()` so apps can prompt the SDK to re-evaluate pending in-app messages once they become ready to display (e.g. after a splash screen is dismissed), without waiting for the next foreground/sync trigger. This is independent of `setAutoDisplayPaused(boolean)`: if auto display is paused, `resumeInAppDisplay()` will not show anything (and logs a warning) until you also call `setAutoDisplayPaused(false)`.
- Added `IterableApi.getInAppManagerOrNull()` and `IterableApi.getEmbeddedManagerOrNull()`, which return `null` when the SDK has not been initialized instead of a no-op manager. Use these when you need to detect the uninitialized state explicitly.

### Changed
- `IterableApi.getInAppManager()` and `IterableApi.getEmbeddedManager()` no longer throw a `RuntimeException` when called before `IterableApi.initialize()`. They now log an error and return a no-op manager (empty lists, ignored commands). To detect the uninitialized state explicitly, use the new `getInAppManagerOrNull()` / `getEmbeddedManagerOrNull()`.

### Migration guide
**No action required.** Existing `IterableInAppHandler` implementations returning `SHOW`/`SKIP` are unaffected.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.iterable.iterableapi

/**
* No-op [IterableEmbeddedManager] returned by [IterableApi.getEmbeddedManager] when the SDK has not
* been initialized. Every method is a benign no-op so callers never crash for a pre-initialization
* usage error. Use [IterableApi.getEmbeddedManagerOrNull] to detect this state explicitly.
*/
internal class EmptyEmbeddedManager : IterableEmbeddedManager() {

Comment thread
rtlsilva marked this conversation as resolved.
private val emptySessionManager = EmbeddedSessionManager()

private fun logNotInitialized(method: String) {
IterableLogger.e(LOG_TAG, "$method called before IterableApi was initialized; no-op. " +
"Call IterableApi.initialize() in Application#onCreate.")
}

override fun addUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
logNotInitialized("addUpdateListener()")
}

override fun removeUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
logNotInitialized("removeUpdateListener()")
}

override fun getUpdateHandlers(): List<IterableEmbeddedUpdateHandler> {
logNotInitialized("getUpdateHandlers()")
return emptyList()
}

override fun getEmbeddedSessionManager(): EmbeddedSessionManager {
logNotInitialized("getEmbeddedSessionManager()")
return emptySessionManager
}

override fun getMessages(placementId: Long): List<IterableEmbeddedMessage>? {
logNotInitialized("getMessages()")
return null
}

override fun reset() {
logNotInitialized("reset()")
}

override fun getPlacementIds(): List<Long> {
logNotInitialized("getPlacementIds()")
return emptyList()
}

override fun syncMessages(placementIds: Array<Long>) {
logNotInitialized("syncMessages()")
}

override fun handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String?) {
logNotInitialized("handleEmbeddedClick()")
}

override fun onSwitchToForeground() {
}

override fun onSwitchToBackground() {
}

private companion object {
private const val LOG_TAG = "EmptyEmbeddedManager"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.iterable.iterableapi;

import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.Collections;
import java.util.List;

/**
* No-op {@link IterableInAppManager} returned by {@link IterableApi#getInAppManager()} when the SDK
* has not been initialized. Every method is a benign no-op so callers never crash for a
* pre-initialization usage error. Use {@link IterableApi#getInAppManagerOrNull()} to detect this
* state explicitly.
*/
class EmptyInAppManager extends IterableInAppManager {
private static final String TAG = "EmptyInAppManager";

EmptyInAppManager() {
super();
}

private void logNotInitialized(String method) {
IterableLogger.e(TAG, method + " called before IterableApi was initialized; no-op. " +
"Call IterableApi.initialize() in Application#onCreate.");
}

@NonNull
@Override
public synchronized List<IterableInAppMessage> getMessages() {
logNotInitialized("getMessages()");
return Collections.emptyList();
}

@Override
synchronized IterableInAppMessage getMessageById(String messageId) {
logNotInitialized("getMessageById()");
return null;
}

@NonNull
@Override
public synchronized List<IterableInAppMessage> getInboxMessages() {
logNotInitialized("getInboxMessages()");
return Collections.emptyList();
}

@Override
public synchronized int getUnreadInboxMessagesCount() {
logNotInitialized("getUnreadInboxMessagesCount()");
return 0;
}

@Override
public synchronized void setRead(@NonNull IterableInAppMessage message, boolean read) {
logNotInitialized("setRead()");
}

@Override
public synchronized void setRead(@NonNull IterableInAppMessage message, boolean read, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
logNotInitialized("setRead()");
if (failureHandler != null) {
failureHandler.onFailure("Iterable SDK is not initialized", null);
}
}

@Override
public void setAutoDisplayPaused(boolean paused) {
logNotInitialized("setAutoDisplayPaused()");
}

@Override
public void resumeInAppDisplay() {
logNotInitialized("resumeInAppDisplay()");
}

@Override
void syncInApp() {
logNotInitialized("syncInApp()");
}

@Override
void reset() {
logNotInitialized("reset()");
}

@Override
public void showMessage(@NonNull IterableInAppMessage message) {
logNotInitialized("showMessage()");
}

@Override
public void showMessage(@NonNull IterableInAppMessage message, @NonNull IterableInAppLocation location) {
logNotInitialized("showMessage()");
}

@Override
public void showMessage(final @NonNull IterableInAppMessage message, boolean consume, final @Nullable IterableHelper.IterableUrlCallback clickCallback) {
logNotInitialized("showMessage()");
}

@Override
public void showMessage(final @NonNull IterableInAppMessage message, boolean consume, final @Nullable IterableHelper.IterableUrlCallback clickCallback, @NonNull IterableInAppLocation inAppLocation) {
logNotInitialized("showMessage()");
}

@Override
public synchronized void removeMessage(@NonNull IterableInAppMessage message) {
logNotInitialized("removeMessage()");
}

@Override
public synchronized void removeMessage(@NonNull IterableInAppMessage message, @NonNull IterableInAppDeleteActionType source, @NonNull IterableInAppLocation clickLocation) {
logNotInitialized("removeMessage()");
}

@Override
public synchronized void removeMessage(@NonNull IterableInAppMessage message, @Nullable IterableInAppDeleteActionType source, @Nullable IterableInAppLocation clickLocation, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
logNotInitialized("removeMessage()");
if (failureHandler != null) {
failureHandler.onFailure("Iterable SDK is not initialized", null);
}
}

@Override
synchronized void removeMessage(String messageId) {
logNotInitialized("removeMessage()");
}

@Override
public void handleInAppClick(@NonNull IterableInAppMessage message, @Nullable Uri url) {
logNotInitialized("handleInAppClick()");
}

@Override
public void onSwitchToForeground() {
}

@Override
public void onSwitchToBackground() {
}

@Override
public void addListener(@NonNull Listener listener) {
logNotInitialized("addListener()");
}

@Override
public void removeListener(@NonNull Listener listener) {
logNotInitialized("removeListener()");
}

@Override
public void notifyOnChange() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public class IterableApi {
private @Nullable UnknownUserManager unknownUserManager;
private @Nullable IterableInAppManager inAppManager;
private @Nullable IterableEmbeddedManager embeddedManager;
private final IterableInAppManager emptyInAppManager = new EmptyInAppManager();
private final IterableEmbeddedManager emptyEmbeddedManager = new EmptyEmbeddedManager();
private String inboxSessionId;
private IterableAuthManager authManager;
private ConcurrentHashMap<String, String> deviceAttributes = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -994,28 +996,45 @@ static void initializeForPush(@Nullable Context context) {

//region SDK public functions
/**
* Returns an {@link IterableInAppManager} that can be used to manage in-app messages.
* Make sure the Iterable API is initialized before calling this method.
* @return {@link IterableInAppManager} instance
* Returns an {@link IterableInAppManager} for managing in-app messages. If the SDK is not initialized,
* logs an error and returns a no-op manager instead of throwing; use {@link #getInAppManagerOrNull()} to detect that.
*/
@NonNull
public IterableInAppManager getInAppManager() {
if (inAppManager == null) {
throw new RuntimeException("IterableApi must be initialized before calling getInAppManager(). " +
"Make sure you call IterableApi#initialize() in Application#onCreate");
IterableLogger.e(TAG, "getInAppManager() called before IterableApi was initialized; " +
"returning a no-op manager. Call IterableApi.initialize() in Application#onCreate.");
return emptyInAppManager;
}
return inAppManager;
}

/** Like {@link #getInAppManager()} but returns {@code null} (never a no-op instance) if not initialized. */
@Nullable
public IterableInAppManager getInAppManagerOrNull() {
return inAppManager;
}

/**
* Returns an {@link IterableEmbeddedManager} for managing embedded messages. If the SDK is not initialized,
* logs an error and returns a no-op manager instead of throwing; use {@link #getEmbeddedManagerOrNull()} to detect that.
*/
@NonNull
public IterableEmbeddedManager getEmbeddedManager() {
if (embeddedManager == null) {
throw new RuntimeException("IterableApi must be initialized before calling getEmbeddedManager(). " +
"Make sure you call IterableApi#initialize() in Application#onCreate");
IterableLogger.e(TAG, "getEmbeddedManager() called before IterableApi was initialized; " +
"returning a no-op manager. Call IterableApi.initialize() in Application#onCreate.");
return emptyEmbeddedManager;
}
return embeddedManager;
}

/** Like {@link #getEmbeddedManager()} but returns {@code null} (never a no-op instance) if not initialized. */
@Nullable
public IterableEmbeddedManager getEmbeddedManagerOrNull() {
return embeddedManager;
}

/**
* Returns the attribution information ({@link IterableAttributionInfo}) for last push open
* or app link click from an email.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.content.Context
import org.json.JSONException
import org.json.JSONObject

public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback {
public open class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback {

// region constants
val TAG = "IterableEmbeddedManager"
Expand All @@ -15,8 +15,8 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
private var localPlacementIds = mutableListOf<Long>()

private var updateHandleListeners = mutableListOf<IterableEmbeddedUpdateHandler>()
private var iterableApi: IterableApi
private var context: Context
private lateinit var iterableApi: IterableApi
private lateinit var context: Context

private var embeddedSessionManager = EmbeddedSessionManager()

Expand All @@ -38,27 +38,32 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
}
}

// No-op constructor for the pre-initialization stub (see EmptyEmbeddedManager). Leaves
// iterableApi/context uninitialized; EmptyEmbeddedManager overrides every method that
// would otherwise touch them.
internal constructor()

// endregion

// region getters and setters

//Add updateHandler to the list
public fun addUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
public open fun addUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
updateHandleListeners.add(updateHandler)
}

//Remove updateHandler from the list
public fun removeUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
public open fun removeUpdateListener(updateHandler: IterableEmbeddedUpdateHandler) {
updateHandleListeners.remove(updateHandler)
embeddedSessionManager.endSession()
}

//Get the list of updateHandlers
public fun getUpdateHandlers(): List<IterableEmbeddedUpdateHandler> {
public open fun getUpdateHandlers(): List<IterableEmbeddedUpdateHandler> {
return updateHandleListeners
}

public fun getEmbeddedSessionManager(): EmbeddedSessionManager {
public open fun getEmbeddedSessionManager(): EmbeddedSessionManager {
return embeddedSessionManager
}

Expand All @@ -67,20 +72,20 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
// region public methods

//Gets the list of embedded messages in memory without syncing
fun getMessages(placementId: Long): List<IterableEmbeddedMessage>? {
open fun getMessages(placementId: Long): List<IterableEmbeddedMessage>? {
return localPlacementMessagesMap[placementId]
}

fun reset() {
open fun reset() {
localPlacementMessagesMap = mutableMapOf()
}

fun getPlacementIds(): List<Long> {
open fun getPlacementIds(): List<Long> {
return localPlacementIds
}

@JvmOverloads
fun syncMessages(placementIds: Array<Long> = emptyArray()) {
open fun syncMessages(placementIds: Array<Long> = emptyArray()) {
if (iterableApi.config.enableEmbeddedMessaging) {
IterableLogger.v(TAG, "Syncing messages...")

Expand Down Expand Up @@ -159,7 +164,7 @@ public class IterableEmbeddedManager : IterableActivityMonitor.AppStateCallback
}
}

fun handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String?) {
open fun handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String?) {
if ((clickedUrl != null) && clickedUrl.toString().isNotEmpty()) {
if (clickedUrl.startsWith(IterableConstants.URL_SCHEME_ACTION)) {
// This is an action:// URL, pass that to the custom action handler
Expand Down
Loading
Loading