diff --git a/CHANGELOG.md b/CHANGELOG.md index f4121c78b..67ac910e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java index 6e4bf7c45..e25aa9fb8 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java @@ -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 @@ -183,6 +191,7 @@ private IterableConfig(Builder builder) { decryptionFailureHandler = builder.decryptionFailureHandler; mobileFrameworkInfo = builder.mobileFrameworkInfo; webViewBaseUrl = builder.webViewBaseUrl; + shouldHideStatusBarOnFullScreenInApp = builder.shouldHideStatusBarOnFullScreenInApp; } public static class Builder { @@ -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() {} @@ -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); diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index b6b9a99f6..22cb3d1ea 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -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 { @@ -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"); + } } } @@ -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); @@ -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"); @@ -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); @@ -310,15 +318,20 @@ 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 */ @@ -326,6 +339,10 @@ public void onSaveInstanceState(@NonNull Bundle outState) { public void onStop() { orientationListener.disable(); + if (isFullScreenWithHiddenStatusBar()) { + restoreActivitySystemBars(); + } + super.onStop(); } @@ -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; @@ -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 { diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt index b017c66a6..e5e1b8ac4 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableConfigTest.kt @@ -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) + } } \ No newline at end of file diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java index ceaf7c586..ea2b3ab5d 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java @@ -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, "Test", "", 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, "Test", "", 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, "Test", "", 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()); + } }