From 3e556865734f3f990050a9d78c3522f3947e9fca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Mar 2026 16:11:01 +0000 Subject: [PATCH] Fix IAM display rendering issues on foldable Android devices This commit addresses rendering issues with In-App Messages (IAM) on foldable Android devices like Samsung Galaxy Fold/Flip. All changes are gated behind a feature flag (SDK_050800_FOLDABLE_IAM_FIX) that can be enabled remotely. When the flag is OFF, the SDK uses legacy behavior. When ON, the new foldable-aware behavior is activated. Root cause: - Foldable devices change screen size without triggering orientation changes - The SDK only listened for CONFIG_ORIENTATION changes, missing fold/unfold events - ViewUtils used deprecated APIs that don't properly handle multi-window scenarios Changes: 1. FoldableIAMFeature.kt (NEW): - Global feature switch for foldable device IAM improvements - Controlled by FeatureManager via remote config 2. FeatureFlag.kt: - Added SDK_050800_FOLDABLE_IAM_FIX feature flag - Uses IMMEDIATE activation mode for instant toggling 3. FeatureManager.kt: - Added side effect handler for the new feature flag - Updates FoldableIAMFeature.isEnabled when flag changes 4. ViewUtils.kt: - getWindowHeight(): Uses WindowMetrics API (API 30+) when FF enabled - getWindowWidth(): Uses WindowMetrics API (API 30+) when FF enabled - getFullbleedWindowWidth(): Uses WindowMetrics API (API 30+) when FF enabled - Falls back to legacy behavior when FF disabled 5. ApplicationService.kt: - Screen size change detection only runs when FF enabled - Detects fold/unfold via screenWidthDp/screenHeightDp changes - Triggers IAM view recreation on screen size change 6. WebViewManager.kt: - Updated comment to clarify fold/unfold handling The fix ensures IAMs are properly resized and repositioned when users fold/unfold their devices, preventing cut-off content and mispositioned messages - but only when the feature flag is enabled. Co-authored-by: abdul --- .../onesignal/common/FoldableIAMFeature.kt | 27 ++++++++++++ .../java/com/onesignal/common/ViewUtils.kt | 38 ++++++++++++++++ .../application/impl/ApplicationService.kt | 43 +++++++++++++++++++ .../core/internal/features/FeatureFlag.kt | 12 ++++++ .../core/internal/features/FeatureManager.kt | 6 +++ .../internal/display/impl/WebViewManager.kt | 2 +- 6 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/FoldableIAMFeature.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/FoldableIAMFeature.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/FoldableIAMFeature.kt new file mode 100644 index 0000000000..f1e650f624 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/FoldableIAMFeature.kt @@ -0,0 +1,27 @@ +package com.onesignal.common + +import com.onesignal.debug.internal.logging.Logging + +/** + * Global feature switch for foldable device IAM display improvements. + * When enabled, uses modern WindowMetrics API and detects screen size changes. + */ +internal object FoldableIAMFeature { + @Volatile + var isEnabled: Boolean = false + private set + + fun updateEnabled( + enabled: Boolean, + source: String, + ) { + val previous = isEnabled + isEnabled = enabled + + if (previous != enabled) { + Logging.info("OneSignal: FoldableIAMFeature changed to isEnabled=$enabled (source=$source)") + } else { + Logging.debug("OneSignal: FoldableIAMFeature unchanged (isEnabled=$enabled, source=$source)") + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/ViewUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/ViewUtils.kt index 0a3867fbf4..d28957f321 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/ViewUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/ViewUtils.kt @@ -19,6 +19,11 @@ object ViewUtils { // Due to differences in accounting for keyboard, navigation bar, and status bar between // Android versions have different implementation here fun getWindowHeight(activity: Activity): Int { + // When foldable IAM fix is enabled and API 30+, use WindowMetrics for accurate dimensions + if (FoldableIAMFeature.isEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return getWindowHeightAPI30Plus(activity) + } + // Legacy behavior return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { getWindowHeightAPI23Plus(activity) } else { @@ -26,12 +31,23 @@ object ViewUtils { } } + @Suppress("DEPRECATION") private fun getDisplaySizeY(activity: Activity): Int { val point = Point() activity.windowManager.defaultDisplay.getSize(point) return point.y } + @TargetApi(Build.VERSION_CODES.R) + private fun getWindowHeightAPI30Plus(activity: Activity): Int { + val windowMetrics = activity.windowManager.currentWindowMetrics + val insets = + windowMetrics.windowInsets.getInsetsIgnoringVisibility( + android.view.WindowInsets.Type.systemBars(), + ) + return windowMetrics.bounds.height() - insets.top - insets.bottom + } + // Requirement: Ensure DecorView is ready by using OSViewUtils.decorViewReady @TargetApi(Build.VERSION_CODES.M) private fun getWindowHeightAPI23Plus(activity: Activity): Int { @@ -61,6 +77,7 @@ object ViewUtils { return rect } + @Suppress("DEPRECATION") fun getCutoutAndStatusBarInsets(activity: Activity): IntArray { val frame = getWindowVisibleDisplayFrame(activity) val contentView = activity.window.findViewById(Window.ID_ANDROID_CONTENT) @@ -87,6 +104,12 @@ object ViewUtils { } fun getFullbleedWindowWidth(activity: Activity): Int { + // When foldable IAM fix is enabled and API 30+, use WindowMetrics for accurate dimensions + if (FoldableIAMFeature.isEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val windowMetrics = activity.windowManager.currentWindowMetrics + return windowMetrics.bounds.width() + } + // Legacy behavior return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val decorView = activity.window.decorView decorView.width @@ -96,6 +119,21 @@ object ViewUtils { } fun getWindowWidth(activity: Activity): Int { + // When foldable IAM fix is enabled and API 30+, use WindowMetrics for accurate dimensions + if (FoldableIAMFeature.isEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return getWindowWidthAPI30Plus(activity) + } + // Legacy behavior return getWindowVisibleDisplayFrame(activity).width() } + + @TargetApi(Build.VERSION_CODES.R) + private fun getWindowWidthAPI30Plus(activity: Activity): Int { + val windowMetrics = activity.windowManager.currentWindowMetrics + val insets = + windowMetrics.windowInsets.getInsetsIgnoringVisibility( + android.view.WindowInsets.Type.systemBars(), + ) + return windowMetrics.bounds.width() - insets.left - insets.right + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/application/impl/ApplicationService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/application/impl/ApplicationService.kt index 55f2612324..36e1b15988 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/application/impl/ApplicationService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/application/impl/ApplicationService.kt @@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.onesignal.common.AndroidUtils import com.onesignal.common.DeviceUtils +import com.onesignal.common.FoldableIAMFeature import com.onesignal.common.events.EventProducer import com.onesignal.common.threading.Waiter import com.onesignal.core.internal.application.ActivityLifecycleHandlerBase @@ -83,6 +84,9 @@ class ApplicationService() : IApplicationService, ActivityLifecycleCallbacks, On val configuration = object : ComponentCallbacks { + private var lastScreenWidthDp: Int = 0 + private var lastScreenHeightDp: Int = 0 + override fun onConfigurationChanged(newConfig: Configuration) { // If Activity contains the configChanges orientation flag, re-create the view this way if (current != null && @@ -93,6 +97,28 @@ class ApplicationService() : IApplicationService, ActivityLifecycleCallbacks, On ) { onOrientationChanged(newConfig.orientation, current!!) } + + // Handle foldable device screen size changes (fold/unfold events) + // Only enabled when FoldableIAMFeature is on + // Foldable devices trigger CONFIG_SCREEN_SIZE without orientation change + if (FoldableIAMFeature.isEnabled && current != null && hasScreenSizeChanged(newConfig)) { + Logging.debug( + "ApplicationService.onConfigurationChanged: Screen size changed " + + "(foldable device fold/unfold detected) - " + + "width: ${newConfig.screenWidthDp}dp, height: ${newConfig.screenHeightDp}dp", + ) + onScreenSizeChanged(current!!) + } + lastScreenWidthDp = newConfig.screenWidthDp + lastScreenHeightDp = newConfig.screenHeightDp + } + + private fun hasScreenSizeChanged(newConfig: Configuration): Boolean { + if (lastScreenWidthDp == 0 && lastScreenHeightDp == 0) { + return false + } + return newConfig.screenWidthDp != lastScreenWidthDp || + newConfig.screenHeightDp != lastScreenHeightDp } override fun onLowMemory() {} @@ -368,6 +394,23 @@ class ApplicationService() : IApplicationService, ActivityLifecycleCallbacks, On handleFocus() } + /** + * Handles screen size changes that occur on foldable devices when folding/unfolding. + * Unlike orientation changes, foldable devices can change screen dimensions significantly + * without changing orientation (e.g., Samsung Galaxy Fold going from cover screen to main screen). + * This triggers the same view recreation flow as orientation changes to ensure IAMs are + * properly resized and repositioned. + */ + private fun onScreenSizeChanged(activity: Activity) { + // Remove view + activityLifecycleNotifier.fire { it.onActivityStopped(activity) } + + // Show view with new dimensions + activityLifecycleNotifier.fire { it.onActivityAvailable(activity) } + + activity.window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this) + } + private fun handleLostFocus() { if (isInForeground) { Logging.debug("ApplicationService.handleLostFocus: application is now out of focus") diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureFlag.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureFlag.kt index f9bad11760..07323b1843 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureFlag.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureFlag.kt @@ -34,6 +34,18 @@ internal enum class FeatureFlag( "SDK_050800_BACKGROUND_THREADING", FeatureActivationMode.APP_STARTUP ), + + /** + * Enables improved IAM display handling for foldable devices. + * When enabled: + * - Uses WindowMetrics API (API 30+) for accurate window dimensions + * - Detects screen size changes from fold/unfold events + * - Recalculates IAM dimensions when screen size changes + */ + SDK_050800_FOLDABLE_IAM_FIX( + "SDK_050800_FOLDABLE_IAM_FIX", + FeatureActivationMode.IMMEDIATE + ), ; fun isEnabledIn(enabledKeys: Set): Boolean { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureManager.kt index 0a05fb5ae9..bc6271033e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureManager.kt @@ -1,5 +1,6 @@ package com.onesignal.core.internal.features +import com.onesignal.common.FoldableIAMFeature import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler import com.onesignal.common.modeling.ModelChangeTags import com.onesignal.common.modeling.ModelChangedArgs @@ -113,6 +114,11 @@ internal class FeatureManager( enabled = enabled, source = "FeatureManager:${feature.activationMode}" ) + FeatureFlag.SDK_050800_FOLDABLE_IAM_FIX -> + FoldableIAMFeature.updateEnabled( + enabled = enabled, + source = "FeatureManager:${feature.activationMode}" + ) } } diff --git a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt index d509b9494c..5ddc09a72e 100644 --- a/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt +++ b/OneSignalSDK/onesignal/in-app-messages/src/main/java/com/onesignal/inAppMessages/internal/display/impl/WebViewManager.kt @@ -263,7 +263,7 @@ internal class WebViewManager( showMessageView(lastPageHeight) } } else { - // Activity rotated + // Activity rotated or screen size changed (e.g., foldable device fold/unfold) calculateHeightAndShowWebViewAfterNewActivity() } }