From 2f0f4b2498d6ff415f45d4992ec69826f720ac78 Mon Sep 17 00:00:00 2001 From: UnnatiCP <129381063+unnaticleverpush@users.noreply.github.com> Date: Thu, 7 May 2026 13:21:07 +0530 Subject: [PATCH 1/2] feature/cp-11483-make-banner-non-blocking Added support for non-blocking in-app banners to allow background interaction while banners are visible. --- .../main/java/com/cleverpush/CleverPush.java | 9 + .../banner/AppBannerCarouselAdapter.java | 163 ++++++++++++++++- .../com/cleverpush/banner/AppBannerPopup.java | 173 ++++++++++++++++-- 3 files changed, 331 insertions(+), 14 deletions(-) diff --git a/cleverpush/src/main/java/com/cleverpush/CleverPush.java b/cleverpush/src/main/java/com/cleverpush/CleverPush.java index 98340f2d..0de00437 100644 --- a/cleverpush/src/main/java/com/cleverpush/CleverPush.java +++ b/cleverpush/src/main/java/com/cleverpush/CleverPush.java @@ -245,6 +245,7 @@ public class CleverPush { int appBannerPerDay; int appBannerPerSession; public static boolean notificationClickInProgress = false; + public boolean appBannersNonBlocking = false; public CleverPush(@NonNull Context context) { if (context == null) { @@ -4653,4 +4654,12 @@ private boolean isChannelIdInvalid(String channelId, String methodName) { } return false; } + + public boolean isAppBannersNonBlocking() { + return appBannersNonBlocking; + } + + public void setAppBannersNonBlocking(boolean appBannersNonBlocking) { + this.appBannersNonBlocking = appBannersNonBlocking; + } } diff --git a/cleverpush/src/main/java/com/cleverpush/banner/AppBannerCarouselAdapter.java b/cleverpush/src/main/java/com/cleverpush/banner/AppBannerCarouselAdapter.java index 983bdc42..b733dfd4 100644 --- a/cleverpush/src/main/java/com/cleverpush/banner/AppBannerCarouselAdapter.java +++ b/cleverpush/src/main/java/com/cleverpush/banner/AppBannerCarouselAdapter.java @@ -736,10 +736,15 @@ private void fixFullscreenHtmlBannerUI(LinearLayout body, ConstraintLayout webLa @SuppressLint("SetJavaScriptEnabled") private void composeHtmlBanner(LinearLayout body, String htmlContent) { try { + boolean isNonBlockingAppBanners = + CleverPush.getInstance(CleverPush.context).isAppBannersNonBlocking(); + activity.runOnUiThread(() -> { + String html = VoucherCodeUtils.replaceVoucherCodeString(htmlContent, voucherCode); String lower = html.toLowerCase(Locale.ROOT); String contextJson = getSubscriptionContextJson(); + String jsToInject = "" + "\n"; + + if (isNonBlockingAppBanners) { + jsToInject += getNonBlockingMeasurementScript(); + } + String htmlWithJs; if (lower.contains("")) { htmlWithJs = Pattern @@ -787,11 +797,15 @@ private void composeHtmlBanner(LinearLayout body, String htmlContent) { jsToInject + ""; } + ConstraintLayout webLayout = (ConstraintLayout) activity.getLayoutInflater().inflate(R.layout.app_banner_html, null); + WebView webView = webLayout.findViewById(R.id.webView); + webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setLoadsImagesAutomatically(true); + webView.addJavascriptInterface(new CleverpushInterface(webView), "CleverPush"); webView.setWebViewClient(new AppBannerWebViewClient()); @@ -801,7 +815,11 @@ private void composeHtmlBanner(LinearLayout body, String htmlContent) { params.height = ViewGroup.LayoutParams.MATCH_PARENT; webView.setLayoutParams(params); webView.requestLayout(); - fixFullscreenHtmlBannerUI(body, webLayout, webView); + if (isNonBlockingAppBanners) { + applyNonBlockingBannerUI(body, webLayout, webView); + } else { + fixFullscreenHtmlBannerUI(body, webLayout, webView); + } }); // Ensure WebView is scrollable @@ -812,7 +830,11 @@ private void composeHtmlBanner(LinearLayout body, String htmlContent) { return false; }); - fixFullscreenHtmlBannerUI(body, webLayout, webView); + if (isNonBlockingAppBanners) { + applyNonBlockingBannerUI(body, webLayout, webView); + } else { + fixFullscreenHtmlBannerUI(body, webLayout, webView); + } String encodedHtml = null; try { @@ -830,6 +852,118 @@ private void composeHtmlBanner(LinearLayout body, String htmlContent) { } } + /** + * Returns a JS snippet that measures the bounding rect of the visible banner content + * inside the WebView and reports the result back to Android via the JS bridge. + * Used for non-blocking HTML banners so the popup window can be shrunk to wrap only + * the actual banner area, leaving the rest of the screen interactive. + */ + private String getNonBlockingMeasurementScript() { + return "\n"; + } + + /** + * Configures the WebView and surrounding containers for a non-blocking HTML banner. + * + * For full-screen banners (type: "full"), the WebView is sized to the full screen height + * (anchored at the bottom of webLayout) so HTML using "position: fixed; bottom" renders + * against the actual screen viewport. AppBannerPopup.body is separately anchored at the + * bottom of the popup (see displayBanner), so when the popup is later shrunk by the JS + * measurement callback to wrap only the banner area, the WebView's bottom (where the + * banner content lives) stays inside the popup window and remains visible. + * + * For non-full banners, a default WebView height (60% of screen) gives the WebView a + * viewport for "position: fixed" rendering without forcing the popup to fullscreen. + */ + private void applyNonBlockingBannerUI(LinearLayout body, ConstraintLayout webLayout, WebView webView) { + int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + boolean isFullScreenBanner = + appBannerPopup != null + && appBannerPopup.getData() != null + && "full".equalsIgnoreCase(appBannerPopup.getData().getPositionType()); + + int webViewHeightPx = isFullScreenBanner ? screenHeight : (int) (screenHeight * 0.6); + + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(webLayout); + constraintSet.clear(R.id.webView, ConstraintSet.TOP); + constraintSet.constrainHeight(R.id.webView, webViewHeightPx); + constraintSet.constrainWidth(R.id.webView, ConstraintSet.MATCH_CONSTRAINT); + constraintSet.connect(R.id.webView, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, 0); + constraintSet.connect(R.id.webView, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, 0); + constraintSet.connect(R.id.webView, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, 0); + constraintSet.setDimensionRatio(R.id.webView, null); + constraintSet.applyTo(webLayout); + + ViewGroup.LayoutParams webViewParams = webView.getLayoutParams(); + if (webViewParams != null) { + webViewParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + webViewParams.height = webViewHeightPx; + webView.setLayoutParams(webViewParams); + } + + ViewGroup.LayoutParams webLayoutParams = webLayout.getLayoutParams(); + if (webLayoutParams != null) { + webLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + webLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + webLayout.setLayoutParams(webLayoutParams); + } + + if (body != null) { + ViewGroup.LayoutParams bodyParams = body.getLayoutParams(); + if (bodyParams != null) { + bodyParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + body.setLayoutParams(bodyParams); + } + body.setPadding(0, 0, 0, 0); + } + + webView.setVerticalScrollBarEnabled(false); + webView.setHorizontalScrollBarEnabled(false); + } + public static int pxToDp(int px) { return (int) (px * Resources.getSystem().getDisplayMetrics().density); } @@ -1014,6 +1148,31 @@ public void copyToClipboard(String text) { clipboard.setPrimaryClip(clip); } } + + /** + * Receives the bounding rect of the visible banner content from the JS measurement + * script (in CSS pixels) and forwards it to the popup so it can shrink itself to + * wrap only the actual banner area, leaving the rest of the screen interactive. + */ + @JavascriptInterface + public void setHtmlBannerBounds(int leftCss, int topCss, int widthCss, int heightCss, + int viewportWidthCss, int viewportHeightCss) { + try { + if (activity == null) return; + activity.runOnUiThread(() -> { + try { + if (appBannerPopup != null) { + appBannerPopup.applyMeasuredHtmlBannerBounds( + leftCss, topCss, widthCss, heightCss, viewportWidthCss, viewportHeightCss); + } + } catch (Exception ex) { + Logger.e(TAG, "Error applying measured HTML banner bounds.", ex); + } + }); + } catch (Exception ex) { + Logger.e(TAG, "Error in setHtmlBannerBounds.", ex); + } + } } /** diff --git a/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java b/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java index a5a2ec52..86c8a84d 100644 --- a/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java +++ b/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java @@ -11,6 +11,7 @@ import android.os.Build; import android.os.Handler; +import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.Gravity; @@ -85,6 +86,9 @@ public class AppBannerPopup { int currentDisplayedPagePosition; private boolean isInitialized = false; + // Gravity used when showing the popup. Used to compute the correct y-offset for popup.update() + // since update() expresses position relative to the original showAtLocation gravity. + private int popupShowGravity = Gravity.TOP; AppBannerPopup(Activity activity, Banner data) { this.activity = activity; @@ -181,13 +185,42 @@ public void init() { body = popupRoot.findViewById(R.id.bannerBody); bannerBackGroundImage = popupRoot.findViewById(R.id.bannerBackgroundImage); - popup = new PopupWindow( - popupRoot, - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - true - ); - currentDisplayedPagePosition = -1; + boolean isNonBlockingAppBanners = CleverPush.getInstance(CleverPush.context).isAppBannersNonBlocking(); + + if (!isNonBlockingAppBanners) { + popup = new PopupWindow( + popupRoot, + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + true + ); + currentDisplayedPagePosition = -1; + } else { + boolean isFullScreenBanner = data.getPositionType().equalsIgnoreCase(POSITION_TYPE_FULL); + // Non-full banners must not use a full-screen popup window: MATCH_PARENT height keeps a + // window over the entire activity, so touches never reach MainActivity (setTouchModal does + // not change window bounds). WRAP_CONTENT limits the window to the banner so the rest of + // the screen stays interactive. + int popupWindowHeight = + isFullScreenBanner ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; + popup = new PopupWindow( + popupRoot, + ViewGroup.LayoutParams.MATCH_PARENT, + popupWindowHeight, + isFullScreenBanner + ); + currentDisplayedPagePosition = -1; + + if (!isFullScreenBanner || isHTMLBanner()) { + popup.setFocusable(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + popup.setTouchModal(false); + } + parent.setClickable(false); + parent.setFocusable(false); + frameLayout.setClickable(true); + } + } if (isHTMLBanner()) { parent.setBackgroundColor(Color.TRANSPARENT); @@ -408,6 +441,8 @@ private void displayBanner(LinearLayout body) { } setUpBannerBlocks(); + boolean isNonBlockingAppBanners = CleverPush.getInstance(CleverPush.context).isAppBannersNonBlocking(); + if (data.getPositionType().equalsIgnoreCase(POSITION_TYPE_FULL)) { ConstraintLayout mConstraintLayout = (ConstraintLayout) popupRoot; ConstraintSet mConstraintSet = new ConstraintSet(); @@ -424,16 +459,49 @@ private void displayBanner(LinearLayout body) { mConstraintSet.constrainHeight(R.id.frameLayout, ConstraintSet.MATCH_CONSTRAINT); mConstraintSet.applyTo(mConstraintLayout); - body.setLayoutParams( - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + if (isNonBlockingAppBanners && isHTMLBanner()) { + // For non-blocking full-screen HTML banners, give the body a fixed full-screen + // height anchored at the bottom of the popup. This keeps the WebView's bottom + // (where HTML using "position: fixed; bottom: ...;" renders) inside the popup + // window even after applyMeasuredHtmlBannerBounds shrinks the popup to wrap + // only the banner area. + int screenHeightPx = activity.getResources().getDisplayMetrics().heightPixels; + FrameLayout.LayoutParams nonBlockingBodyParams = + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, screenHeightPx); + nonBlockingBodyParams.gravity = Gravity.BOTTOM; + body.setLayoutParams(nonBlockingBodyParams); + } else { + body.setLayoutParams( + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } runInMain(() -> { if (!canShow()) { return; } - popup.showAtLocation(getRoot(), Gravity.TOP, 0, 0); + if (isNonBlockingAppBanners && isHTMLBanner()) { + popupShowGravity = Gravity.BOTTOM; + popup.showAtLocation(getRoot(), Gravity.BOTTOM, 0, 0); + } else { + popupShowGravity = Gravity.TOP; + popup.showAtLocation(getRoot(), Gravity.TOP, 0, 0); + } toggleShowing(true); }); } else { + if (isNonBlockingAppBanners) { + ViewGroup.LayoutParams nonFullRootLp = popupRoot.getLayoutParams(); + if (nonFullRootLp == null) { + popupRoot.setLayoutParams( + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } else { + nonFullRootLp.width = ViewGroup.LayoutParams.MATCH_PARENT; + nonFullRootLp.height = ViewGroup.LayoutParams.WRAP_CONTENT; + popupRoot.setLayoutParams(nonFullRootLp); + } + popup.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); + popup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + } + body.setLayoutParams( new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); @@ -452,6 +520,7 @@ private void displayBanner(LinearLayout body) { if (!canShow()) { return; } + popupShowGravity = Gravity.TOP; popup.showAtLocation(getRoot(), Gravity.TOP, 0, 0); toggleShowing(true); }); @@ -468,13 +537,16 @@ private void displayBanner(LinearLayout body) { if (!canShow()) { return; } + popupShowGravity = Gravity.BOTTOM; popup.showAtLocation(getRoot(), Gravity.BOTTOM, 0, 0); toggleShowing(true); }); break; default: - popup.setFocusable(true); - popup.setOutsideTouchable(false); + if (!isNonBlockingAppBanners) { + popup.setFocusable(true); + popup.setOutsideTouchable(false); + } popup.setTouchable(true); popup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); popup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); @@ -489,6 +561,7 @@ private void displayBanner(LinearLayout body) { if (!canShow()) { return; } + popupShowGravity = Gravity.CENTER; popup.showAtLocation(getRoot(), Gravity.CENTER, 0, 0); toggleShowing(true); }); @@ -714,6 +787,82 @@ protected void onPostExecute(Boolean isRootReady) { } } + /** + * Resizes the popup window of a non-blocking HTML banner so it wraps only the actual + * banner content area, leaving the rest of the screen interactive. Called from the + * JS measurement bridge with the banner's bounding rect in CSS pixels relative to the + * WebView viewport. + * + * The popup is anchored at the gravity it was originally shown with (BOTTOM by default + * for non-blocking HTML banners). For top-positioned banner content, the popup is moved + * up to leave the bottom of the screen interactive; for bottom-positioned content it + * stays anchored to the bottom. + */ + public void applyMeasuredHtmlBannerBounds(int leftCss, int topCss, int widthCss, int heightCss, + int viewportWidthCss, int viewportHeightCss) { + try { + if (!isHTMLBanner()) return; + if (!CleverPush.getInstance(CleverPush.context).isAppBannersNonBlocking()) return; + if (popup == null || !popup.isShowing()) return; + if (viewportWidthCss <= 0 || viewportHeightCss <= 0) return; + if (heightCss <= 0) return; + + DisplayMetrics dm = activity.getResources().getDisplayMetrics(); + int screenWidth = dm.widthPixels; + int screenHeight = dm.heightPixels; + + float scaleY = (float) screenHeight / viewportHeightCss; + int topPx = Math.max(0, (int) (topCss * scaleY)); + int heightPx = (int) (heightCss * scaleY); + int marginPx = (int) (10 * dm.density); + + int distFromTop = topPx; + int distFromBottom = Math.max(0, screenHeight - (topPx + heightPx)); + + // Wrap the banner area plus the smaller margin so the popup hugs the visible content, + // anchored to whichever edge the banner is closer to. Also align body's gravity to + // match: body has a fixed full-screen height anchored at one edge of frameLayout so + // the visible portion of the (full-size) WebView inside it lines up with where the + // HTML banner actually renders ("position: fixed; top:" → top, "...; bottom:" → bottom). + int popupHeight; + int popupYOffset; + int bodyGravity; + if (distFromTop <= distFromBottom) { + popupHeight = topPx + heightPx + marginPx; + if (popupShowGravity == Gravity.BOTTOM) { + popupYOffset = screenHeight - popupHeight; + } else { + popupYOffset = 0; + } + bodyGravity = Gravity.TOP; + } else { + popupHeight = heightPx + distFromBottom + marginPx; + if (popupShowGravity == Gravity.BOTTOM) { + popupYOffset = 0; + } else { + popupYOffset = screenHeight - popupHeight; + } + bodyGravity = Gravity.BOTTOM; + } + + if (popupHeight <= 0 || popupHeight > screenHeight) { + return; + } + + if (body != null && body.getLayoutParams() instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams bodyParams = (FrameLayout.LayoutParams) body.getLayoutParams(); + if (bodyParams.gravity != bodyGravity) { + bodyParams.gravity = bodyGravity; + body.setLayoutParams(bodyParams); + } + } + + popup.update(0, popupYOffset, screenWidth, popupHeight); + } catch (Exception e) { + Logger.e(TAG, "Error in applyMeasuredHtmlBannerBounds.", e); + } + } + private void setNotchColor(boolean isFinish) { try { final String notchColor; From 74f325336779e46683e0f9ef020d8df449bb54f1 Mon Sep 17 00:00:00 2001 From: UnnatiCP <129381063+unnaticleverpush@users.noreply.github.com> Date: Thu, 7 May 2026 15:01:21 +0530 Subject: [PATCH 2/2] feature/cp-11483-make-banner-non-blocking Fix non-blocking HTML banner popup updates to run on UI thread --- .../src/main/java/com/cleverpush/banner/AppBannerPopup.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java b/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java index 86c8a84d..410ffa3c 100644 --- a/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java +++ b/cleverpush/src/main/java/com/cleverpush/banner/AppBannerPopup.java @@ -10,6 +10,7 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Handler; +import android.os.Looper; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -801,6 +802,11 @@ protected void onPostExecute(Boolean isRootReady) { public void applyMeasuredHtmlBannerBounds(int leftCss, int topCss, int widthCss, int heightCss, int viewportWidthCss, int viewportHeightCss) { try { + if (Looper.myLooper() != Looper.getMainLooper()) { + mainHandler.post(() -> applyMeasuredHtmlBannerBounds( + leftCss, topCss, widthCss, heightCss, viewportWidthCss, viewportHeightCss)); + return; + } if (!isHTMLBanner()) return; if (!CleverPush.getInstance(CleverPush.context).isAppBannersNonBlocking()) return; if (popup == null || !popup.isShowing()) return;