Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -62,6 +66,12 @@ public class IterableInAppFragmentHTMLNotification extends DialogFragment implem
private String htmlString;
private String messageId;

// Resize debouncing fields
private Handler resizeHandler = new Handler();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
Handler.Handler
should be avoided because it has been deprecated.
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;
Expand Down Expand Up @@ -92,6 +102,7 @@ public static IterableInAppFragmentHTMLNotification createInstance(@NonNull Stri

/**
* Returns the notification instance currently being shown
*
* @return notification instance
*/
public static IterableInAppFragmentHTMLNotification getInstance() {
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -166,40 +192,84 @@ 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
Comment on lines +209 to +215
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows for resize script to be called only limted number of times. This method when implemented, felt it was only called when landscape to portrait rotation was made. Hence explicit conditioning is required.

final Handler handler = new Handler();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
Handler.Handler
should be avoided because it has been deprecated.
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;
}
}
}
};
}

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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand All @@ -489,6 +644,7 @@ public void run() {

/**
* Returns the vertical position of the dialog for the given padding
*
* @param padding
* @return
*/
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
Loading