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

## [Unreleased]
### Added
- Added `setShouldHideStatusBarOnFullScreenInApp` option to `IterableConfig.Builder`. When enabled, full screen in-app messages will hide the system status bar and navigation bar while the message is displayed, and automatically restore them when dismissed. Defaults to `false`.

## [3.6.4]
### Fixed
- Updated `customPayload` of In-App Messages to be `@Nullable`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ public class IterableConfig {
@Nullable
final String webViewBaseUrl;

/**
* When set to {@code true}, full screen in-app messages will hide the system status bar
* and navigation bar while the message is displayed. The system bars are automatically
* restored when the message is dismissed.
* Defaults to {@code false}.
*/
final boolean shouldHideStatusBarOnFullScreenInApp;

/**
* Get the configured WebView base URL
* @return Base URL for WebView content, or null if not configured
Expand Down Expand Up @@ -183,6 +191,7 @@ private IterableConfig(Builder builder) {
decryptionFailureHandler = builder.decryptionFailureHandler;
mobileFrameworkInfo = builder.mobileFrameworkInfo;
webViewBaseUrl = builder.webViewBaseUrl;
shouldHideStatusBarOnFullScreenInApp = builder.shouldHideStatusBarOnFullScreenInApp;
}

public static class Builder {
Expand Down Expand Up @@ -211,6 +220,7 @@ public static class Builder {
private IterableIdentityResolution identityResolution = new IterableIdentityResolution();
private IterableUnknownUserHandler iterableUnknownUserHandler;
private String webViewBaseUrl;
private boolean shouldHideStatusBarOnFullScreenInApp = false;

public Builder() {}

Expand Down Expand Up @@ -465,6 +475,19 @@ public Builder setWebViewBaseUrl(@Nullable String webViewBaseUrl) {
return this;
}

/**
* Set whether the SDK should hide the system status bar and navigation bar when
* displaying full screen in-app messages. When enabled, the system bars are hidden
* while the message is shown and automatically restored when it is dismissed.
* Defaults to {@code false}.
* @param shouldHide {@code true} to hide system bars for full screen in-app messages
*/
@NonNull
public Builder setShouldHideStatusBarOnFullScreenInApp(boolean shouldHide) {
this.shouldHideStatusBarOnFullScreenInApp = shouldHide;
return this;
}

@NonNull
public IterableConfig build() {
return new IterableConfig(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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 @@ -125,10 +127,18 @@ public IterableInAppFragmentHTMLNotification() {
public void onStart() {
super.onStart();

// Set dialog positioning after the dialog is created and shown (only for non-fullscreen)
Dialog dialog = getDialog();
if (dialog != null && getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) {
applyWindowGravity(dialog.getWindow(), "onStart");
if (dialog != null) {
if (isFullScreenWithHiddenStatusBar()) {
hideActivitySystemBars();

Window dialogWindow = dialog.getWindow();
if (dialogWindow != null) {
WindowCompat.setDecorFitsSystemWindows(dialogWindow, false);
}
} else if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) {
applyWindowGravity(dialog.getWindow(), "onStart");
}
}
}

Expand Down Expand Up @@ -176,9 +186,7 @@ public void onCancel(DialogInterface dialog) {
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) {
if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN && 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);
Expand All @@ -191,10 +199,6 @@ public void onCancel(DialogInterface dialog) {
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 @@ -295,9 +299,13 @@ public void run() {
@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 (isFullScreenWithHiddenStatusBar()) {
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
v.setPadding(0, 0, 0, 0);
return WindowInsetsCompat.CONSUMED;
});
} else if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) {
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(0, sysBars.top, 0, sysBars.bottom);
Expand All @@ -310,22 +318,31 @@ public void setLoaded(boolean loaded) {
this.loaded = loaded;
}

/**
* Sets up the webView and the dialog layout
*/
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(IN_APP_OPEN_TRACKED, true);
}

@Override
public void onResume() {
super.onResume();
if (isFullScreenWithHiddenStatusBar()) {
hideActivitySystemBars();
}
}

/**
* On Stop of the dialog
*/
@Override
public void onStop() {
orientationListener.disable();

if (isFullScreenWithHiddenStatusBar()) {
restoreActivitySystemBars();
}

super.onStop();
}

Expand Down Expand Up @@ -623,7 +640,6 @@ public void run() {
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);
} else {
// Resize the WebView directly with explicit size
float relativeHeight = height * getResources().getDisplayMetrics().density;
Expand Down Expand Up @@ -747,6 +763,53 @@ InAppLayout getInAppLayout(Rect padding) {
return InAppLayout.CENTER;
}
}

private boolean isFullScreenWithHiddenStatusBar() {
return getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN && shouldHideStatusBar();
}

private boolean shouldHideStatusBar() {
try {
return IterableApi.sharedInstance != null
&& IterableApi.sharedInstance.config != null
&& IterableApi.sharedInstance.config.shouldHideStatusBarOnFullScreenInApp;
} catch (Exception e) {
return false;
}
}

private void hideActivitySystemBars() {
Activity activity = getActivity();
if (activity == null || activity.getWindow() == null) {
return;
}

try {
Window window = activity.getWindow();
WindowCompat.setDecorFitsSystemWindows(window, false);
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, window.getDecorView());
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
} catch (Exception e) {
IterableLogger.d(TAG, "Failed to hide system bars on activity: " + e.getMessage());
}
}

private void restoreActivitySystemBars() {
Activity activity = getActivity();
if (activity == null || activity.getWindow() == null) {
return;
}

try {
Window window = activity.getWindow();
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(window, window.getDecorView());
controller.show(WindowInsetsCompat.Type.systemBars());
WindowCompat.setDecorFitsSystemWindows(window, true);
} catch (Exception e) {
IterableLogger.d(TAG, "Failed to restore system bars on activity: " + e.getMessage());
}
}
}

enum InAppLayout {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,18 @@ class IterableConfigTest {
val config: IterableConfig = configBuilder.build()
assertFalse(config.keychainEncryption)
}

@Test
fun defaultShouldHideStatusBarOnFullScreenInApp() {
val config = IterableConfig.Builder().build()
assertFalse(config.shouldHideStatusBarOnFullScreenInApp)
}

@Test
fun setShouldHideStatusBarOnFullScreenInApp() {
val config = IterableConfig.Builder()
.setShouldHideStatusBarOnFullScreenInApp(true)
.build()
assertTrue(config.shouldHideStatusBarOnFullScreenInApp)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,57 @@ public void testRoundToNearest90Degrees_EdgeCases() {
assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(315));
assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(359));
}

// ===== Status Bar Hiding Config Tests =====

@Test
public void testFullScreenInApp_StatusBarNotHiddenByDefault() {
// With default config (shouldHideStatusBarOnFullScreenInApp = false),
// full screen in-app messages should show without crashing and without hiding system bars
Rect fullScreenPadding = new Rect(0, 0, 0, 0);
IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "<html><body>Test</body></html>", "", null, 0.0, fullScreenPadding, true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP);
shadowOf(getMainLooper()).idle();

IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance();
assertNotNull(notification);
assertNotNull(notification.getDialog());
assertTrue(notification.getDialog().isShowing());
}

@Test
public void testFullScreenInApp_StatusBarHiddenWhenConfigEnabled() {
// Re-initialize with the config flag enabled
IterableTestUtils.resetIterableApi();
IterableTestUtils.createIterableApiNew(builder ->
builder.setShouldHideStatusBarOnFullScreenInApp(true)
);

Rect fullScreenPadding = new Rect(0, 0, 0, 0);
IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "<html><body>Test</body></html>", "", null, 0.0, fullScreenPadding, true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP);
shadowOf(getMainLooper()).idle();

IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance();
assertNotNull(notification);
assertNotNull(notification.getDialog());
assertTrue(notification.getDialog().isShowing());
}

@Test
public void testNonFullScreenInApp_UnaffectedByStatusBarConfig() {
// Re-initialize with the config flag enabled
IterableTestUtils.resetIterableApi();
IterableTestUtils.createIterableApiNew(builder ->
builder.setShouldHideStatusBarOnFullScreenInApp(true)
);

// Use top padding (non-fullscreen) - should not be affected by the status bar config
Rect topPadding = new Rect(0, 0, 0, -1);
IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "<html><body>Test</body></html>", "", null, 0.0, topPadding, true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP);
shadowOf(getMainLooper()).idle();

IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance();
assertNotNull(notification);
assertNotNull(notification.getDialog());
assertTrue(notification.getDialog().isShowing());
}
}
Loading