From 0fb31c15bad7a6156e8f92d78cafb81a76b5296e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Wed, 10 Sep 2025 07:19:02 -0700 Subject: [PATCH 1/9] Cursor Iteration Needs reduction in code. Lot of repition. Heavy review and testing needed. Committing as inapps are looking stable and are sticking with the layout with back to back phone rotations when inapp performance would fail usually --- ...IterableInAppFragmentHTMLNotification.java | 241 ++++++++++++++++-- .../iterableapi/IterableWebChromeClient.java | 5 +- 2 files changed, 229 insertions(+), 17 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index 779c1c93c..df67a8248 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -26,6 +26,7 @@ import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; import android.widget.RelativeLayout; import androidx.annotation.NonNull; @@ -58,6 +59,12 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem private boolean callbackOnCancel = false; private String htmlString; private String messageId; + + // Resize debouncing fields + private Handler resizeHandler = new Handler(); + private Runnable pendingResizeRunnable; + private float lastContentHeight = -1; + private static final int RESIZE_DEBOUNCE_DELAY_MS = 200; private double backgroundAlpha; //TODO: remove in a future version private Rect insetPadding; @@ -105,6 +112,35 @@ public IterableInAppFragmentHTMLNotification() { insetPadding = new Rect(); this.setStyle(DialogFragment.STYLE_NO_FRAME, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar); } + + @Override + public void onStart() { + super.onStart(); + + // Set dialog positioning after the dialog is created and shown + Dialog dialog = getDialog(); + if (dialog != null) { + Window window = dialog.getWindow(); + if (window != null) { + WindowManager.LayoutParams windowParams = window.getAttributes(); + int startGravity = getVerticalLocation(insetPadding); + + if (startGravity == Gravity.CENTER_VERTICAL) { + windowParams.gravity = Gravity.CENTER; + IterableLogger.d(TAG, "Set dialog gravity to CENTER in onStart"); + } else if (startGravity == Gravity.TOP) { + windowParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Set dialog gravity to TOP in onStart"); + } else if (startGravity == Gravity.BOTTOM) { + windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Set dialog gravity to BOTTOM in onStart"); + } + + window.setAttributes(windowParams); + IterableLogger.d(TAG, "Applied window gravity in onStart: " + windowParams.gravity); + } + } + } @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -144,6 +180,25 @@ public void onCancel(DialogInterface dialog) { } }); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + + // Set window gravity for the dialog + Window window = dialog.getWindow(); + WindowManager.LayoutParams windowParams = window.getAttributes(); + int dialogGravity = getVerticalLocation(insetPadding); + + if (dialogGravity == Gravity.CENTER_VERTICAL) { + windowParams.gravity = Gravity.CENTER; + IterableLogger.d(TAG, "Set dialog gravity to CENTER in onCreateDialog"); + } else if (dialogGravity == Gravity.TOP) { + windowParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Set dialog gravity to TOP in onCreateDialog"); + } else if (dialogGravity == Gravity.BOTTOM) { + windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Set dialog gravity to BOTTOM in onCreateDialog"); + } + + window.setAttributes(windowParams); + if (getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN) { dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } else if (getInAppLayout(insetPadding) != InAppLayout.TOP) { @@ -162,23 +217,57 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c if (getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN) { getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } + + // Set initial window gravity based on inset padding + Window window = getDialog().getWindow(); + WindowManager.LayoutParams windowParams = window.getAttributes(); + int windowGravity = getVerticalLocation(insetPadding); + + if (windowGravity == Gravity.CENTER_VERTICAL) { + windowParams.gravity = Gravity.CENTER; + IterableLogger.d(TAG, "Set initial CENTER window gravity in onCreateView"); + } else if (windowGravity == Gravity.TOP) { + windowParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Set initial TOP window gravity in onCreateView"); + } else if (windowGravity == Gravity.BOTTOM) { + windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Set initial BOTTOM window gravity in onCreateView"); + } + + window.setAttributes(windowParams); webView = new IterableWebView(getContext()); webView.setId(R.id.webView); + + // Debug the HTML content + IterableLogger.d(TAG, "HTML content preview: " + (htmlString.length() > 200 ? htmlString.substring(0, 200) + "..." : htmlString)); + webView.createWithHtml(this, htmlString); if (orientationListener == null) { orientationListener = new OrientationEventListener(getContext(), SensorManager.SENSOR_DELAY_NORMAL) { + private int lastOrientation = -1; + // Resize the webView on device rotation public void onOrientationChanged(int orientation) { - if (loaded) { - final Handler handler = new Handler(); - handler.postDelayed(new Runnable() { - @Override - public void run() { - runResizeScript(); - } - }, 1000); + if (loaded && webView != null) { + // Only trigger on significant orientation changes (90 degree increments) + int currentOrientation = ((orientation + 45) / 90) * 90; + if (currentOrientation != lastOrientation && lastOrientation != -1) { + lastOrientation = currentOrientation; + + // Use longer delay for orientation changes to allow layout to stabilize + final Handler handler = new Handler(); + handler.postDelayed(new Runnable() { + @Override + public void run() { + IterableLogger.d(TAG, "Orientation changed, triggering resize"); + runResizeScript(); + } + }, 1500); // Increased delay for better stability + } else if (lastOrientation == -1) { + lastOrientation = currentOrientation; + } } } }; @@ -186,17 +275,53 @@ public void run() { orientationListener.enable(); - RelativeLayout relativeLayout = new RelativeLayout(this.getContext()); - RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); - relativeLayout.setVerticalGravity(getVerticalLocation(insetPadding)); - relativeLayout.addView(webView, layoutParams); + // Create a FrameLayout as the main container for better positioning control + FrameLayout frameLayout = new FrameLayout(this.getContext()); + + // Create a RelativeLayout as a wrapper for the WebView + RelativeLayout webViewContainer = new RelativeLayout(this.getContext()); + + int gravity = getVerticalLocation(insetPadding); + IterableLogger.d(TAG, "Initial setup - gravity: " + gravity + " for inset padding: " + insetPadding); + + // Set FrameLayout gravity based on positioning + FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ); + + if (gravity == Gravity.CENTER_VERTICAL) { + containerParams.gravity = Gravity.CENTER; + IterableLogger.d(TAG, "Applied CENTER gravity to container"); + } else if (gravity == Gravity.TOP) { + containerParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Applied TOP gravity to container"); + } else if (gravity == Gravity.BOTTOM) { + containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + IterableLogger.d(TAG, "Applied BOTTOM gravity to container"); + } + + // Add WebView to the RelativeLayout container with WRAP_CONTENT for proper sizing + RelativeLayout.LayoutParams webViewParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT + ); + webViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); + webViewContainer.addView(webView, webViewParams); + + IterableLogger.d(TAG, "Added WebView with WRAP_CONTENT and CENTER_IN_PARENT rule"); + + // Add the container to the FrameLayout + frameLayout.addView(webViewContainer, containerParams); + + IterableLogger.d(TAG, "Created FrameLayout with positioned RelativeLayout container"); if (savedInstanceState == null || !savedInstanceState.getBoolean(IN_APP_OPEN_TRACKED, false)) { IterableApi.sharedInstance.trackInAppOpen(messageId, location); } prepareToShowWebView(); - return relativeLayout; + return frameLayout; } public void setLoaded(boolean loaded) { @@ -226,6 +351,12 @@ public void onStop() { public void onDestroy() { super.onDestroy(); + // Clean up pending resize operations + if (resizeHandler != null && pendingResizeRunnable != null) { + resizeHandler.removeCallbacks(pendingResizeRunnable); + pendingResizeRunnable = null; + } + if (this.getActivity() != null && this.getActivity().isChangingConfigurations()) { return; } @@ -414,7 +545,50 @@ private void processMessageRemoval() { @Override public void runResizeScript() { - resize(webView.getContentHeight()); + // Cancel any pending resize operation + if (pendingResizeRunnable != null) { + resizeHandler.removeCallbacks(pendingResizeRunnable); + } + + // Schedule a debounced resize operation + pendingResizeRunnable = new Runnable() { + @Override + public void run() { + performResizeWithValidation(); + } + }; + + resizeHandler.postDelayed(pendingResizeRunnable, RESIZE_DEBOUNCE_DELAY_MS); + } + + private void performResizeWithValidation() { + if (webView == null) { + IterableLogger.w(TAG, "WebView is null, skipping resize"); + return; + } + + float currentHeight = webView.getContentHeight(); + + // Validate content height + if (currentHeight <= 0) { + IterableLogger.w(TAG, "Invalid content height: " + currentHeight + "dp, skipping resize"); + return; + } + + // Check if height has stabilized (avoid unnecessary resizes for same height) + if (Math.abs(currentHeight - lastContentHeight) < 1.0f) { + IterableLogger.d(TAG, "Content height unchanged (" + currentHeight + "dp), skipping resize"); + return; + } + + lastContentHeight = currentHeight; + + IterableLogger.d( + TAG, + "💚 Resizing in-app to height: " + currentHeight + "dp" + ); + + resize(currentHeight); } /** @@ -462,9 +636,44 @@ public void run() { 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; - RelativeLayout.LayoutParams webViewLayout = new RelativeLayout.LayoutParams(getResources().getDisplayMetrics().widthPixels, (int) relativeHeight); - webView.setLayoutParams(webViewLayout); + int newWebViewWidth = getResources().getDisplayMetrics().widthPixels; + int newWebViewHeight = (int) relativeHeight; + + // Set WebView to explicit size + RelativeLayout.LayoutParams webViewParams = new RelativeLayout.LayoutParams(newWebViewWidth, newWebViewHeight); + + // Apply positioning based on gravity + int resizeGravity = getVerticalLocation(insetPadding); + IterableLogger.d(TAG, "Resizing WebView directly - gravity: " + resizeGravity + " size: " + newWebViewWidth + "x" + newWebViewHeight + "px for inset padding: " + insetPadding); + + if (resizeGravity == Gravity.CENTER_VERTICAL) { + webViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); + IterableLogger.d(TAG, "Applied CENTER_IN_PARENT to WebView"); + } else if (resizeGravity == Gravity.TOP) { + webViewParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + webViewParams.addRule(RelativeLayout.CENTER_HORIZONTAL); + IterableLogger.d(TAG, "Applied TOP alignment to WebView"); + } else if (resizeGravity == Gravity.BOTTOM) { + webViewParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); + webViewParams.addRule(RelativeLayout.CENTER_HORIZONTAL); + IterableLogger.d(TAG, "Applied BOTTOM alignment to WebView"); + } + + // Make dialog full screen to allow proper positioning + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); + + // Apply the new layout params to WebView + webView.setLayoutParams(webViewParams); + + // Force layout updates + webView.requestLayout(); + if (webView.getParent() instanceof ViewGroup) { + ((ViewGroup) webView.getParent()).requestLayout(); + } + + IterableLogger.d(TAG, "Applied explicit size and positioning to WebView: " + newWebViewWidth + "x" + newWebViewHeight); } } catch (IllegalArgumentException e) { IterableLogger.e(TAG, "Exception while trying to resize an in-app message", e); diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebChromeClient.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebChromeClient.java index 7631bbae0..8e630ca84 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebChromeClient.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableWebChromeClient.java @@ -12,6 +12,9 @@ public class IterableWebChromeClient extends WebChromeClient { @Override public void onProgressChanged(WebView view, int newProgress) { - inAppHTMLNotification.runResizeScript(); + // Only trigger resize when page is fully loaded (100%) to avoid multiple rapid calls + if (newProgress == 100) { + inAppHTMLNotification.runResizeScript(); + } } } From 8e15c0a3bb9586c897db1eeb3b86ebc8b20b6fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Wed, 29 Oct 2025 14:05:34 -0700 Subject: [PATCH 2/9] Code refactor --- ...IterableInAppFragmentHTMLNotification.java | 172 ++++++++---------- 1 file changed, 78 insertions(+), 94 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index 69483b175..86fb3d813 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -52,9 +52,12 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem private static final int DELAY_THRESHOLD_MS = 500; - @Nullable static IterableInAppFragmentHTMLNotification notification; - @Nullable static IterableHelper.IterableUrlCallback clickCallback; - @Nullable static IterableInAppLocation location; + @Nullable + static IterableInAppFragmentHTMLNotification notification; + @Nullable + static IterableHelper.IterableUrlCallback clickCallback; + @Nullable + static IterableInAppLocation location; private IterableWebView webView; private boolean loaded; @@ -62,7 +65,7 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem private boolean callbackOnCancel = false; private String htmlString; private String messageId; - + // Resize debouncing fields private Handler resizeHandler = new Handler(); private Runnable pendingResizeRunnable; @@ -99,6 +102,7 @@ public static IterableInAppFragmentHTMLNotification createInstance(@NonNull Stri /** * Returns the notification instance currently being shown + * * @return notification instance */ public static IterableInAppFragmentHTMLNotification getInstance() { @@ -115,33 +119,15 @@ public IterableInAppFragmentHTMLNotification() { insetPadding = new Rect(); this.setStyle(DialogFragment.STYLE_NO_FRAME, androidx.appcompat.R.style.Theme_AppCompat_NoActionBar); } - + @Override public void onStart() { super.onStart(); - + // Set dialog positioning after the dialog is created and shown Dialog dialog = getDialog(); if (dialog != null) { - Window window = dialog.getWindow(); - if (window != null) { - WindowManager.LayoutParams windowParams = window.getAttributes(); - int startGravity = getVerticalLocation(insetPadding); - - if (startGravity == Gravity.CENTER_VERTICAL) { - windowParams.gravity = Gravity.CENTER; - IterableLogger.d(TAG, "Set dialog gravity to CENTER in onStart"); - } else if (startGravity == Gravity.TOP) { - windowParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Set dialog gravity to TOP in onStart"); - } else if (startGravity == Gravity.BOTTOM) { - windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Set dialog gravity to BOTTOM in onStart"); - } - - window.setAttributes(windowParams); - IterableLogger.d(TAG, "Applied window gravity in onStart: " + windowParams.gravity); - } + applyWindowGravity(dialog.getWindow(), "onStart"); } } @@ -183,25 +169,10 @@ public void onCancel(DialogInterface dialog) { } }); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - + // Set window gravity for the dialog - Window window = dialog.getWindow(); - WindowManager.LayoutParams windowParams = window.getAttributes(); - int dialogGravity = getVerticalLocation(insetPadding); - - if (dialogGravity == Gravity.CENTER_VERTICAL) { - windowParams.gravity = Gravity.CENTER; - IterableLogger.d(TAG, "Set dialog gravity to CENTER in onCreateDialog"); - } else if (dialogGravity == Gravity.TOP) { - windowParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Set dialog gravity to TOP in onCreateDialog"); - } else if (dialogGravity == Gravity.BOTTOM) { - windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Set dialog gravity to BOTTOM in onCreateDialog"); - } - - window.setAttributes(windowParams); - + 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) { @@ -220,37 +191,22 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c if (getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN) { getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } - + // Set initial window gravity based on inset padding - Window window = getDialog().getWindow(); - WindowManager.LayoutParams windowParams = window.getAttributes(); - int windowGravity = getVerticalLocation(insetPadding); - - if (windowGravity == Gravity.CENTER_VERTICAL) { - windowParams.gravity = Gravity.CENTER; - IterableLogger.d(TAG, "Set initial CENTER window gravity in onCreateView"); - } else if (windowGravity == Gravity.TOP) { - windowParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Set initial TOP window gravity in onCreateView"); - } else if (windowGravity == Gravity.BOTTOM) { - windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Set initial BOTTOM window gravity in onCreateView"); - } - - window.setAttributes(windowParams); + applyWindowGravity(getDialog().getWindow(), "onCreateView"); webView = new IterableWebView(getContext()); webView.setId(R.id.webView); - + // Debug the HTML content IterableLogger.d(TAG, "HTML content preview: " + (htmlString.length() > 200 ? htmlString.substring(0, 200) + "..." : htmlString)); - + webView.createWithHtml(this, htmlString); if (orientationListener == null) { orientationListener = new OrientationEventListener(getContext(), SensorManager.SENSOR_DELAY_NORMAL) { private int lastOrientation = -1; - + // Resize the webView on device rotation public void onOrientationChanged(int orientation) { if (loaded && webView != null) { @@ -258,7 +214,7 @@ public void onOrientationChanged(int orientation) { int currentOrientation = ((orientation + 45) / 90) * 90; if (currentOrientation != lastOrientation && lastOrientation != -1) { lastOrientation = currentOrientation; - + // Use longer delay for orientation changes to allow layout to stabilize final Handler handler = new Handler(); handler.postDelayed(new Runnable() { @@ -280,19 +236,19 @@ public void run() { // Create a FrameLayout as the main container for better positioning control FrameLayout frameLayout = new FrameLayout(this.getContext()); - + // Create a RelativeLayout as a wrapper for the WebView RelativeLayout webViewContainer = new RelativeLayout(this.getContext()); - + int gravity = getVerticalLocation(insetPadding); IterableLogger.d(TAG, "Initial setup - gravity: " + gravity + " for inset padding: " + insetPadding); - + // Set FrameLayout gravity based on positioning FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ); - + if (gravity == Gravity.CENTER_VERTICAL) { containerParams.gravity = Gravity.CENTER; IterableLogger.d(TAG, "Applied CENTER gravity to container"); @@ -303,20 +259,20 @@ public void run() { containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; IterableLogger.d(TAG, "Applied BOTTOM gravity to container"); } - + // Add WebView to the RelativeLayout container with WRAP_CONTENT for proper sizing RelativeLayout.LayoutParams webViewParams = new RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT ); webViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); webViewContainer.addView(webView, webViewParams); - + IterableLogger.d(TAG, "Added WebView with WRAP_CONTENT and CENTER_IN_PARENT rule"); - + // Add the container to the FrameLayout frameLayout.addView(webViewContainer, containerParams); - + IterableLogger.d(TAG, "Created FrameLayout with positioned RelativeLayout container"); if (savedInstanceState == null || !savedInstanceState.getBoolean(IN_APP_OPEN_TRACKED, false)) { @@ -519,7 +475,7 @@ private void hideWebView() { try { Animation anim = AnimationUtils.loadAnimation(getContext(), - animationResource); + animationResource); anim.setDuration(IterableConstants.ITERABLE_IN_APP_ANIMATION_DURATION); webView.startAnimation(anim); } catch (Exception e) { @@ -563,7 +519,7 @@ public void runResizeScript() { if (pendingResizeRunnable != null) { resizeHandler.removeCallbacks(pendingResizeRunnable); } - + // Schedule a debounced resize operation pendingResizeRunnable = new Runnable() { @Override @@ -571,42 +527,43 @@ public void run() { performResizeWithValidation(); } }; - + resizeHandler.postDelayed(pendingResizeRunnable, RESIZE_DEBOUNCE_DELAY_MS); } - + private void performResizeWithValidation() { if (webView == null) { IterableLogger.w(TAG, "WebView is null, skipping resize"); return; } - + float currentHeight = webView.getContentHeight(); - + // Validate content height if (currentHeight <= 0) { IterableLogger.w(TAG, "Invalid content height: " + currentHeight + "dp, skipping resize"); return; } - + // Check if height has stabilized (avoid unnecessary resizes for same height) if (Math.abs(currentHeight - lastContentHeight) < 1.0f) { IterableLogger.d(TAG, "Content height unchanged (" + currentHeight + "dp), skipping resize"); return; } - + lastContentHeight = currentHeight; - + IterableLogger.d( - TAG, - "💚 Resizing in-app to height: " + currentHeight + "dp" + TAG, + "💚 Resizing in-app to height: " + currentHeight + "dp" ); - + resize(currentHeight); } /** * Resizes the dialog window based upon the size of its webView HTML content + * * @param height */ public void resize(final float height) { @@ -621,7 +578,7 @@ public void run() { try { // Since this is run asynchronously, notification might've been dismissed already if (getContext() == null || notification == null || notification.getDialog() == null || - notification.getDialog().getWindow() == null || !notification.getDialog().isShowing()) { + notification.getDialog().getWindow() == null || !notification.getDialog().isShowing()) { return; } @@ -654,14 +611,14 @@ public void run() { float relativeHeight = height * getResources().getDisplayMetrics().density; int newWebViewWidth = getResources().getDisplayMetrics().widthPixels; int newWebViewHeight = (int) relativeHeight; - + // Set WebView to explicit size RelativeLayout.LayoutParams webViewParams = new RelativeLayout.LayoutParams(newWebViewWidth, newWebViewHeight); - + // Apply positioning based on gravity int resizeGravity = getVerticalLocation(insetPadding); IterableLogger.d(TAG, "Resizing WebView directly - gravity: " + resizeGravity + " size: " + newWebViewWidth + "x" + newWebViewHeight + "px for inset padding: " + insetPadding); - + if (resizeGravity == Gravity.CENTER_VERTICAL) { webViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); IterableLogger.d(TAG, "Applied CENTER_IN_PARENT to WebView"); @@ -674,19 +631,19 @@ public void run() { webViewParams.addRule(RelativeLayout.CENTER_HORIZONTAL); IterableLogger.d(TAG, "Applied BOTTOM alignment to WebView"); } - + // Make dialog full screen to allow proper positioning window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); - + // Apply the new layout params to WebView webView.setLayoutParams(webViewParams); - + // Force layout updates webView.requestLayout(); if (webView.getParent() instanceof ViewGroup) { ((ViewGroup) webView.getParent()).requestLayout(); } - + IterableLogger.d(TAG, "Applied explicit size and positioning to WebView: " + newWebViewWidth + "x" + newWebViewHeight); } } catch (IllegalArgumentException e) { @@ -698,6 +655,7 @@ public void run() { /** * Returns the vertical position of the dialog for the given padding + * * @param padding * @return */ @@ -711,6 +669,32 @@ int getVerticalLocation(Rect padding) { return gravity; } + /** + * Sets the window gravity based on inset padding + * + * @param window The dialog window to configure + * @param context Debug context string for logging + */ + private void applyWindowGravity(Window window, String context) { + if (window == null) { + return; + } + + WindowManager.LayoutParams windowParams = window.getAttributes(); + int gravity = getVerticalLocation(insetPadding); + + if (gravity == Gravity.CENTER_VERTICAL) { + windowParams.gravity = Gravity.CENTER; + } else if (gravity == Gravity.TOP) { + windowParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + } else if (gravity == Gravity.BOTTOM) { + windowParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + } + + window.setAttributes(windowParams); + IterableLogger.d(TAG, "Set window gravity in " + context + ": " + windowParams.gravity); + } + InAppLayout getInAppLayout(Rect padding) { if (padding.top == 0 && padding.bottom == 0) { return InAppLayout.FULLSCREEN; From e85d78a51c2c6a9f19eda783b36287d55bfcfe72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Thu, 30 Oct 2025 13:14:44 -0700 Subject: [PATCH 3/9] Possible flaky test fix? And removed unnecessary logging --- .../iterable/iterableapi/IterableApiResponseTest.java | 10 ++++++++-- .../IterableInAppFragmentHTMLNotification.java | 11 ----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java index 021c70b1d..afab7676b 100644 --- a/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java +++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/IterableApiResponseTest.java @@ -37,8 +37,14 @@ public class IterableApiResponseTest { private MockWebServer server; @Before - public void setUp() { + public void setUp() throws IOException { server = new MockWebServer(); + // Explicitly start the server to ensure it's ready + try { + server.start(); + } catch (IllegalStateException e) { + // Server may already be started by url() call below, which is fine + } IterableApi.overrideURLEndpointPath(server.url("").toString()); createIterableApi(); } @@ -138,7 +144,7 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) { new IterableRequestTask().execute(request); server.takeRequest(5, TimeUnit.SECONDS); - assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS)); + assertTrue("onFailure is called", signal.await(5, TimeUnit.SECONDS)); } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index 86fb3d813..468f1da23 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -198,9 +198,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c webView = new IterableWebView(getContext()); webView.setId(R.id.webView); - // Debug the HTML content - IterableLogger.d(TAG, "HTML content preview: " + (htmlString.length() > 200 ? htmlString.substring(0, 200) + "..." : htmlString)); - webView.createWithHtml(this, htmlString); if (orientationListener == null) { @@ -241,7 +238,6 @@ public void run() { RelativeLayout webViewContainer = new RelativeLayout(this.getContext()); int gravity = getVerticalLocation(insetPadding); - IterableLogger.d(TAG, "Initial setup - gravity: " + gravity + " for inset padding: " + insetPadding); // Set FrameLayout gravity based on positioning FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams( @@ -251,13 +247,10 @@ public void run() { if (gravity == Gravity.CENTER_VERTICAL) { containerParams.gravity = Gravity.CENTER; - IterableLogger.d(TAG, "Applied CENTER gravity to container"); } else if (gravity == Gravity.TOP) { containerParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Applied TOP gravity to container"); } else if (gravity == Gravity.BOTTOM) { containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; - IterableLogger.d(TAG, "Applied BOTTOM gravity to container"); } // Add WebView to the RelativeLayout container with WRAP_CONTENT for proper sizing @@ -268,13 +261,9 @@ public void run() { webViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); webViewContainer.addView(webView, webViewParams); - IterableLogger.d(TAG, "Added WebView with WRAP_CONTENT and CENTER_IN_PARENT rule"); - // Add the container to the FrameLayout frameLayout.addView(webViewContainer, containerParams); - IterableLogger.d(TAG, "Created FrameLayout with positioned RelativeLayout container"); - if (savedInstanceState == null || !savedInstanceState.getBoolean(IN_APP_OPEN_TRACKED, false)) { IterableApi.sharedInstance.trackInAppOpen(messageId, location); } From a1e5ab9791e3ae27121b8142a80fe0fa992639e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Mon, 24 Nov 2025 20:08:45 -0800 Subject: [PATCH 4/9] Adding tests --- .../IterableInAppHTMLNotificationTest.java | 266 +++++++++++++++++- .../IterableWebChromeClientTest.java | 104 +++++++ 2 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java index 39b9526b6..9eb0882ad 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java @@ -1,23 +1,285 @@ package com.iterable.iterableapi; import android.graphics.Rect; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; import androidx.fragment.app.FragmentActivity; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.robolectric.Robolectric; import org.robolectric.android.controller.ActivityController; +import org.robolectric.shadows.ShadowDialog; +import org.robolectric.shadows.ShadowLooper; + +import static android.os.Looper.getMainLooper; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; public class IterableInAppHTMLNotificationTest extends BaseTest { + private ActivityController controller; + private FragmentActivity activity; + + @Before + public void setUp() { + IterableInAppFragmentHTMLNotification.notification = null; + IterableTestUtils.createIterableApiNew(); + controller = Robolectric.buildActivity(FragmentActivity.class).create().start().resume(); + activity = controller.get(); + } + + @After + public void tearDown() { + if (controller != null) { + if (ShadowDialog.getLatestDialog() != null) { + ShadowDialog.getLatestDialog().dismiss(); + } + controller.pause().stop().destroy(); + } + IterableInAppFragmentHTMLNotification.notification = null; + IterableTestUtils.resetIterableApi(); + } + @Test public void testDoNotCrashOnResizeAfterDismiss() { - ActivityController controller = Robolectric.buildActivity(FragmentActivity.class).create().start().resume(); - FragmentActivity activity = controller.get(); IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance(); notification.dismiss(); notification.resize(500.0f); } + + // ===== Resize Debouncing Tests ===== + + @Test + public void testResizeDebouncing_MultipleRapidCalls() { + IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "Test", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); + shadowOf(getMainLooper()).idle(); + + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance(); + assertNotNull(notification); + notification.setLoaded(true); // Mark as loaded so WebView content height can be retrieved + + // Test that multiple rapid calls to runResizeScript don't crash + // The debouncing mechanism should handle this gracefully + notification.runResizeScript(); + notification.runResizeScript(); + notification.runResizeScript(); + + // Process all pending tasks including delayed ones (200ms debounce) + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + // Test passes if no exceptions are thrown - debouncing works correctly + // Note: resize may not execute if WebView content height is invalid (validation), + // but the debouncing mechanism should still prevent multiple rapid executions + } + + @Test + public void testResizeDebouncing_CancelsPendingResize() { + IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "Test", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); + shadowOf(getMainLooper()).idle(); + + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance(); + assertNotNull(notification); + notification.setLoaded(true); // Mark as loaded so WebView content height can be retrieved + + // First call + notification.runResizeScript(); + shadowOf(getMainLooper()).idle(); + + // Second call should cancel the first (before 200ms debounce delay) + notification.runResizeScript(); + + // Process all pending tasks including delayed ones + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + // Test passes if no exceptions are thrown - cancellation works correctly + // The debouncing mechanism should cancel the first pending resize + } + + // ===== Resize Validation Tests ===== + + @Test + public void testResizeValidation_NullWebView() { + IterableInAppFragmentHTMLNotification notification = new IterableInAppFragmentHTMLNotification(); + // WebView is null initially, so performResizeWithValidation should handle it gracefully + // We can't directly call performResizeWithValidation as it's private, but we can test via runResizeScript + notification.runResizeScript(); + shadowOf(getMainLooper()).idle(); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + // Should not crash - validation should skip when webView is null + } + + @Test + public void testResizeValidation_HandlesGracefully() { + // Test that resize validation works through the public API + IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "Test", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); + shadowOf(getMainLooper()).idle(); + + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance(); + assertNotNull(notification); + + // Test that runResizeScript can be called multiple times without crashing + // The validation logic will handle invalid heights internally + notification.runResizeScript(); + shadowOf(getMainLooper()).idle(); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + // Should not crash + } + + // ===== Window Gravity Tests ===== + + @Test + public void testApplyWindowGravity_Center() { + Rect padding = new Rect(0, -1, 0, -1); // Center padding + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( + "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + + // Verify gravity calculation for center padding + assertEquals(Gravity.CENTER_VERTICAL, notification.getVerticalLocation(padding)); + } + + @Test + public void testApplyWindowGravity_Top() { + Rect padding = new Rect(0, 0, 0, -1); // Top padding + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( + "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + + assertEquals(Gravity.TOP, notification.getVerticalLocation(padding)); + } + + @Test + public void testApplyWindowGravity_Bottom() { + Rect padding = new Rect(0, -1, 0, 0); // Bottom padding + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( + "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + + assertEquals(Gravity.BOTTOM, notification.getVerticalLocation(padding)); + } + + @Test + public void testApplyWindowGravity_HandlesNullWindow() { + Rect padding = new Rect(0, 0, 0, -1); + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( + "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + + // Test that getVerticalLocation works correctly + assertEquals(Gravity.TOP, notification.getVerticalLocation(padding)); + // applyWindowGravity is private but is called in onStart/onCreateDialog/onCreateView + // and should handle null window gracefully + } + + // ===== Layout Structure Tests ===== + + @Test + public void testLayoutStructure_FrameLayoutRoot() { + IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "Test", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); + shadowOf(getMainLooper()).idle(); + + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance(); + assertNotNull(notification); + + // Verify the view hierarchy uses FrameLayout as root + // This is tested indirectly through the layout creation + assertNotNull(notification.getView()); + assertTrue(notification.getView() instanceof FrameLayout); + } + + @Test + public void testLayoutStructure_RelativeLayoutWrapper() { + IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "Test", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); + shadowOf(getMainLooper()).idle(); + + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance(); + assertNotNull(notification); + + ViewGroup rootView = (ViewGroup) notification.getView(); + assertNotNull(rootView); + + // FrameLayout should contain a RelativeLayout + if (rootView.getChildCount() > 0) { + ViewGroup child = (ViewGroup) rootView.getChildAt(0); + assertTrue(child instanceof RelativeLayout); + } + } + + @Test + public void testLayoutStructure_GravityApplied() { + 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); + + ViewGroup rootView = (ViewGroup) notification.getView(); + assertNotNull(rootView); + + // Verify FrameLayout has child with gravity set + if (rootView.getChildCount() > 0) { + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) rootView.getChildAt(0).getLayoutParams(); + assertNotNull(params); + // Gravity should be set based on padding (TOP in this case) + assertEquals(Gravity.TOP | Gravity.CENTER_HORIZONTAL, params.gravity); + } + } + + // ===== Orientation Change Tests ===== + + @Test + public void testOrientationChange_PortraitToLandscape() throws Exception { + IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "Test", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); + shadowOf(getMainLooper()).idle(); + + IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.getInstance(); + assertNotNull(notification); + notification.setLoaded(true); // Mark as loaded to enable orientation listener + + // Use reflection to access the private orientationListener field + java.lang.reflect.Field orientationListenerField = IterableInAppFragmentHTMLNotification.class.getDeclaredField("orientationListener"); + orientationListenerField.setAccessible(true); + android.view.OrientationEventListener orientationListener = (android.view.OrientationEventListener) orientationListenerField.get(notification); + assertNotNull("Orientation listener should be initialized", orientationListener); + + // Simulate portrait orientation (0 degrees) - first call sets lastOrientation + // The listener only triggers on changes, so we need to set the initial state + orientationListener.onOrientationChanged(0); + shadowOf(getMainLooper()).idle(); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + // Verify that no resize was triggered on initial orientation (lastOrientation == -1) + // The listener should not trigger resize on the first orientation reading + // We verify this indirectly by ensuring no exceptions were thrown + + // Now simulate landscape orientation (90 degrees) - this should trigger resize + // Portrait = 0°, Landscape = 90° or 270° + orientationListener.onOrientationChanged(90); + shadowOf(getMainLooper()).idle(); + + // The orientation change triggers a delayed resize (1500ms delay) + // Process all pending tasks including the delayed resize + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + // Verify that the orientation change mechanism worked correctly + // The test passes if no exceptions are thrown and the resize was triggered + // We verify indirectly by ensuring the notification still exists and is in a valid state + assertNotNull("Notification should still exist after orientation change", notification); + + // The resize should have been triggered (1500ms delay + 200ms debounce) + // If runResizeScript wasn't called, we would have seen validation errors or exceptions + // The fact that we get here without exceptions means the orientation change handling worked + } } diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java new file mode 100644 index 000000000..777e90b5e --- /dev/null +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java @@ -0,0 +1,104 @@ +package com.iterable.iterableapi; + +import android.webkit.WebView; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for IterableWebChromeClient + * Verifies that resize is only triggered when page is fully loaded (100% progress) + */ +public class IterableWebChromeClientTest extends BaseTest { + + @Mock + private IterableWebView.HTMLNotificationCallbacks mockCallbacks; + + private IterableWebChromeClient webChromeClient; + private WebView mockWebView; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + webChromeClient = new IterableWebChromeClient(mockCallbacks); + mockWebView = mock(WebView.class); + } + + @Test + public void testOnProgressChanged_TriggersResizeAt100Percent() { + // Test: Resize should be triggered when progress reaches 100% + webChromeClient.onProgressChanged(mockWebView, 100); + + verify(mockCallbacks, times(1)).runResizeScript(); + } + + @Test + public void testOnProgressChanged_DoesNotTriggerBelow100Percent() { + // Test: Resize should NOT be triggered for progress < 100% + webChromeClient.onProgressChanged(mockWebView, 0); + webChromeClient.onProgressChanged(mockWebView, 50); + webChromeClient.onProgressChanged(mockWebView, 99); + + verify(mockCallbacks, never()).runResizeScript(); + } + + @Test + public void testOnProgressChanged_MultipleProgressUpdates() { + // Test: Multiple progress updates should only trigger resize once at 100% + webChromeClient.onProgressChanged(mockWebView, 0); + webChromeClient.onProgressChanged(mockWebView, 25); + webChromeClient.onProgressChanged(mockWebView, 50); + webChromeClient.onProgressChanged(mockWebView, 75); + webChromeClient.onProgressChanged(mockWebView, 99); + webChromeClient.onProgressChanged(mockWebView, 100); + + // Should only be called once at 100% + verify(mockCallbacks, times(1)).runResizeScript(); + } + + @Test + public void testOnProgressChanged_Multiple100PercentCalls() { + // Test: If 100% is called multiple times, resize should be called each time + // (though this is unlikely in practice, we test the behavior) + webChromeClient.onProgressChanged(mockWebView, 100); + webChromeClient.onProgressChanged(mockWebView, 100); + webChromeClient.onProgressChanged(mockWebView, 100); + + verify(mockCallbacks, times(3)).runResizeScript(); + } + + @Test + public void testOnProgressChanged_ProgressSequence() { + // Test: Realistic progress sequence from 0 to 100 + int[] progressValues = {0, 10, 25, 50, 75, 90, 95, 99, 100}; + + for (int progress : progressValues) { + webChromeClient.onProgressChanged(mockWebView, progress); + } + + // Should only be called once at 100% + verify(mockCallbacks, times(1)).runResizeScript(); + } + + @Test + public void testOnProgressChanged_EdgeCases() { + // Test: Edge cases for progress values + webChromeClient.onProgressChanged(mockWebView, -1); // Invalid negative + webChromeClient.onProgressChanged(mockWebView, 101); // Invalid > 100 + webChromeClient.onProgressChanged(mockWebView, 100); // Valid 100% + + // Only 100% should trigger + verify(mockCallbacks, times(1)).runResizeScript(); + } +} + From 863f28198681eb82dc2aa6432518244fc6edbf6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Mon, 24 Nov 2025 23:24:58 -0800 Subject: [PATCH 5/9] Full Screen InApps to avoid FrameLayout Updated the tests --- ...IterableInAppFragmentHTMLNotification.java | 98 ++++++++++++------- .../IterableInAppHTMLNotificationTest.java | 35 +++++-- 2 files changed, 89 insertions(+), 44 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index 468f1da23..90e87d806 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -124,9 +124,9 @@ public IterableInAppFragmentHTMLNotification() { public void onStart() { super.onStart(); - // Set dialog positioning after the dialog is created and shown + // Set dialog positioning after the dialog is created and shown (only for non-fullscreen) Dialog dialog = getDialog(); - if (dialog != null) { + if (dialog != null && getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) { applyWindowGravity(dialog.getWindow(), "onStart"); } } @@ -170,8 +170,10 @@ public void onCancel(DialogInterface dialog) { }); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - // Set window gravity for the dialog - applyWindowGravity(dialog.getWindow(), "onCreateDialog"); + // Set window gravity for the dialog (only for non-fullscreen) + if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) { + applyWindowGravity(dialog.getWindow(), "onCreateDialog"); + } if (getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN) { dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); @@ -192,8 +194,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c getDialog().getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } - // Set initial window gravity based on inset padding - applyWindowGravity(getDialog().getWindow(), "onCreateView"); + // Set initial window gravity based on inset padding (only for non-fullscreen) + if (getInAppLayout(insetPadding) != InAppLayout.FULLSCREEN) { + applyWindowGravity(getDialog().getWindow(), "onCreateView"); + } webView = new IterableWebView(getContext()); webView.setId(R.id.webView); @@ -234,36 +238,51 @@ public void run() { // Create a FrameLayout as the main container for better positioning control FrameLayout frameLayout = new FrameLayout(this.getContext()); - // Create a RelativeLayout as a wrapper for the WebView - RelativeLayout webViewContainer = new RelativeLayout(this.getContext()); - - int gravity = getVerticalLocation(insetPadding); + // Check if this is a full screen in-app + InAppLayout inAppLayout = getInAppLayout(insetPadding); + boolean isFullScreen = (inAppLayout == InAppLayout.FULLSCREEN); + + if (isFullScreen) { + // For full screen in-apps, use MATCH_PARENT for both container and WebView + // Use FrameLayout.LayoutParams for direct child of FrameLayout + FrameLayout.LayoutParams webViewParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ); + frameLayout.addView(webView, webViewParams); + } else { + // For non-fullscreen in-apps, use the new layout structure with positioning + // Create a RelativeLayout as a wrapper for the WebView + RelativeLayout webViewContainer = new RelativeLayout(this.getContext()); + + int gravity = getVerticalLocation(insetPadding); + + // Set FrameLayout gravity based on positioning + FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ); + + if (gravity == Gravity.CENTER_VERTICAL) { + containerParams.gravity = Gravity.CENTER; + } else if (gravity == Gravity.TOP) { + containerParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + } else if (gravity == Gravity.BOTTOM) { + containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + } - // Set FrameLayout gravity based on positioning - FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT - ); + // Add WebView to the RelativeLayout container with WRAP_CONTENT for proper sizing + RelativeLayout.LayoutParams webViewParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT + ); + webViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); + webViewContainer.addView(webView, webViewParams); - if (gravity == Gravity.CENTER_VERTICAL) { - containerParams.gravity = Gravity.CENTER; - } else if (gravity == Gravity.TOP) { - containerParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - } else if (gravity == Gravity.BOTTOM) { - containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + // Add the container to the FrameLayout + frameLayout.addView(webViewContainer, containerParams); } - // Add WebView to the RelativeLayout container with WRAP_CONTENT for proper sizing - RelativeLayout.LayoutParams webViewParams = new RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.WRAP_CONTENT, - RelativeLayout.LayoutParams.WRAP_CONTENT - ); - webViewParams.addRule(RelativeLayout.CENTER_IN_PARENT); - webViewContainer.addView(webView, webViewParams); - - // Add the container to the FrameLayout - frameLayout.addView(webViewContainer, containerParams); - if (savedInstanceState == null || !savedInstanceState.getBoolean(IN_APP_OPEN_TRACKED, false)) { IterableApi.sharedInstance.trackInAppOpen(messageId, location); } @@ -275,12 +294,15 @@ 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 - ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> { - Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(0, sysBars.top, 0, sysBars.bottom); - return insets; - }); + // 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) { + ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> { + Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(0, sysBars.top, 0, sysBars.bottom); + return insets; + }); + } } public void setLoaded(boolean loaded) { diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java index 9eb0882ad..a97ca768e 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java @@ -200,7 +200,10 @@ public void testLayoutStructure_FrameLayoutRoot() { @Test public void testLayoutStructure_RelativeLayoutWrapper() { - IterableInAppDisplayer.showIterableFragmentNotificationHTML(activity, "Test", "", null, 0.0, new Rect(), true, new IterableInAppMessage.InAppBgColor(null, 0.0f), false, IterableInAppLocation.IN_APP); + // Use non-fullscreen padding (top padding) to test RelativeLayout wrapper + // Full screen in-apps don't use RelativeLayout wrapper, only non-fullscreen ones do + 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(); @@ -209,11 +212,31 @@ public void testLayoutStructure_RelativeLayoutWrapper() { ViewGroup rootView = (ViewGroup) notification.getView(); assertNotNull(rootView); - // FrameLayout should contain a RelativeLayout - if (rootView.getChildCount() > 0) { - ViewGroup child = (ViewGroup) rootView.getChildAt(0); - assertTrue(child instanceof RelativeLayout); - } + // For non-fullscreen in-apps, FrameLayout should contain a RelativeLayout wrapper + assertTrue("Root view should have children", rootView.getChildCount() > 0); + ViewGroup child = (ViewGroup) rootView.getChildAt(0); + assertTrue("First child should be RelativeLayout for non-fullscreen in-apps", child instanceof RelativeLayout); + } + + @Test + public void testLayoutStructure_FullScreenNoRelativeLayoutWrapper() { + // Test that full screen in-apps don't use RelativeLayout wrapper + Rect fullScreenPadding = new Rect(0, 0, 0, 0); // Full screen + 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); + + ViewGroup rootView = (ViewGroup) notification.getView(); + assertNotNull(rootView); + + // For full screen in-apps, FrameLayout should contain WebView directly (no RelativeLayout wrapper) + assertTrue("Root view should have children", rootView.getChildCount() > 0); + ViewGroup child = (ViewGroup) rootView.getChildAt(0); + // WebView is not a ViewGroup, so we check it's not a RelativeLayout + assertTrue("First child should be WebView (not RelativeLayout) for full screen in-apps", + !(child instanceof RelativeLayout)); } @Test From 598a159b0d0a91e280f89b9bbfb5571c6095312e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Mon, 24 Nov 2025 23:31:30 -0800 Subject: [PATCH 6/9] Adding github PR review suggestion To not use new Handler() --- .../IterableInAppFragmentHTMLNotification.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index 90e87d806..118658d26 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -15,6 +15,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.util.DisplayMetrics; import android.view.Display; import android.view.Gravity; @@ -67,7 +68,7 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem private String messageId; // Resize debouncing fields - private Handler resizeHandler = new Handler(); + private Handler resizeHandler; private Runnable pendingResizeRunnable; private float lastContentHeight = -1; private static final int RESIZE_DEBOUNCE_DELAY_MS = 200; @@ -217,7 +218,7 @@ public void onOrientationChanged(int orientation) { lastOrientation = currentOrientation; // Use longer delay for orientation changes to allow layout to stabilize - final Handler handler = new Handler(); + final Handler handler = new Handler(Looper.getMainLooper()); handler.postDelayed(new Runnable() { @Override public void run() { @@ -335,8 +336,9 @@ public void onDestroy() { // Clean up pending resize operations if (resizeHandler != null && pendingResizeRunnable != null) { resizeHandler.removeCallbacks(pendingResizeRunnable); - pendingResizeRunnable = null; } + pendingResizeRunnable = null; + resizeHandler = null; if (this.getActivity() != null && this.getActivity().isChangingConfigurations()) { return; @@ -526,6 +528,11 @@ private void processMessageRemoval() { @Override public void runResizeScript() { + // Initialize handler lazily with main looper to avoid Looper issues in tests + if (resizeHandler == null) { + resizeHandler = new Handler(Looper.getMainLooper()); + } + // Cancel any pending resize operation if (pendingResizeRunnable != null) { resizeHandler.removeCallbacks(pendingResizeRunnable); From 3e4dcd15dfda573df3b31a9ea17e13b8317d3611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Tue, 25 Nov 2025 02:19:00 -0800 Subject: [PATCH 7/9] orientation listener calculation code testing --- ...IterableInAppFragmentHTMLNotification.java | 16 +++- .../IterableInAppHTMLNotificationTest.java | 78 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index 118658d26..de1235833 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -213,7 +213,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onOrientationChanged(int orientation) { if (loaded && webView != null) { // Only trigger on significant orientation changes (90 degree increments) - int currentOrientation = ((orientation + 45) / 90) * 90; + int currentOrientation = roundToNearest90Degrees(orientation); if (currentOrientation != lastOrientation && lastOrientation != -1) { lastOrientation = currentOrientation; @@ -687,6 +687,20 @@ int getVerticalLocation(Rect padding) { return gravity; } + /** + * Rounds an orientation value to the nearest 90-degree increment. + * This is used to detect significant orientation changes (portrait/landscape). + * + * The calculation uses integer division: ((orientation + 45) / 90) * 90 + * This rounds to the nearest multiple of 90 by adding 45 before dividing. + * + * @param orientation The orientation value in degrees (typically 0-359 from OrientationEventListener) + * @return The orientation rounded to the nearest 90-degree increment (0, 90, 180, 270, or 360) + */ + static int roundToNearest90Degrees(int orientation) { + return ((orientation + 45) / 90) * 90; + } + /** * Sets the window gravity based on inset padding * diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java index a97ca768e..2f2ce044f 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java @@ -305,4 +305,82 @@ public void testOrientationChange_PortraitToLandscape() throws Exception { // If runResizeScript wasn't called, we would have seen validation errors or exceptions // The fact that we get here without exceptions means the orientation change handling worked } + + // ===== Orientation Rounding Tests ===== + + @Test + public void testRoundToNearest90Degrees_Zero() { + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(0)); + } + + @Test + public void testRoundToNearest90Degrees_StandardOrientations() { + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(0)); + assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(90)); + assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(180)); + assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(270)); + assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(360)); + } + + @Test + public void testRoundToNearest90Degrees_BoundaryValues() { + // Values that round down to 0 (0-44) + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(44)); + + // Values that round up to 90 (45-134) + assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(45)); + assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(89)); + assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(90)); + assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(134)); + + // Values that round up to 180 (135-224) + assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(135)); + assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(180)); + assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(224)); + + // Values that round up to 270 (225-314) + assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(225)); + assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(270)); + assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(314)); + + // Values that round up to 360 (315-359) + assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(315)); + assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(359)); + } + + @Test + public void testRoundToNearest90Degrees_NearZero() { + // Test values very close to 0 + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(1)); + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-1)); + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-44)); + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-45)); + } + + @Test + public void testRoundToNearest90Degrees_NegativeValues() { + // Test negative values (though OrientationEventListener typically returns 0-359) + // These test the integer division behavior with negative numbers + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-1)); + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-45)); + assertEquals(-90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-46)); + assertEquals(-90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-90)); + assertEquals(-90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-134)); + assertEquals(-180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-135)); + } + + @Test + public void testRoundToNearest90Degrees_EdgeCases() { + // Test edge cases around boundaries + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(0)); + assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(44)); + assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(45)); + assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(134)); + assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(135)); + assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(224)); + assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(225)); + assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(314)); + assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(315)); + assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(359)); + } } From 38bfdfc5207c318d2ff7ccdfc256957d7cd7d59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Tue, 25 Nov 2025 08:33:33 -0800 Subject: [PATCH 8/9] Correction after github checks --- ...IterableInAppFragmentHTMLNotification.java | 6 ++-- .../IterableInAppHTMLNotificationTest.java | 32 +++++++++---------- .../IterableWebChromeClientTest.java | 3 -- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index de1235833..4ac3c19d4 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -691,14 +691,14 @@ int getVerticalLocation(Rect padding) { * Rounds an orientation value to the nearest 90-degree increment. * This is used to detect significant orientation changes (portrait/landscape). * - * The calculation uses integer division: ((orientation + 45) / 90) * 90 - * This rounds to the nearest multiple of 90 by adding 45 before dividing. + * The calculation rounds to the nearest multiple of 90 by adding 45 before dividing. + * Uses floating point division to correctly handle negative numbers. * * @param orientation The orientation value in degrees (typically 0-359 from OrientationEventListener) * @return The orientation rounded to the nearest 90-degree increment (0, 90, 180, 270, or 360) */ static int roundToNearest90Degrees(int orientation) { - return ((orientation + 45) / 90) * 90; + return (int) (Math.round((orientation + 45.0) / 90.0) * 90); } /** diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java index 2f2ce044f..514cfe412 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java @@ -20,12 +20,6 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; public class IterableInAppHTMLNotificationTest extends BaseTest { @@ -146,7 +140,8 @@ public void testResizeValidation_HandlesGracefully() { public void testApplyWindowGravity_Center() { Rect padding = new Rect(0, -1, 0, -1); // Center padding IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( - "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + "Test", false, uri -> { + }, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); // Verify gravity calculation for center padding assertEquals(Gravity.CENTER_VERTICAL, notification.getVerticalLocation(padding)); @@ -156,7 +151,8 @@ public void testApplyWindowGravity_Center() { public void testApplyWindowGravity_Top() { Rect padding = new Rect(0, 0, 0, -1); // Top padding IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( - "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + "Test", false, uri -> { + }, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); assertEquals(Gravity.TOP, notification.getVerticalLocation(padding)); } @@ -165,7 +161,8 @@ public void testApplyWindowGravity_Top() { public void testApplyWindowGravity_Bottom() { Rect padding = new Rect(0, -1, 0, 0); // Bottom padding IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( - "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + "Test", false, uri -> { + }, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); assertEquals(Gravity.BOTTOM, notification.getVerticalLocation(padding)); } @@ -174,8 +171,9 @@ public void testApplyWindowGravity_Bottom() { public void testApplyWindowGravity_HandlesNullWindow() { Rect padding = new Rect(0, 0, 0, -1); IterableInAppFragmentHTMLNotification notification = IterableInAppFragmentHTMLNotification.createInstance( - "Test", false, uri -> {}, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); - + "Test", false, uri -> { + }, IterableInAppLocation.IN_APP, "msg1", 0.0, padding, false, new IterableInAppMessage.InAppBgColor(null, 0.0f)); + // Test that getVerticalLocation works correctly assertEquals(Gravity.TOP, notification.getVerticalLocation(padding)); // applyWindowGravity is private but is called in onStart/onCreateDialog/onCreateView @@ -235,7 +233,7 @@ public void testLayoutStructure_FullScreenNoRelativeLayoutWrapper() { assertTrue("Root view should have children", rootView.getChildCount() > 0); ViewGroup child = (ViewGroup) rootView.getChildAt(0); // WebView is not a ViewGroup, so we check it's not a RelativeLayout - assertTrue("First child should be WebView (not RelativeLayout) for full screen in-apps", + assertTrue("First child should be WebView (not RelativeLayout) for full screen in-apps", !(child instanceof RelativeLayout)); } @@ -300,7 +298,7 @@ public void testOrientationChange_PortraitToLandscape() throws Exception { // The test passes if no exceptions are thrown and the resize was triggered // We verify indirectly by ensuring the notification still exists and is in a valid state assertNotNull("Notification should still exist after orientation change", notification); - + // The resize should have been triggered (1500ms delay + 200ms debounce) // If runResizeScript wasn't called, we would have seen validation errors or exceptions // The fact that we get here without exceptions means the orientation change handling worked @@ -326,23 +324,23 @@ public void testRoundToNearest90Degrees_StandardOrientations() { public void testRoundToNearest90Degrees_BoundaryValues() { // Values that round down to 0 (0-44) assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(44)); - + // Values that round up to 90 (45-134) assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(45)); assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(89)); assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(90)); assertEquals(90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(134)); - + // Values that round up to 180 (135-224) assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(135)); assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(180)); assertEquals(180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(224)); - + // Values that round up to 270 (225-314) assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(225)); assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(270)); assertEquals(270, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(314)); - + // Values that round up to 360 (315-359) assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(315)); assertEquals(360, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(359)); diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java index 777e90b5e..2ea8d2c88 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableWebChromeClientTest.java @@ -4,14 +4,11 @@ import org.junit.Before; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; From 04852ae1416a1cfd9ea6b0b6584d86b11dc4f12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkshay?= <“ayyanchira.akshay@gmail.com”> Date: Tue, 25 Nov 2025 15:48:23 -0800 Subject: [PATCH 9/9] Test fix --- .../IterableInAppFragmentHTMLNotification.java | 13 +++++++++++-- .../IterableInAppHTMLNotificationTest.java | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java index 4ac3c19d4..b6b9a99f6 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppFragmentHTMLNotification.java @@ -692,13 +692,22 @@ int getVerticalLocation(Rect padding) { * This is used to detect significant orientation changes (portrait/landscape). * * The calculation rounds to the nearest multiple of 90 by adding 45 before dividing. - * Uses floating point division to correctly handle negative numbers. + * For positive numbers, uses integer division (which truncates toward zero). + * For negative numbers, uses floor division to correctly round toward negative infinity. * * @param orientation The orientation value in degrees (typically 0-359 from OrientationEventListener) * @return The orientation rounded to the nearest 90-degree increment (0, 90, 180, 270, or 360) */ static int roundToNearest90Degrees(int orientation) { - return (int) (Math.round((orientation + 45.0) / 90.0) * 90); + if (orientation >= 0) { + // For positive numbers, integer division truncates toward zero + // (0 + 45) / 90 = 0, (44 + 45) / 90 = 0, (45 + 45) / 90 = 1 + return ((orientation + 45) / 90) * 90; + } else { + // For negative numbers, use floor division to round toward negative infinity + // Math.floor((-46 + 45) / 90.0) = Math.floor(-1/90.0) = -1, so -1 * 90 = -90 + return (int) (Math.floor((orientation + 45.0) / 90.0) * 90); + } } /** diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java index 514cfe412..ceaf7c586 100644 --- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java +++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableInAppHTMLNotificationTest.java @@ -363,8 +363,8 @@ public void testRoundToNearest90Degrees_NegativeValues() { assertEquals(0, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-45)); assertEquals(-90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-46)); assertEquals(-90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-90)); - assertEquals(-90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-134)); - assertEquals(-180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-135)); + assertEquals(-90, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-135)); + assertEquals(-180, IterableInAppFragmentHTMLNotification.roundToNearest90Degrees(-136)); } @Test