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
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,38 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
## [Unreleased]
### Added
- New `IterableInAppDisplayMode` enum to control how in-app messages interact with system bars. Configure via `IterableConfig.Builder.setInAppDisplayMode()`:
- `FOLLOW_APP_LAYOUT` (default) — matches the host app's system bar configuration. No change needed for existing integrations.
- `FORCE_EDGE_TO_EDGE` — forces in-app content to draw behind system bars with transparent status and navigation bars.
- `FORCE_FULLSCREEN` — hides the status bar entirely for all in-app messages.
- `FORCE_RESPECT_BOUNDS` — ensures in-app content never overlaps system bars, keeping UI elements like the close button always accessible.

### Changed
- Replaced the deprecated `AsyncTask`-based push notification handling with `WorkManager` for improved reliability and compatibility with modern Android versions. No action is required.
- In-app messages now match the host app's system bar behavior by default. Previously, fullscreen in-apps would always draw content behind the status bar, which could cause UI elements like the close button to be obscured. The new default (`FOLLOW_APP_LAYOUT`) detects whether your app uses edge-to-edge and matches that configuration.

#### Migration guide
**No action required for most apps.** The new default `FOLLOW_APP_LAYOUT` automatically adapts to your app's layout.

If you relied on the previous behavior where fullscreen in-apps drew content behind the status bar, you can restore it explicitly:

```java
// Restore previous behavior: in-app content draws behind system bars
IterableConfig config = new IterableConfig.Builder()
.setInAppDisplayMode(IterableInAppDisplayMode.FORCE_EDGE_TO_EDGE)
.build();
```

If you want to ensure the close button is always accessible regardless of app configuration:

```java
// In-app content never goes behind system bars
IterableConfig config = new IterableConfig.Builder()
.setInAppDisplayMode(IterableInAppDisplayMode.FORCE_RESPECT_BOUNDS)
.build();
```

## [3.6.6]
### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ public class IterableConfig {
@Nullable
final IterableAPIMobileFrameworkInfo mobileFrameworkInfo;

/**
* Controls how in-app messages interact with the system bars (status bar, navigation bar).
* Defaults to {@link IterableInAppDisplayMode#FOLLOW_APP_LAYOUT}.
*/
final IterableInAppDisplayMode inAppDisplayMode;

/**
* Base URL for Webview content loading. Specifically used to enable CORS for external resources.
* If null or empty, defaults to empty string (original behavior with about:blank origin).
Expand Down Expand Up @@ -183,6 +189,7 @@ private IterableConfig(Builder builder) {
decryptionFailureHandler = builder.decryptionFailureHandler;
mobileFrameworkInfo = builder.mobileFrameworkInfo;
webViewBaseUrl = builder.webViewBaseUrl;
inAppDisplayMode = builder.inAppDisplayMode;
}

public static class Builder {
Expand Down Expand Up @@ -211,6 +218,7 @@ public static class Builder {
private IterableIdentityResolution identityResolution = new IterableIdentityResolution();
private IterableUnknownUserHandler iterableUnknownUserHandler;
private String webViewBaseUrl;
private IterableInAppDisplayMode inAppDisplayMode = IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;

public Builder() {}

Expand Down Expand Up @@ -453,6 +461,17 @@ public Builder setMobileFrameworkInfo(@NonNull IterableAPIMobileFrameworkInfo mo
return this;
}

/**
* Set how in-app messages interact with the system bars (status bar, navigation bar).
* Defaults to {@link IterableInAppDisplayMode#FOLLOW_APP_LAYOUT}, which preserves existing behavior.
* @param inAppDisplayMode the display mode for in-app messages
*/
@NonNull
public Builder setInAppDisplayMode(@NonNull IterableInAppDisplayMode inAppDisplayMode) {
this.inAppDisplayMode = inAppDisplayMode;
return this;
}

/**
* Set the base URL for WebView content loading. Used to enable CORS for external resources.
* If not set or null, defaults to empty string (original behavior with about:blank origin).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.iterable.iterableapi;

/**
* Controls how in-app messages interact with the system bars (status bar, navigation bar).
* <p>
* This setting is configured via {@link IterableConfig.Builder#setInAppDisplayMode(IterableInAppDisplayMode)}
* and applies globally to all in-app messages displayed by the SDK.
*/
public enum IterableInAppDisplayMode {

/**
* Default. The in-app message follows the host app's current layout configuration.
* If the app is edge-to-edge, the in-app will display edge-to-edge.
* If the app respects system bar bounds, the in-app will too.
*/
FOLLOW_APP_LAYOUT,

/**
* Forces in-app messages to display edge-to-edge, drawing content behind system bars.
* The in-app content will extend behind the status bar and navigation bar.
*/
FORCE_EDGE_TO_EDGE,

/**
* Forces in-app messages to display in fullscreen mode, hiding the status bar entirely.
* Uses legacy FLAG_FULLSCREEN on API &lt; 30 and WindowInsetsController on API 30+.
*/
FORCE_FULLSCREEN,

/**
* Forces in-app messages to respect system bar boundaries.
* Content will never draw behind the status bar or navigation bar,
* ensuring UI elements like the close button are always accessible.
*/
FORCE_RESPECT_BOUNDS
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
Expand All @@ -17,8 +16,6 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.OrientationEventListener;
Expand All @@ -36,7 +33,9 @@
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.fragment.app.DialogFragment;

public class IterableInAppFragmentHTMLNotification extends DialogFragment implements IterableWebView.HTMLNotificationCallbacks {
Expand Down Expand Up @@ -79,6 +78,8 @@
private boolean shouldAnimate;
private double inAppBackgroundAlpha;
private String inAppBackgroundColor;
private boolean hostIsEdgeToEdge;
private IterableInAppDisplayMode displayMode = IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;

public static IterableInAppFragmentHTMLNotification createInstance(@NonNull String htmlString, boolean callbackOnCancel, @NonNull IterableHelper.IterableUrlCallback clickCallback, @NonNull IterableInAppLocation location, @NonNull String messageId, @NonNull Double backgroundAlpha, @NonNull Rect padding) {
return IterableInAppFragmentHTMLNotification.createInstance(htmlString, callbackOnCancel, clickCallback, location, messageId, backgroundAlpha, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f));
Expand Down Expand Up @@ -150,6 +151,7 @@
}

notification = this;
displayMode = resolveDisplayMode();
}

@NonNull
Expand Down Expand Up @@ -177,13 +179,8 @@
applyWindowGravity(dialog.getWindow(), "onCreateDialog");
}

if (getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN) {
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else if (getInAppLayout(insetPadding) != InAppLayout.TOP) {
// For TOP layout in-app, status bar will be opaque so that the in-app content does not overlap with translucent status bar.
// For other non-fullscreen in-apps layouts (BOTTOM and CENTER), status bar will be translucent
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
hostIsEdgeToEdge = isHostActivityEdgeToEdge();
configureSystemBarsForMode(dialog.getWindow());
return dialog;
}

Expand All @@ -192,10 +189,6 @@
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

if (getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN) {
getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}

// Set initial window gravity based on inset padding (only for non-fullscreen)
if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) {
applyWindowGravity(getDialog().getWindow(), "onCreateView");
Expand Down Expand Up @@ -299,9 +292,7 @@
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Handle edge-to-edge insets with modern approach (only for non-fullscreen)
// Full screen in-apps should not have padding from system bars
if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) {
if (shouldApplySystemBarInsets()) {
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(0, sysBars.top, 0, sysBars.bottom);
Expand Down Expand Up @@ -513,11 +504,7 @@
}
};

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
webView.postOnAnimationDelayed(dismissWebViewRunnable, 400);
} else {
webView.postDelayed(dismissWebViewRunnable, 400);
}
webView.postOnAnimationDelayed(dismissWebViewRunnable, 400);
}

private void processMessageRemoval() {
Expand Down Expand Up @@ -606,30 +593,11 @@
return;
}

DisplayMetrics displayMetrics = activity.getResources().getDisplayMetrics();
Window window = notification.getDialog().getWindow();
Rect insetPadding = notification.insetPadding;

WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();

// Get the correct screen size based on api level
// https://stackoverflow.com/questions/35780980/getting-the-actual-screen-height-android
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
display.getRealSize(size);
} else {
display.getSize(size);
}

int webViewWidth = size.x;
int webViewHeight = size.y;

//Check if the dialog is full screen
if (insetPadding.bottom == 0 && insetPadding.top == 0) {
//Handle full screen
window.setLayout(webViewWidth, webViewHeight);
getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
} else {
// Resize the WebView directly with explicit size
float relativeHeight = height * getResources().getDisplayMetrics().density;
Expand Down Expand Up @@ -716,6 +684,130 @@
}
}

private IterableInAppDisplayMode resolveDisplayMode() {
try {
IterableConfig config = IterableApi.sharedInstance.config;
if (config != null) {
return config.inAppDisplayMode;
}
} catch (Exception e) {
IterableLogger.w(TAG, "Could not resolve display mode from config, using default");
}
return IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;
}

@SuppressWarnings("deprecation")
private void configureSystemBarsForMode(Window window) {
if (window == null) return;
InAppLayout layout = getInAppLayout(insetPadding);

switch (displayMode) {
case FORCE_EDGE_TO_EDGE:
applyEdgeToEdge(window);
break;

case FORCE_FULLSCREEN:
hideStatusBar(window);
break;

case FORCE_RESPECT_BOUNDS:
applyRespectBounds(window);
break;

case FOLLOW_APP_LAYOUT:
default:
configureSystemBarsFollowingApp(window, layout);
break;
}
}

private void applyEdgeToEdge(Window window) {
WindowCompat.setDecorFitsSystemWindows(window, false);
// On API 35+, system bars are transparent by default; these setters are no-ops
if (Build.VERSION.SDK_INT < 35) {
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
}
}

@SuppressWarnings("deprecation")
private void hideStatusBar(Window window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowCompat.setDecorFitsSystemWindows(window, false);
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, window.getDecorView());
controller.hide(WindowInsetsCompat.Type.statusBars());
controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
} else {
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}

private void applyRespectBounds(Window window) {
WindowCompat.setDecorFitsSystemWindows(window, true);
}

@SuppressWarnings("deprecation")
private void configureSystemBarsFollowingApp(Window window, InAppLayout layout) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'layout' is never used.
Activity activity = getActivity();
if (activity == null || activity.getWindow() == null) return;

if (hostIsEdgeToEdge) {
applyEdgeToEdge(window);
} else {
if (Build.VERSION.SDK_INT < 35) {
window.setStatusBarColor(activity.getWindow().getStatusBarColor());
window.setNavigationBarColor(activity.getWindow().getNavigationBarColor());
}
}
}

private boolean shouldApplySystemBarInsets() {
InAppLayout layout = getInAppLayout(insetPadding);

return switch (displayMode) {
case FORCE_EDGE_TO_EDGE, FORCE_FULLSCREEN -> false;
case FORCE_RESPECT_BOUNDS -> true;
default ->
layout != InAppLayout.FULLSCREEN && hostIsEdgeToEdge;
};
}

private boolean isHostActivityEdgeToEdge() {
Activity activity = getActivity();
if (activity == null || activity.getWindow() == null) return false;

if (hasEdgeToEdgeLegacyFlags(activity)) {
return true;
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return isContentDrawnBehindSystemBars(activity);
}

return false;
}

@SuppressWarnings("deprecation")
private boolean hasEdgeToEdgeLegacyFlags(Activity activity) {
int flags = activity.getWindow().getDecorView().getSystemUiVisibility();
return (flags & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0;
}

private boolean isContentDrawnBehindSystemBars(Activity activity) {
View contentView = activity.findViewById(android.R.id.content);
if (contentView == null) return false;

int contentTop = getViewTopPositionInWindow(contentView);
boolean statusBarPushesContentDown = contentTop > 0;
return !statusBarPushesContentDown;
}

private int getViewTopPositionInWindow(View view) {
int[] position = new int[2];
view.getLocationInWindow(position);
return position[1];
}

/**
* Sets the window gravity based on inset padding
*
Expand Down
Loading