Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

### Fixed
- `IterableInAppManager.showMessage` is now safe to call from any thread. Called off the main thread, it posts the display to the main thread instead of running it on the caller thread. Since 3.9.0, a host activity that is not a `FragmentActivity` (for example a Jetpack Compose `ComponentActivity`) uses the Dialog renderer, which crashed with `IllegalStateException: Method addObserver must be called on the main thread` when `showMessage` was called from a background thread, including the React Native bridge thread. Calls from the main thread still display synchronously. Called from another thread, the message's read and consumed state are updated after `showMessage` returns. Like iOS, display always happens on the main thread.
- Push token registration, disable operations, Iterable deep-link redirects, and API requests now use dedicated SDK-owned executors instead of Android's process-wide `AsyncTask` queues. Push and deep-link work retain isolated serial lanes. Ordinary online API requests remain concurrent, while their automatic retries and offline immediate requests retain serial ordering; stored offline requests retain their own FIFO flow. Slow work in one flow no longer delays unrelated SDK operations, and client callbacks and attribution updates continue on the main thread.

## [3.11.0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ void reset() {
}

/**
* Display the in-app message on the screen
* Display the in-app message on the screen. Safe to call from any thread; the message is always
* displayed on the main thread.
* @param message In-App message object retrieved from {@link IterableInAppManager#getMessages()}
*/
public void showMessage(@NonNull IterableInAppMessage message) {
Expand All @@ -255,6 +256,7 @@ public void showMessage(@NonNull IterableInAppMessage message, @NonNull Iterable
/**
* Display the in-app message on the screen. This method, by default, assumes the current location of activity as InApp. To pass
* different inAppLocation as paramter, use showMessage method which takes in IterableAppLocation as a parameter.
* Safe to call from any thread; the message is always displayed on the main thread.
* @param message In-App message object retrieved from {@link IterableInAppManager#getMessages()}
* @param consume A boolean indicating whether to remove the message from the list after showing
* @param clickCallback A callback that is called when the user clicks on a link in the in-app message
Expand All @@ -263,7 +265,28 @@ public void showMessage(final @NonNull IterableInAppMessage message, boolean con
showMessage(message, consume, clickCallback, IterableInAppLocation.IN_APP);
}

public void showMessage(final @NonNull IterableInAppMessage message, boolean consume, final @Nullable IterableHelper.IterableUrlCallback clickCallback, @NonNull IterableInAppLocation inAppLocation) {
/**
* Displays an in-app message on the main thread.
*
* <p><b>Threading:</b> Main-thread calls are synchronous. Other calls are queued, so the
* message's read/consumed state changes after this method returns. The callback runs on main.
*
* @param message message to display
* @param consume whether to consume the message after display
* @param clickCallback callback for clicked links, or {@code null}
* @param inAppLocation where the message is displayed from
*/
public void showMessage(final @NonNull IterableInAppMessage message, final boolean consume, final @Nullable IterableHelper.IterableUrlCallback clickCallback, final @NonNull IterableInAppLocation inAppLocation) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

~L252–254 and ~L268: Threading Javadoc is missing on two public overloads

What the spec says: SDK-791 acceptance criteria: β€œJavadoc on showMessage states the threading behaviour.” The work item names the 4-argument overload as the method that changes.

Why it conflicts: Callers of showMessage(message, location) and showMessage(message, consume, clickCallback, location) do not see the new contract on the method they invoke. The documented overloads do state it, so the checkbox is met for those entry points.

Suggested action: Add the same threading sentence to the location overload and the 4-argument overload.

// Displaying creates dialogs and registers lifecycle observers, which must happen on the main thread.
if (Looper.myLooper() != Looper.getMainLooper()) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
showMessage(message, consume, clickCallback, inAppLocation);
}
});
return;
}
if (displayer.showMessage(message, inAppLocation, new IterableHelper.IterableUrlCallback() {
@Override
public void execute(Uri url) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.iterable.iterableapi;

import static android.os.Looper.getMainLooper;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.robolectric.Shadows.shadowOf;

import android.os.Handler;
import android.os.HandlerThread;

import androidx.activity.ComponentActivity;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.robolectric.Robolectric;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.shadows.ShadowDialog;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class IterableInAppManagerThreadingTest extends BaseTest {

private ActivityController<ComponentActivity> controller;
private IterableInAppManager inAppManager;
private IterableInAppMessage message;

@Before
public void setUp() throws Exception {
IterableTestUtils.createIterableApiNew();
controller = Robolectric.buildActivity(ComponentActivity.class).create().start().resume();
shadowOf(getMainLooper()).idle();
inAppManager = IterableApi.getInstance().getInAppManager();
message = InAppTestUtils.getTestInAppMessage();
}

@After
public void tearDown() {
if (ShadowDialog.getLatestDialog() != null) {
ShadowDialog.getLatestDialog().dismiss();
}
if (controller != null) {
controller.pause().stop().destroy();
}
IterableTestUtils.resetIterableApi();
}

@Test
public void showMessage_fromBackgroundLooperThread_displaysOnMainThreadWithoutCrashing() throws InterruptedException {
// A looper thread, like the React Native native-modules thread.
HandlerThread background = new HandlerThread("background");
background.start();
final AtomicReference<Throwable> thrown = new AtomicReference<>();
final CountDownLatch done = new CountDownLatch(1);
new Handler(background.getLooper()).post(new Runnable() {
@Override
public void run() {
try {
inAppManager.showMessage(message, true, null);
} catch (Throwable t) {
thrown.set(t);
} finally {
done.countDown();
}
}
});
assertTrue(done.await(5, TimeUnit.SECONDS));
background.quitSafely();

assertNull(thrown.get());
assertNull(IterableInAppDialogNotification.getInstance());
assertFalse(message.isRead());
assertFalse(message.isMarkedForDeletion());

shadowOf(getMainLooper()).idle();

assertNotNull(IterableInAppDialogNotification.getInstance());
assertTrue(message.isRead());
assertTrue(message.isMarkedForDeletion());
}

@Test
public void showMessage_fromMainThread_displaysSynchronously() {
inAppManager.showMessage(message, true, null);

assertNotNull(IterableInAppDialogNotification.getInstance());
assertTrue(message.isRead());
assertTrue(message.isMarkedForDeletion());
}
}
Loading