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 f9ac3aa62..468f1da23 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; @@ -51,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,6 +66,12 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem 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; private boolean shouldAnimate; @@ -92,6 +102,7 @@ public static IterableInAppFragmentHTMLNotification createInstance(@NonNull Stri /** * Returns the notification instance currently being shown + * * @return notification instance */ public static IterableInAppFragmentHTMLNotification getInstance() { @@ -109,6 +120,17 @@ public IterableInAppFragmentHTMLNotification() { 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) { + applyWindowGravity(dialog.getWindow(), "onStart"); + } + } + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -147,6 +169,10 @@ public void onCancel(DialogInterface dialog) { } }); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + + // Set window gravity for the dialog + applyWindowGravity(dialog.getWindow(), "onCreateDialog"); + if (getInAppLayout(insetPadding) == InAppLayout.FULLSCREEN) { dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } else if (getInAppLayout(insetPadding) != InAppLayout.TOP) { @@ -166,22 +192,38 @@ 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"); + webView = new IterableWebView(getContext()); webView.setId(R.id.webView); + 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; + } } } }; @@ -189,17 +231,45 @@ 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); + + // 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; + } + + // 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); } prepareToShowWebView(); - return relativeLayout; + return frameLayout; } @Override @@ -240,6 +310,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; } @@ -388,7 +464,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) { @@ -428,11 +504,55 @@ 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); } /** * Resizes the dialog window based upon the size of its webView HTML content + * * @param height */ public void resize(final float height) { @@ -447,7 +567,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; } @@ -476,9 +596,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); @@ -489,6 +644,7 @@ public void run() { /** * Returns the vertical position of the dialog for the given padding + * * @param padding * @return */ @@ -502,6 +658,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; 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(); + } } }