Skip to content

Commit eccf755

Browse files
New config enum to deal with in-app layouts and how they should appear in the app. Changing default bahaviour to follow the app layout
1 parent c4b8202 commit eccf755

4 files changed

Lines changed: 171 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,36 @@ All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased]
6-
### Fixed
7-
- In-app messages now match the host app's system bar behavior instead of overriding it. This fixes fullscreen in-apps drawing content behind the status bar.
6+
### Added
7+
- New `IterableInAppDisplayMode` enum to control how in-app messages interact with system bars. Configure via `IterableConfig.Builder.setInAppDisplayMode()`:
8+
- `FOLLOW_APP_LAYOUT` (default) — matches the host app's system bar configuration. No change needed for existing integrations.
9+
- `FORCE_EDGE_TO_EDGE` — forces in-app content to draw behind system bars with transparent status and navigation bars.
10+
- `FORCE_FULLSCREEN` — hides the status bar entirely for all in-app messages.
11+
- `FORCE_RESPECT_BOUNDS` — ensures in-app content never overlaps system bars, keeping UI elements like the close button always accessible.
12+
13+
### Changed
14+
- 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.
15+
16+
### Migration guide
17+
**No action required for most apps.** The new default `FOLLOW_APP_LAYOUT` automatically adapts to your app's layout.
18+
19+
If you relied on the previous behavior where fullscreen in-apps drew content behind the status bar, you can restore it explicitly:
20+
21+
```java
22+
// Restore previous behavior: in-app content draws behind system bars
23+
IterableConfig config = new IterableConfig.Builder()
24+
.setInAppDisplayMode(IterableInAppDisplayMode.FORCE_EDGE_TO_EDGE)
25+
.build();
26+
```
27+
28+
If you want to ensure the close button is always accessible regardless of app configuration:
29+
30+
```java
31+
// In-app content never goes behind system bars
32+
IterableConfig config = new IterableConfig.Builder()
33+
.setInAppDisplayMode(IterableInAppDisplayMode.FORCE_RESPECT_BOUNDS)
34+
.build();
35+
```
836

937
## [3.7.0]
1038
- Replaced the deprecated `AsyncTask`-based push notification handling with `WorkManager` for improved reliability and compatibility with modern Android versions. No action is required.

iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ public class IterableConfig {
140140
@Nullable
141141
final IterableAPIMobileFrameworkInfo mobileFrameworkInfo;
142142

143+
/**
144+
* Controls how in-app messages interact with the system bars (status bar, navigation bar).
145+
* Defaults to {@link IterableInAppDisplayMode#FOLLOW_APP_LAYOUT}.
146+
*/
147+
final IterableInAppDisplayMode inAppDisplayMode;
148+
143149
/**
144150
* Base URL for Webview content loading. Specifically used to enable CORS for external resources.
145151
* If null or empty, defaults to empty string (original behavior with about:blank origin).
@@ -183,6 +189,7 @@ private IterableConfig(Builder builder) {
183189
decryptionFailureHandler = builder.decryptionFailureHandler;
184190
mobileFrameworkInfo = builder.mobileFrameworkInfo;
185191
webViewBaseUrl = builder.webViewBaseUrl;
192+
inAppDisplayMode = builder.inAppDisplayMode;
186193
}
187194

188195
public static class Builder {
@@ -211,6 +218,7 @@ public static class Builder {
211218
private IterableIdentityResolution identityResolution = new IterableIdentityResolution();
212219
private IterableUnknownUserHandler iterableUnknownUserHandler;
213220
private String webViewBaseUrl;
221+
private IterableInAppDisplayMode inAppDisplayMode = IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;
214222

215223
public Builder() {}
216224

@@ -453,6 +461,17 @@ public Builder setMobileFrameworkInfo(@NonNull IterableAPIMobileFrameworkInfo mo
453461
return this;
454462
}
455463

464+
/**
465+
* Set how in-app messages interact with the system bars (status bar, navigation bar).
466+
* Defaults to {@link IterableInAppDisplayMode#FOLLOW_APP_LAYOUT}, which preserves existing behavior.
467+
* @param inAppDisplayMode the display mode for in-app messages
468+
*/
469+
@NonNull
470+
public Builder setInAppDisplayMode(@NonNull IterableInAppDisplayMode inAppDisplayMode) {
471+
this.inAppDisplayMode = inAppDisplayMode;
472+
return this;
473+
}
474+
456475
/**
457476
* Set the base URL for WebView content loading. Used to enable CORS for external resources.
458477
* If not set or null, defaults to empty string (original behavior with about:blank origin).
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.iterable.iterableapi;
2+
3+
/**
4+
* Controls how in-app messages interact with the system bars (status bar, navigation bar).
5+
* <p>
6+
* This setting is configured via {@link IterableConfig.Builder#setInAppDisplayMode(IterableInAppDisplayMode)}
7+
* and applies globally to all in-app messages displayed by the SDK.
8+
*/
9+
public enum IterableInAppDisplayMode {
10+
11+
/**
12+
* Default. The in-app message follows the host app's current layout configuration.
13+
* If the app is edge-to-edge, the in-app will display edge-to-edge.
14+
* If the app respects system bar bounds, the in-app will too.
15+
*/
16+
FOLLOW_APP_LAYOUT,
17+
18+
/**
19+
* Forces in-app messages to display edge-to-edge, drawing content behind system bars.
20+
* The in-app content will extend behind the status bar and navigation bar.
21+
*/
22+
FORCE_EDGE_TO_EDGE,
23+
24+
/**
25+
* Forces in-app messages to display in fullscreen mode, hiding the status bar entirely.
26+
* Uses legacy FLAG_FULLSCREEN on API &lt; 30 and WindowInsetsController on API 30+.
27+
*/
28+
FORCE_FULLSCREEN,
29+
30+
/**
31+
* Forces in-app messages to respect system bar boundaries.
32+
* Content will never draw behind the status bar or navigation bar,
33+
* ensuring UI elements like the close button are always accessible.
34+
*/
35+
FORCE_RESPECT_BOUNDS
36+
}

iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import androidx.core.view.ViewCompat;
3535
import androidx.core.view.WindowCompat;
3636
import androidx.core.view.WindowInsetsCompat;
37+
import androidx.core.view.WindowInsetsControllerCompat;
3738
import androidx.fragment.app.DialogFragment;
3839

3940
public class IterableInAppFragmentHTMLNotification extends DialogFragment implements IterableWebView.HTMLNotificationCallbacks {
@@ -77,6 +78,7 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem
7778
private double inAppBackgroundAlpha;
7879
private String inAppBackgroundColor;
7980
private boolean hostIsEdgeToEdge;
81+
private IterableInAppDisplayMode displayMode = IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;
8082

8183
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) {
8284
return IterableInAppFragmentHTMLNotification.createInstance(htmlString, callbackOnCancel, clickCallback, location, messageId, backgroundAlpha, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f));
@@ -148,6 +150,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
148150
}
149151

150152
notification = this;
153+
displayMode = resolveDisplayMode();
151154
}
152155

153156
@NonNull
@@ -176,7 +179,7 @@ public void onCancel(DialogInterface dialog) {
176179
}
177180

178181
hostIsEdgeToEdge = isHostActivityEdgeToEdge();
179-
configureSystemBarBehavior(dialog.getWindow());
182+
configureSystemBarsForMode(dialog.getWindow());
180183
return dialog;
181184
}
182185

@@ -288,7 +291,7 @@ public void run() {
288291
@Override
289292
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
290293
super.onViewCreated(view, savedInstanceState);
291-
if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN && hostIsEdgeToEdge) {
294+
if (shouldApplySystemBarInsets()) {
292295
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
293296
Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
294297
v.setPadding(0, sysBars.top, 0, sysBars.bottom);
@@ -500,11 +503,7 @@ public void run() {
500503
}
501504
};
502505

503-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
504-
webView.postOnAnimationDelayed(dismissWebViewRunnable, 400);
505-
} else {
506-
webView.postDelayed(dismissWebViewRunnable, 400);
507-
}
506+
webView.postOnAnimationDelayed(dismissWebViewRunnable, 400);
508507
}
509508

510509
private void processMessageRemoval() {
@@ -684,21 +683,94 @@ static int roundToNearest90Degrees(int orientation) {
684683
}
685684
}
686685

687-
private void configureSystemBarBehavior(Window window) {
686+
private IterableInAppDisplayMode resolveDisplayMode() {
687+
try {
688+
IterableConfig config = IterableApi.sharedInstance.config;
689+
if (config != null) {
690+
return config.inAppDisplayMode;
691+
}
692+
} catch (Exception e) {
693+
IterableLogger.w(TAG, "Could not resolve display mode from config, using default");
694+
}
695+
return IterableInAppDisplayMode.FOLLOW_APP_LAYOUT;
696+
}
697+
698+
@SuppressWarnings("deprecation")
699+
private void configureSystemBarsForMode(Window window) {
688700
if (window == null) return;
701+
InAppLayout layout = getInAppLayout(insetPadding);
702+
703+
switch (displayMode) {
704+
case FORCE_EDGE_TO_EDGE:
705+
applyEdgeToEdge(window);
706+
break;
707+
708+
case FORCE_FULLSCREEN:
709+
hideStatusBar(window);
710+
break;
711+
712+
case FORCE_RESPECT_BOUNDS:
713+
applyRespectBounds(window);
714+
break;
715+
716+
case FOLLOW_APP_LAYOUT:
717+
default:
718+
configureSystemBarsFollowingApp(window, layout);
719+
break;
720+
}
721+
}
722+
723+
private void applyEdgeToEdge(Window window) {
724+
WindowCompat.setDecorFitsSystemWindows(window, false);
725+
// On API 35+, system bars are transparent by default; these setters are no-ops
726+
if (Build.VERSION.SDK_INT < 35) {
727+
window.setStatusBarColor(Color.TRANSPARENT);
728+
window.setNavigationBarColor(Color.TRANSPARENT);
729+
}
730+
}
731+
732+
@SuppressWarnings("deprecation")
733+
private void hideStatusBar(Window window) {
734+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
735+
WindowCompat.setDecorFitsSystemWindows(window, false);
736+
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, window.getDecorView());
737+
controller.hide(WindowInsetsCompat.Type.statusBars());
738+
controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
739+
} else {
740+
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
741+
}
742+
}
743+
744+
private void applyRespectBounds(Window window) {
745+
WindowCompat.setDecorFitsSystemWindows(window, true);
746+
}
747+
748+
@SuppressWarnings("deprecation")
749+
private void configureSystemBarsFollowingApp(Window window, InAppLayout layout) {
689750
Activity activity = getActivity();
690751
if (activity == null || activity.getWindow() == null) return;
691752

692753
if (hostIsEdgeToEdge) {
693-
WindowCompat.setDecorFitsSystemWindows(window, false);
694-
window.setStatusBarColor(Color.TRANSPARENT);
695-
window.setNavigationBarColor(Color.TRANSPARENT);
754+
applyEdgeToEdge(window);
696755
} else {
697-
window.setStatusBarColor(activity.getWindow().getStatusBarColor());
698-
window.setNavigationBarColor(activity.getWindow().getNavigationBarColor());
756+
if (Build.VERSION.SDK_INT < 35) {
757+
window.setStatusBarColor(activity.getWindow().getStatusBarColor());
758+
window.setNavigationBarColor(activity.getWindow().getNavigationBarColor());
759+
}
699760
}
700761
}
701762

763+
private boolean shouldApplySystemBarInsets() {
764+
InAppLayout layout = getInAppLayout(insetPadding);
765+
766+
return switch (displayMode) {
767+
case FORCE_EDGE_TO_EDGE, FORCE_FULLSCREEN -> false;
768+
case FORCE_RESPECT_BOUNDS -> true;
769+
default ->
770+
layout != InAppLayout.FULLSCREEN && hostIsEdgeToEdge;
771+
};
772+
}
773+
702774
private boolean isHostActivityEdgeToEdge() {
703775
Activity activity = getActivity();
704776
if (activity == null || activity.getWindow() == null) return false;
@@ -714,6 +786,7 @@ private boolean isHostActivityEdgeToEdge() {
714786
return false;
715787
}
716788

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

0 commit comments

Comments
 (0)