diff --git a/CHANGELOG.md b/CHANGELOG.md index e933ef2f1..9e2204ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## X.X.X * Added a new config flag `setUseSerialExecutor(boolean useSerial)` for selecting immediate request executor type. +* Added a new config option `setWebviewDisplayOption(WebViewDisplayOption)` to control how Content and Feedback Widgets are displayed. + * `IMMERSIVE` mode (default): Full-screen display (except cutouts). + * `SAFE_AREA` mode: Omits status bar, navigation bar and cutouts when displaying webviews. + * Immediate requests now will be run by parallel executor instead of serial by default. ## 25.4.4 diff --git a/app/src/main/java/ly/count/android/demo/App.java b/app/src/main/java/ly/count/android/demo/App.java index 42f90d236..fe0ef9a6c 100644 --- a/app/src/main/java/ly/count/android/demo/App.java +++ b/app/src/main/java/ly/count/android/demo/App.java @@ -28,6 +28,7 @@ import ly.count.android.sdk.CrashData; import ly.count.android.sdk.GlobalCrashFilterCallback; import ly.count.android.sdk.ModuleLog; +import ly.count.android.sdk.WebViewDisplayOption; import ly.count.android.sdk.messaging.CountlyConfigPush; import ly.count.android.sdk.messaging.CountlyPush; @@ -146,7 +147,8 @@ public void onCreate() { Map customUserProperties = new ConcurrentHashMap<>(); customUserProperties.put("A", 1); - CountlyConfig config = new CountlyConfig(this, COUNTLY_APP_KEY, COUNTLY_SERVER_URL)//.setDeviceId("67567") + CountlyConfig config = new CountlyConfig(this, COUNTLY_APP_KEY, COUNTLY_SERVER_URL) + // .setDeviceId("a" + applicationStartTimestamp ) .setLoggingEnabled(true) .setLogListener(new ModuleLog.LogCallback() { @Override public void LogHappened(String logMessage, ModuleLog.LogLevel logLevel) { @@ -173,42 +175,30 @@ public void onCreate() { } }) .enableAutomaticViewTracking() - // uncomment the line below to enable auto enrolling the user to AB experiments when downloading RC data //.enrollABOnRCDownload() - // .setMaxRequestQueueSize(5) + //.setMaxRequestQueueSize(5) .enableAutomaticViewShortNames() .setGlobalViewSegmentation(automaticViewSegmentation) .setAutomaticViewTrackingExclusions(new Class[] { ActivityExampleCustomEvents.class }) - .setPushIntentAddMetadata(true) - .setLocation("us", "Böston 墨尔本", "-23.8043604,-46.6718331", "10.2.33.12") //.setDisableLocation() - //.enableManualSessionControl() //.enableManualSessionControlHybridMode() - //.enableTemporaryDeviceIdMode() - .setRequiresConsent(true) - - //for giving all consent values .giveAllConsents() - - //in case you want to control what consent is given during init //.setConsentEnabled(new String[] { // Countly.CountlyFeatureNames.push, Countly.CountlyFeatureNames.sessions, Countly.CountlyFeatureNames.location, // Countly.CountlyFeatureNames.attribution, Countly.CountlyFeatureNames.crashes, Countly.CountlyFeatureNames.events, // Countly.CountlyFeatureNames.starRating, Countly.CountlyFeatureNames.users, Countly.CountlyFeatureNames.views, // Countly.CountlyFeatureNames.apm, Countly.CountlyFeatureNames.remoteConfig, Countly.CountlyFeatureNames.feedback //}) - .setHttpPostForced(false) .setParameterTamperingProtectionSalt("test-salt-checksum") .addCustomNetworkRequestHeaders(customHeaderValues) //.enableCertificatePinning(certificates) //.enablePublicKeyPinning(certificates) - .RemoteConfigRegisterGlobalCallback((downloadResult, error, fullValueUpdate, downloadedValues) -> { if (error == null) { Log.d(Countly.TAG, "Automatic remote config download has completed. " + Countly.sharedInstance().remoteConfig().getValues()); @@ -216,14 +206,13 @@ public void onCreate() { Log.d(Countly.TAG, "Automatic remote config download encountered a problem, " + error); } }) - .setTrackOrientationChanges(true) //.setMetricOverride(metricOverride) - + .setWebviewDisplayOption(WebViewDisplayOption.IMMERSIVE) //.enableServerConfiguration() - .setUserProperties(customUserProperties); + // crash configuration config.crashes .enableCrashReporting() .enableRecordAllThreadsWithCrash() @@ -234,13 +223,16 @@ public void onCreate() { } }); + // APM configuration config.apm.enableAppStartTimeTracking() .enableForegroundBackgroundTracking() .setAppStartTimestampOverride(applicationStartTimestamp); + Countly.sharedInstance().init(config); //Log.i(demoTag, "After calling init. This should return 'true', the value is:" + Countly.sharedInstance().isInitialized()); + //--- PUSH NOTIFICATIONS SETUP ----// List allowedClassNames = new ArrayList<>(); allowedClassNames.add("MainActivity"); List allowedPackageNames = new ArrayList<>(); diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java index 94257685a..fb48467c5 100644 --- a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java @@ -208,6 +208,7 @@ public class CountlyConfig { // If set to true, immediate requests will use serial AsyncTask executor instead of the thread pool boolean useSerialExecutor = false; + WebViewDisplayOption webViewDisplayOption = WebViewDisplayOption.IMMERSIVE; /** * THIS VARIABLE SHOULD NOT BE USED @@ -1055,6 +1056,20 @@ public synchronized CountlyConfig setRequestTimeoutDuration(int requestTimeoutDu return this; } + /** + * Set the webview display option for Content and Feedback Widgets + * + * @param displayOption IMMERSIVE for full screen with hidden system UI, or + * SAFE_AREA to use app usable area and not overlap system UI + * @return config content to chain calls + */ + public synchronized CountlyConfig setWebviewDisplayOption(WebViewDisplayOption displayOption) { + if (displayOption != null) { + this.webViewDisplayOption = displayOption; + } + return this; + } + /** * To select the legacy AsyncTask.execute (serial executor) or * instead executeOnExecutor(THREAD_POOL_EXECUTOR) diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java index 12758370b..ade4eb2eb 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -190,14 +190,42 @@ private String prepareContentFetchRequest(@NonNull DisplayMetrics displayMetrics int currentOrientation = resources.getConfiguration().orientation; boolean portrait = currentOrientation == Configuration.ORIENTATION_PORTRAIT; - int scaledWidth = (int) Math.ceil(displayMetrics.widthPixels / displayMetrics.density); - int scaledHeight = (int) Math.ceil(displayMetrics.heightPixels / displayMetrics.density); + int portraitWidth, portraitHeight, landscapeWidth, landscapeHeight; + + int totalWidthPx = displayMetrics.widthPixels; + int totalHeightPx = displayMetrics.heightPixels; + int totalWidthDp = (int) Math.ceil(totalWidthPx / displayMetrics.density); + int totalHeightDp = (int) Math.ceil(totalHeightPx / displayMetrics.density); + L.d("[ModuleContent] prepareContentFetchRequest, total screen dimensions (px): [" + totalWidthPx + "x" + totalHeightPx + "], (dp): [" + totalWidthDp + "x" + totalHeightDp + "], density: [" + displayMetrics.density + "]"); + + WebViewDisplayOption displayOption = _cly.config_.webViewDisplayOption; + L.d("[ModuleContent] prepareContentFetchRequest, display option: [" + displayOption + "]"); + + if (displayOption == WebViewDisplayOption.SAFE_AREA) { + L.d("[ModuleContent] prepareContentFetchRequest, calculating safe area dimensions..."); + SafeAreaDimensions safeArea = SafeAreaCalculator.calculateSafeAreaDimensions(_cly.context_, L); + + // px to dp + portraitWidth = (int) Math.ceil(safeArea.portraitWidth / displayMetrics.density); + portraitHeight = (int) Math.ceil(safeArea.portraitHeight / displayMetrics.density); + landscapeWidth = (int) Math.ceil(safeArea.landscapeWidth / displayMetrics.density); + landscapeHeight = (int) Math.ceil(safeArea.landscapeHeight / displayMetrics.density); + + L.d("[ModuleContent] prepareContentFetchRequest, safe area dimensions (px->dp) - Portrait: [" + safeArea.portraitWidth + "x" + safeArea.portraitHeight + " px] -> [" + portraitWidth + "x" + portraitHeight + " dp], topOffset: [" + safeArea.portraitTopOffset + " px]"); + L.d("[ModuleContent] prepareContentFetchRequest, safe area dimensions (px->dp) - Landscape: [" + safeArea.landscapeWidth + "x" + safeArea.landscapeHeight + " px] -> [" + landscapeWidth + "x" + landscapeHeight + " dp], topOffset: [" + safeArea.landscapeTopOffset + " px]"); + } else { + int scaledWidth = totalWidthDp; + int scaledHeight = totalHeightDp; + + portraitWidth = portrait ? scaledWidth : scaledHeight; + portraitHeight = portrait ? scaledHeight : scaledWidth; + landscapeWidth = portrait ? scaledHeight : scaledWidth; + landscapeHeight = portrait ? scaledWidth : scaledHeight; + + L.d("[ModuleContent] prepareContentFetchRequest, using immersive mode (full screen) dimensions (dp) - Portrait: [" + portraitWidth + "x" + portraitHeight + "], Landscape: [" + landscapeWidth + "x" + landscapeHeight + "]"); + } - // this calculation needs improvement for status bar and navigation bar - int portraitWidth = portrait ? scaledWidth : scaledHeight; - int portraitHeight = portrait ? scaledHeight : scaledWidth; - int landscapeWidth = portrait ? scaledHeight : scaledWidth; - int landscapeHeight = portrait ? scaledWidth : scaledHeight; + L.i("[ModuleContent] prepareContentFetchRequest, FINAL dimensions to send to server (dp) - Portrait: [" + portraitWidth + "x" + portraitHeight + "], Landscape: [" + landscapeWidth + "x" + landscapeHeight + "]"); String language = Locale.getDefault().getLanguage().toLowerCase(); String deviceType = deviceInfo.mp.getDeviceType(_cly.context_); @@ -219,13 +247,28 @@ Map parseContent(@NonNull JSONObject respons JSONObject coordinates = response.optJSONObject("geo"); assert coordinates != null; - placementCoordinates.put(Configuration.ORIENTATION_PORTRAIT, extractOrientationPlacements(coordinates, displayMetrics.density, "p", content)); - placementCoordinates.put(Configuration.ORIENTATION_LANDSCAPE, extractOrientationPlacements(coordinates, displayMetrics.density, "l", content)); + + WebViewDisplayOption displayOption = _cly.config_.webViewDisplayOption; + SafeAreaDimensions safeArea = null; + + if (displayOption == WebViewDisplayOption.SAFE_AREA) { + L.d("[ModuleContent] parseContent, calculating safe area for coordinate adjustment..."); + safeArea = SafeAreaCalculator.calculateSafeAreaDimensions(_cly.context_, L); + } + + placementCoordinates.put(Configuration.ORIENTATION_PORTRAIT, + extractOrientationPlacements(coordinates, displayMetrics.density, "p", content, + displayOption, safeArea != null ? safeArea.portraitTopOffset : 0, safeArea != null ? safeArea.portraitLeftOffset : 0)); + placementCoordinates.put(Configuration.ORIENTATION_LANDSCAPE, + extractOrientationPlacements(coordinates, displayMetrics.density, "l", content, + displayOption, safeArea != null ? safeArea.landscapeTopOffset : 0, safeArea != null ? safeArea.landscapeLeftOffset : 0)); return placementCoordinates; } - private TransparentActivityConfig extractOrientationPlacements(@NonNull JSONObject placements, float density, @NonNull String orientation, @NonNull String content) { + private TransparentActivityConfig extractOrientationPlacements(@NonNull JSONObject placements, + float density, @NonNull String orientation, @NonNull String content, + WebViewDisplayOption displayOption, int topOffset, int leftOffset) { if (placements.has(orientation)) { JSONObject orientationPlacements = placements.optJSONObject(orientation); assert orientationPlacements != null; @@ -234,8 +277,21 @@ private TransparentActivityConfig extractOrientationPlacements(@NonNull JSONObje int w = orientationPlacements.optInt("w"); int h = orientationPlacements.optInt("h"); L.d("[ModuleContent] extractOrientationPlacements, orientation: [" + orientation + "], x: [" + x + "], y: [" + y + "], w: [" + w + "], h: [" + h + "]"); - TransparentActivityConfig config = new TransparentActivityConfig((int) Math.ceil(x * density), (int) Math.ceil(y * density), (int) Math.ceil(w * density), (int) Math.ceil(h * density)); + + int xPx = Math.round(x * density); + int yPx = Math.round(y * density); + int wPx = Math.round(w * density); + int hPx = Math.round(h * density); + L.d("[ModuleContent] extractOrientationPlacements, orientation: [" + orientation + "], converting dp->px: [" + w + "x" + h + " dp] -> [" + wPx + "x" + hPx + " px], density: [" + density + "]"); + + TransparentActivityConfig config = new TransparentActivityConfig(xPx, yPx, wPx, hPx); config.url = content; + config.useSafeArea = (displayOption == WebViewDisplayOption.SAFE_AREA); + config.topOffset = topOffset; + config.leftOffset = leftOffset; + + L.d("[ModuleContent] extractOrientationPlacements, orientation: [" + orientation + "], created config - useSafeArea: [" + config.useSafeArea + "], topOffset: [" + config.topOffset + "], leftOffset: [" + config.leftOffset + "]"); + return config; } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java index 228596859..d8e4b78c3 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java @@ -344,20 +344,59 @@ private void showFeedbackWidget_newActivity(@NonNull Context context, String url int currentOrientation = resources.getConfiguration().orientation; boolean portrait = currentOrientation == Configuration.ORIENTATION_PORTRAIT; - int width = displayMetrics.widthPixels; - int height = displayMetrics.heightPixels; + int portraitWidth, portraitHeight, landscapeWidth, landscapeHeight; + int portraitTopOffset = 0; + int landscapeTopOffset = 0; + int portraitLeftOffset = 0; + int landscapeLeftOffset = 0; + + int totalWidthPx = displayMetrics.widthPixels; + int totalHeightPx = displayMetrics.heightPixels; + L.d("[ModuleFeedback] showFeedbackWidget_newActivity, total screen dimensions (px): [" + totalWidthPx + "x" + totalHeightPx + "], density: [" + displayMetrics.density + "]"); + + WebViewDisplayOption displayOption = _cly.config_.webViewDisplayOption; + L.d("[ModuleFeedback] showFeedbackWidget_newActivity, display option: [" + displayOption + "]"); + + if (displayOption == WebViewDisplayOption.SAFE_AREA) { + L.d("[ModuleFeedback] showFeedbackWidget_newActivity, calculating safe area dimensions..."); + SafeAreaDimensions safeArea = SafeAreaCalculator.calculateSafeAreaDimensions(context, L); + + portraitWidth = safeArea.portraitWidth; + portraitHeight = safeArea.portraitHeight; + landscapeWidth = safeArea.landscapeWidth; + landscapeHeight = safeArea.landscapeHeight; + portraitTopOffset = safeArea.portraitTopOffset; + landscapeTopOffset = safeArea.landscapeTopOffset; + portraitLeftOffset = safeArea.portraitLeftOffset; + landscapeLeftOffset = safeArea.landscapeLeftOffset; + + L.d("[ModuleFeedback] showFeedbackWidget_newActivity, safe area dimensions (px) - Portrait: [" + portraitWidth + "x" + portraitHeight + "], topOffset: [" + portraitTopOffset + "], leftOffset: [" + portraitLeftOffset + "]"); + L.d("[ModuleFeedback] showFeedbackWidget_newActivity, safe area dimensions (px) - Landscape: [" + landscapeWidth + "x" + landscapeHeight + "], topOffset: [" + landscapeTopOffset + "], leftOffset: [" + landscapeLeftOffset + "]"); + } else { + int width = displayMetrics.widthPixels; + int height = displayMetrics.heightPixels; + + portraitWidth = portrait ? width : height; + portraitHeight = portrait ? height : width; + landscapeWidth = portrait ? height : width; + landscapeHeight = portrait ? width : height; + + L.d("[ModuleFeedback] showFeedbackWidget_newActivity, using immersive mode (full screen) dimensions (px) - Portrait: [" + portraitWidth + "x" + portraitHeight + "], Landscape: [" + landscapeWidth + "x" + landscapeHeight + "]"); + } - // this calculation needs improvement for status bar and navigation bar - int portraitWidth = portrait ? width : height; - int portraitHeight = portrait ? height : width; - int landscapeWidth = portrait ? height : width; - int landscapeHeight = portrait ? width : height; + L.i("[ModuleFeedback] showFeedbackWidget_newActivity, FINAL dimensions for widget (px) - Portrait: [" + portraitWidth + "x" + portraitHeight + "], Landscape: [" + landscapeWidth + "x" + landscapeHeight + "]"); Map placementCoordinates = new ConcurrentHashMap<>(); TransparentActivityConfig pConfig = new TransparentActivityConfig(0, 0, portraitWidth, portraitHeight); TransparentActivityConfig lConfig = new TransparentActivityConfig(0, 0, landscapeWidth, landscapeHeight); pConfig.url = url; lConfig.url = url; + pConfig.useSafeArea = (displayOption == WebViewDisplayOption.SAFE_AREA); + lConfig.useSafeArea = (displayOption == WebViewDisplayOption.SAFE_AREA); + pConfig.topOffset = portraitTopOffset; + lConfig.topOffset = landscapeTopOffset; + pConfig.leftOffset = portraitLeftOffset; + lConfig.leftOffset = landscapeLeftOffset; placementCoordinates.put(Configuration.ORIENTATION_PORTRAIT, pConfig); placementCoordinates.put(Configuration.ORIENTATION_LANDSCAPE, lConfig); diff --git a/sdk/src/main/java/ly/count/android/sdk/SafeAreaCalculator.java b/sdk/src/main/java/ly/count/android/sdk/SafeAreaCalculator.java new file mode 100644 index 000000000..705b80bbf --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/SafeAreaCalculator.java @@ -0,0 +1,402 @@ +package ly.count.android.sdk; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.DisplayCutout; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import androidx.annotation.NonNull; + +/** + * Utility class to calculate safe area dimensions for webview display + * Takes into account cutout, status bar, and navigation bar + */ +class SafeAreaCalculator { + + private static final int DEFAULT_BUTTON_NAV_BAR_HEIGHT_DP = 48; + private static final int DEFAULT_GESTURE_NAV_BAR_HEIGHT_DP = 24; + + private SafeAreaCalculator() { + } + + /** + * Calculates safe area dimensions for both portrait and landscape orientations + * Compatible with Android 21+ + * + * @param context Context to use for calculations + * @param L Logger for debugging + * @return SafeAreaDimensions object containing calculated dimensions and offsets + */ + @NonNull + static SafeAreaDimensions calculateSafeAreaDimensions(@NonNull Context context, @NonNull ModuleLog L) { + final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + final Resources resources = context.getResources(); + final int currentOrientation = resources.getConfiguration().orientation; + final boolean isPortrait = currentOrientation == Configuration.ORIENTATION_PORTRAIT; + + L.d("[SafeAreaCalculator] calculateSafeAreaDimensions, current orientation: [" + (isPortrait ? "portrait" : "landscape") + "], API level: [" + Build.VERSION.SDK_INT + "]"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return calculateSafeAreaDimensionsR(context, wm, isPortrait, L); + } else { + return calculateSafeAreaDimensionsLegacy(context, wm, isPortrait, L); + } + } + + @TargetApi(Build.VERSION_CODES.R) + private static SafeAreaDimensions calculateSafeAreaDimensionsR(@NonNull Context context, + @NonNull WindowManager wm, boolean isPortrait, @NonNull ModuleLog L) { + final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); + final WindowInsets windowInsets = windowMetrics.getWindowInsets(); + final Rect bounds = windowMetrics.getBounds(); + + float density = context.getResources().getDisplayMetrics().density; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + density = windowMetrics.getDensity(); + } + + int currentWidth = bounds.width(); + int currentHeight = bounds.height(); + + int portraitWidth = isPortrait ? currentWidth : currentHeight; + int portraitHeight = isPortrait ? currentHeight : currentWidth; + int landscapeWidth = isPortrait ? currentHeight : currentWidth; + int landscapeHeight = isPortrait ? currentWidth : currentHeight; + + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsR, window bounds (px): width=[" + currentWidth + "], height=[" + currentHeight + "], density=[" + density + "]"); + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsR, mapped orientation dimensions (px) - Portrait: [" + portraitWidth + "x" + portraitHeight + "] Landscape: [" + landscapeWidth + "x" + landscapeHeight + "]"); + + SafeAreaInsets portraitInsets = calculateInsetsForOrientation( + context, windowInsets, true, density, portraitWidth, portraitHeight, L); + SafeAreaInsets landscapeInsets = calculateInsetsForOrientation( + context, windowInsets, false, density, landscapeWidth, landscapeHeight, L); + + SafeAreaDimensions result = new SafeAreaDimensions( + portraitInsets.width, + portraitInsets.height, + landscapeInsets.width, + landscapeInsets.height, + portraitInsets.topOffset, + landscapeInsets.topOffset, + portraitInsets.leftOffset, + landscapeInsets.leftOffset + ); + + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsR, final safe area (px) - Portrait: [" + result.portraitWidth + "x" + result.portraitHeight + "], topOffset=[" + result.portraitTopOffset + "], leftOffset=[" + result.portraitLeftOffset + "]"); + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsR, final safe area (px) - Landscape: [" + result.landscapeWidth + "x" + result.landscapeHeight + "], topOffset=[" + result.landscapeTopOffset + "], leftOffset=[" + result.landscapeLeftOffset + "]"); + + return result; + } + + @SuppressWarnings("deprecation") + private static SafeAreaDimensions calculateSafeAreaDimensionsLegacy(@NonNull Context context, + @NonNull WindowManager wm, boolean isPortrait, @NonNull ModuleLog L) { + final Display display = wm.getDefaultDisplay(); + final DisplayMetrics metrics = new DisplayMetrics(); + display.getRealMetrics(metrics); + + if (context instanceof Activity) { + UtilsDevice.getCutout((Activity) context); + } + + float density = metrics.density; + int currentWidth = metrics.widthPixels; + int currentHeight = metrics.heightPixels; + + int portraitWidth = isPortrait ? currentWidth : currentHeight; + int portraitHeight = isPortrait ? currentHeight : currentWidth; + int landscapeWidth = isPortrait ? currentHeight : currentWidth; + int landscapeHeight = isPortrait ? currentWidth : currentHeight; + + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsLegacy, display metrics (px): width=[" + currentWidth + "], height=[" + currentHeight + "], density=[" + density + "]"); + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsLegacy, mapped orientation dimensions (px) - Portrait: [" + portraitWidth + "x" + portraitHeight + "] Landscape: [" + landscapeWidth + "x" + landscapeHeight + "]"); + + SafeAreaInsets portraitInsets = calculateInsetsLegacy( + context, true, density, portraitWidth, portraitHeight, L); + SafeAreaInsets landscapeInsets = calculateInsetsLegacy( + context, false, density, landscapeWidth, landscapeHeight, L); + + SafeAreaDimensions result = new SafeAreaDimensions( + portraitInsets.width, + portraitInsets.height, + landscapeInsets.width, + landscapeInsets.height, + portraitInsets.topOffset, + landscapeInsets.topOffset, + portraitInsets.leftOffset, + landscapeInsets.leftOffset + ); + + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsLegacy, final safe area (px) - Portrait: [" + result.portraitWidth + "x" + result.portraitHeight + "], topOffset=[" + result.portraitTopOffset + "], leftOffset=[" + result.portraitLeftOffset + "]"); + L.d("[SafeAreaCalculator] calculateSafeAreaDimensionsLegacy, final safe area (px) - Landscape: [" + result.landscapeWidth + "x" + result.landscapeHeight + "], topOffset=[" + result.landscapeTopOffset + "], leftOffset=[" + result.landscapeLeftOffset + "]"); + + return result; + } + + @TargetApi(Build.VERSION_CODES.R) + private static SafeAreaInsets calculateInsetsForOrientation(@NonNull Context context, + @NonNull WindowInsets windowInsets, boolean isPortrait, float density, + int widthForOrientation, int heightForOrientation, @NonNull ModuleLog L) { + + String orientationStr = isPortrait ? "portrait" : "landscape"; + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], total dimensions (px): [" + widthForOrientation + "x" + heightForOrientation + "]"); + + int topInset = 0; + int bottomInset = 0; + int leftInset = 0; + int rightInset = 0; + int statusBarInset = 0; + int cutoutInset = 0; + int navBarInset = 0; + + boolean isActivity = context instanceof Activity; + + boolean statusBarVisible = windowInsets.isVisible(WindowInsets.Type.statusBars()); + boolean navBarVisible = windowInsets.isVisible(WindowInsets.Type.navigationBars()); + boolean cutoutVisible = windowInsets.isVisible(WindowInsets.Type.displayCutout()); + + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], context type: [" + (isActivity ? "Activity" : "Non-Activity") + "], visibility - statusBar=[" + statusBarVisible + "], navBar=[" + navBarVisible + "], cutout=[" + cutoutVisible + "]"); + + if (statusBarVisible && isActivity) { + Insets statusBarInsets = windowInsets.getInsets(WindowInsets.Type.statusBars()); + statusBarInset = statusBarInsets.top; + topInset = Math.max(topInset, statusBarInset); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], status bar inset (px): [" + statusBarInset + "]"); + } + + if (cutoutVisible) { + Insets cutoutInsets = windowInsets.getInsets(WindowInsets.Type.displayCutout()); + if (isPortrait) { + cutoutInset = cutoutInsets.top; + topInset = Math.max(topInset, cutoutInset); + bottomInset = Math.max(bottomInset, cutoutInsets.bottom); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], cutout insets (px) - top=[" + cutoutInsets.top + "], bottom=[" + cutoutInsets.bottom + "]"); + } else { + topInset = Math.max(topInset, cutoutInsets.top); + bottomInset = Math.max(bottomInset, cutoutInsets.bottom); + leftInset = Math.max(leftInset, cutoutInsets.left); + rightInset = Math.max(rightInset, cutoutInsets.right); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], cutout insets (px) - top=[" + cutoutInsets.top + "], bottom=[" + cutoutInsets.bottom + "], left=[" + cutoutInsets.left + "], right=[" + cutoutInsets.right + "]"); + } + } + + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], top inset (px) - using MAX(statusBar=" + statusBarInset + ", cutout=" + cutoutInset + ") = [" + topInset + "]"); + + if (navBarVisible) { + Insets navBarInsets = windowInsets.getInsets(WindowInsets.Type.navigationBars()); + + boolean isGestureNav = isGestureNavigation(navBarInsets, density); + String navType = isGestureNav ? "gesture" : "button"; + + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar type: [" + navType + "], raw insets (px) - top=[" + navBarInsets.top + "], bottom=[" + navBarInsets.bottom + "], left=[" + navBarInsets.left + "], right=[" + navBarInsets.right + "]"); + + if (isPortrait) { + navBarInset = navBarInsets.bottom; + if (navBarInset == 0) { + navBarInset = getDefaultNavBarInset(isGestureNav, density); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar returned 0, using default [" + navType + "] value (px): [" + navBarInset + "]"); + } + bottomInset = Math.max(bottomInset, navBarInset); + } else { + if (navBarInsets.bottom > 0) { + bottomInset = Math.max(bottomInset, navBarInsets.bottom); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar at bottom, applying bottom inset (px): [" + navBarInsets.bottom + "]"); + } + if (navBarInsets.left > 0) { + leftInset = Math.max(leftInset, navBarInsets.left); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar at left, applying left inset (px): [" + navBarInsets.left + "]"); + } + if (navBarInsets.right > 0) { + rightInset = Math.max(rightInset, navBarInsets.right); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar at right, applying right inset (px): [" + navBarInsets.right + "]"); + } + + if (bottomInset == 0 && leftInset == 0 && rightInset == 0) { + navBarInset = getDefaultNavBarInset(isGestureNav, density); + bottomInset = navBarInset; + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar returned 0, using default [" + navType + "] value at bottom (px): [" + navBarInset + "]"); + } + } + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], applied nav bar insets (px) - bottom=[" + bottomInset + "], left=[" + leftInset + "], right=[" + rightInset + "]"); + } + + int width = widthForOrientation - leftInset - rightInset; + int height = heightForOrientation - topInset - bottomInset; + + int leftOffset = 0; + if (!isPortrait) { + Insets navBarInsets = navBarVisible ? windowInsets.getInsets(WindowInsets.Type.navigationBars()) : Insets.NONE; + Insets cutoutInsets = cutoutVisible ? windowInsets.getInsets(WindowInsets.Type.displayCutout()) : Insets.NONE; + + if (navBarInsets.left > 0) { + leftOffset = navBarInsets.left; + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar at left - leftOffset=[" + leftOffset + "] (navBar=" + navBarInsets.left + ")"); + } + // cutout inset is already included in the leftInset calculation above + else if (navBarInsets.right > 0) { + leftOffset = 0; + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], nav bar at right - leftOffset=[" + leftOffset + "] (system will handle cutout positioning)"); + } + } + + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], final insets (px) - top=[" + topInset + "], bottom=[" + bottomInset + "], left=[" + leftInset + "], right=[" + rightInset + "]"); + L.d("[SafeAreaCalculator] calculateInsetsForOrientation [" + orientationStr + "], final safe area (px): [" + width + "x" + height + "], leftOffset=[" + leftOffset + "]"); + + return new SafeAreaInsets(width, height, topInset, leftOffset); + } + + private static SafeAreaInsets calculateInsetsLegacy(@NonNull Context context, + boolean isPortrait, float density, int widthForOrientation, int heightForOrientation, @NonNull ModuleLog L) { + + String orientationStr = isPortrait ? "portrait" : "landscape"; + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], total dimensions (px): [" + widthForOrientation + "x" + heightForOrientation + "]"); + + int topInset = 0; + int bottomInset = 0; + int leftInset = 0; + int rightInset = 0; + int cutoutInset = 0; + int statusBarInset = 0; + int navBarInset = 0; + + DisplayCutout cutout = UtilsDevice.cutout; + boolean hasCutout = (cutout != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P); + if (hasCutout) { + if (isPortrait) { + cutoutInset = cutout.getSafeInsetTop(); + topInset = Math.max(topInset, cutoutInset); + bottomInset = Math.max(bottomInset, cutout.getSafeInsetBottom()); + leftInset = Math.max(leftInset, cutout.getSafeInsetLeft()); + rightInset = Math.max(rightInset, cutout.getSafeInsetRight()); + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], cutout insets (px) - top=[" + cutout.getSafeInsetTop() + "], bottom=[" + cutout.getSafeInsetBottom() + "], left=[" + cutout.getSafeInsetLeft() + "], right=[" + cutout.getSafeInsetRight() + "]"); + } else { + topInset = Math.max(topInset, cutout.getSafeInsetTop()); + bottomInset = Math.max(bottomInset, cutout.getSafeInsetBottom()); + leftInset = Math.max(leftInset, cutout.getSafeInsetLeft()); + rightInset = Math.max(rightInset, cutout.getSafeInsetRight()); + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], cutout insets (px) - top=[" + cutout.getSafeInsetTop() + "], bottom=[" + cutout.getSafeInsetBottom() + "], left=[" + cutout.getSafeInsetLeft() + "], right=[" + cutout.getSafeInsetRight() + "]"); + } + } else { + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], no cutout detected"); + } + + statusBarInset = getStatusBarHeight(context); + topInset = Math.max(topInset, statusBarInset); + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], status bar height (px): [" + statusBarInset + "]"); + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], top inset (px) - using MAX(statusBar=" + statusBarInset + ", cutout=" + cutoutInset + ") = [" + topInset + "]"); + + int navBarHeightFromResource = getNavigationBarHeight(context, isPortrait); + + boolean navBarVisible = isNavigationBarVisible(context); + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], nav bar visible: [" + navBarVisible + "], resource height (px): [" + navBarHeightFromResource + "]"); + + if (navBarVisible) { + boolean isGestureNav = navBarHeightFromResource < (int) (density * 40); // < 40dp likely gesture + String navType = isGestureNav ? "gesture" : "button"; + + navBarInset = navBarHeightFromResource; + if (navBarInset == 0) { + navBarInset = getDefaultNavBarInset(isGestureNav, density); + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], nav bar height is 0, using default [" + navType + "] value (px): [" + navBarInset + "]"); + } else { + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], nav bar type: [" + navType + "], height (px): [" + navBarInset + "]"); + } + + if (isPortrait) { + bottomInset = Math.max(bottomInset, navBarInset); + } else { + bottomInset = Math.max(bottomInset, navBarInset); + } + } + + int width = widthForOrientation - leftInset - rightInset; + int height = heightForOrientation - topInset - bottomInset; + + // < Android R, we cannot reliably detect nav bar position + int leftOffset = 0; + + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], final insets (px) - top=[" + topInset + "], bottom=[" + bottomInset + "], left=[" + leftInset + "], right=[" + rightInset + "]"); + L.d("[SafeAreaCalculator] calculateInsetsLegacy [" + orientationStr + "], final safe area (px): [" + width + "x" + height + "], leftOffset=[" + leftOffset + "]"); + + return new SafeAreaInsets(width, height, topInset, leftOffset); + } + + /** + * Determine if device is using gesture navigation + * Gesture navigation typically has smaller navigation bar height + */ + private static boolean isGestureNavigation(@NonNull Insets navBarInsets, float density) { + int maxInset = Math.max(Math.max(navBarInsets.bottom, navBarInsets.left), navBarInsets.right); + int dpValue = (int) (maxInset / density); + return dpValue < 40; + } + + /** + * Provides the default navigation bar inset when system-provided inset is zero. + */ + private static int getDefaultNavBarInset(boolean isGestureNav, float density) { + return (int) (density * (isGestureNav ? DEFAULT_GESTURE_NAV_BAR_HEIGHT_DP : DEFAULT_BUTTON_NAV_BAR_HEIGHT_DP)); + } + + private static int getStatusBarHeight(@NonNull Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + private static int getNavigationBarHeight(@NonNull Context context, boolean isPortrait) { + int result = 0; + String resourceName = isPortrait ? "navigation_bar_height" : "navigation_bar_height_landscape"; + int resourceId = context.getResources().getIdentifier(resourceName, "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + @SuppressWarnings("deprecation") + private static boolean isNavigationBarVisible(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + DisplayMetrics realMetrics = new DisplayMetrics(); + display.getRealMetrics(realMetrics); + + DisplayMetrics displayMetrics = new DisplayMetrics(); + display.getMetrics(displayMetrics); + + return (realMetrics.widthPixels - displayMetrics.widthPixels) > 0 + || (realMetrics.heightPixels - displayMetrics.heightPixels) > 0; + } + return true; + } + + /** + * Helper class to hold inset calculations + */ + private static class SafeAreaInsets { + final int width; + final int height; + final int topOffset; + final int leftOffset; + + SafeAreaInsets(int width, int height, int topOffset, int leftOffset) { + this.width = width; + this.height = height; + this.topOffset = topOffset; + this.leftOffset = leftOffset; + } + } +} diff --git a/sdk/src/main/java/ly/count/android/sdk/SafeAreaDimensions.java b/sdk/src/main/java/ly/count/android/sdk/SafeAreaDimensions.java new file mode 100644 index 000000000..f1983292d --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/SafeAreaDimensions.java @@ -0,0 +1,27 @@ +package ly.count.android.sdk; + +/** + * Class to hold safe area dimensions and offsets + */ +class SafeAreaDimensions { + int portraitWidth; + int portraitHeight; + int landscapeWidth; + int landscapeHeight; + int portraitTopOffset; + int landscapeTopOffset; + int portraitLeftOffset; + int landscapeLeftOffset; + + SafeAreaDimensions(int portraitWidth, int portraitHeight, int landscapeWidth, int landscapeHeight, + int portraitTopOffset, int landscapeTopOffset, int portraitLeftOffset, int landscapeLeftOffset) { + this.portraitWidth = portraitWidth; + this.portraitHeight = portraitHeight; + this.landscapeWidth = landscapeWidth; + this.landscapeHeight = landscapeHeight; + this.portraitTopOffset = portraitTopOffset; + this.landscapeTopOffset = landscapeTopOffset; + this.portraitLeftOffset = portraitLeftOffset; + this.landscapeLeftOffset = landscapeLeftOffset; + } +} diff --git a/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java b/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java index 11a23c716..a510e3e72 100644 --- a/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java +++ b/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java @@ -55,8 +55,8 @@ protected void onCreate(Bundle savedInstanceState) { configLandscape = (TransparentActivityConfig) intent.getSerializableExtra(CONFIGURATION_LANDSCAPE); configPortrait = (TransparentActivityConfig) intent.getSerializableExtra(CONFIGURATION_PORTRAIT); Log.v(Countly.TAG, "[TransparentActivity] onCreate, orientation: " + currentOrientation); - Log.v(Countly.TAG, "[TransparentActivity] onCreate, configLandscape x: [" + configLandscape.x + "] y: [" + configLandscape.y + "] width: [" + configLandscape.width + "] height: [" + configLandscape.height + "]"); - Log.v(Countly.TAG, "[TransparentActivity] onCreate, configPortrait x: [" + configPortrait.x + "] y: [" + configPortrait.y + "] width: [" + configPortrait.width + "] height: [" + configPortrait.height + "]"); + Log.v(Countly.TAG, "[TransparentActivity] onCreate, configLandscape x: [" + configLandscape.x + "] y: [" + configLandscape.y + "] width: [" + configLandscape.width + "] height: [" + configLandscape.height + "], topOffset: [" + configLandscape.topOffset + "], leftOffset: [" + configLandscape.leftOffset + "]"); + Log.v(Countly.TAG, "[TransparentActivity] onCreate, configPortrait x: [" + configPortrait.x + "] y: [" + configPortrait.y + "] width: [" + configPortrait.width + "] height: [" + configPortrait.height + "], topOffset: [" + configPortrait.topOffset + "], leftOffset: [" + configPortrait.leftOffset + "]"); TransparentActivityConfig config; if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { @@ -70,13 +70,33 @@ protected void onCreate(Bundle savedInstanceState) { // Configure window layout parameters WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.gravity = Gravity.TOP | Gravity.START; // try out START - params.x = config.x; - params.y = config.y; + + int adjustedX = config.x; + int adjustedY = config.y; + + if (config.useSafeArea) { + if (config.leftOffset > 0) { + adjustedX += config.leftOffset; + Log.d(Countly.TAG, "[TransparentActivity] onCreate, using safe area mode, adjusting x from [" + config.x + "] to [" + adjustedX + "] (leftOffset: " + config.leftOffset + ")"); + } + if (config.topOffset > 0) { + adjustedY += config.topOffset; + Log.d(Countly.TAG, "[TransparentActivity] onCreate, using safe area mode, adjusting y from [" + config.y + "] to [" + adjustedY + "] (topOffset: " + config.topOffset + ")"); + } + } + + params.x = adjustedX; + params.y = adjustedY; + params.height = config.height; params.width = config.width; params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; getWindow().setAttributes(params); + + WindowManager.LayoutParams verifyParams = getWindow().getAttributes(); + Log.d(Countly.TAG, "[TransparentActivity] onCreate, AFTER setAttributes - params.x: [" + verifyParams.x + "], params.y: [" + verifyParams.y + "], params.gravity: [" + verifyParams.gravity + "], width: [" + verifyParams.width + "], height: [" + verifyParams.height + "]"); + getWindow().setBackgroundDrawableResource(android.R.color.transparent); // Create and configure the layout @@ -91,36 +111,62 @@ protected void onCreate(Bundle savedInstanceState) { } private TransparentActivityConfig setupConfig(@Nullable TransparentActivityConfig config) { - final DisplayMetrics metrics = UtilsDevice.getDisplayMetrics(this); - if (config == null) { - Log.w(Countly.TAG, "[TransparentActivity] setupConfig, Config is null, using default values with full screen size"); + Log.w(Countly.TAG, "[TransparentActivity] setupConfig, Config is null, using default values"); + final DisplayMetrics metrics = UtilsDevice.getDisplayMetrics(this); return new TransparentActivityConfig(0, 0, metrics.widthPixels, metrics.heightPixels); } - if (config.width < 1) { - config.width = metrics.widthPixels; - } - if (config.height < 1) { - config.height = metrics.heightPixels; + if (!config.useSafeArea) { + final DisplayMetrics metrics = UtilsDevice.getDisplayMetrics(this); + + if (config.width < 1) { + config.width = metrics.widthPixels; + } + if (config.height < 1) { + config.height = metrics.heightPixels; + } } + if (config.x < 1) { config.x = 0; } if (config.y < 1) { config.y = 0; } + + Log.d(Countly.TAG, "[TransparentActivity] setupConfig, final config - x: [" + config.x + "], y: [" + config.y + "], width: [" + config.width + "], height: [" + config.height + "], useSafeArea: [" + config.useSafeArea + "]"); return config; } private void resizeContent(TransparentActivityConfig config) { - Log.d(Countly.TAG, "[TransparentActivity] resizeContent, config x: [" + config.x + "] y: [" + config.y + "] width: [" + config.width + "] height: [" + config.height + "]"); + Log.d(Countly.TAG, "[TransparentActivity] resizeContent(config), config dimensions (px): [" + config.width + "x" + config.height + "], x: [" + config.x + "], y: [" + config.y + "], useSafeArea: [" + config.useSafeArea + "], topOffset: [" + config.topOffset + "], leftOffset: [" + config.leftOffset + "]"); + + int adjustedX = config.x; + int adjustedY = config.y; + + if (config.useSafeArea) { + if (config.leftOffset > 0) { + adjustedX += config.leftOffset; + Log.d(Countly.TAG, "[TransparentActivity] resizeContent(config), applying left offset, adjusted x: [" + adjustedX + "]"); + } + if (config.topOffset > 0) { + adjustedY += config.topOffset; + Log.d(Countly.TAG, "[TransparentActivity] resizeContent(config), applying top offset, adjusted y: [" + adjustedY + "]"); + } + } + WindowManager.LayoutParams params = getWindow().getAttributes(); - params.x = config.x; - params.y = config.y; + Log.d(Countly.TAG, "[TransparentActivity] resizeContent(config), BEFORE - params.x: [" + params.x + "], params.y: [" + params.y + "], params.gravity: [" + params.gravity + "]"); + params.gravity = Gravity.TOP | Gravity.START; // safe? + params.x = adjustedX; + params.y = adjustedY; params.height = config.height; params.width = config.width; getWindow().setAttributes(params); + + WindowManager.LayoutParams verifyParams = getWindow().getAttributes(); + Log.d(Countly.TAG, "[TransparentActivity] resizeContent(config), AFTER - params.x: [" + verifyParams.x + "], params.y: [" + verifyParams.y + "], params.gravity: [" + verifyParams.gravity + "], width: [" + verifyParams.width + "], height: [" + verifyParams.height + "]"); ViewGroup.LayoutParams layoutParams = relativeLayout.getLayoutParams(); layoutParams.width = config.width; @@ -131,6 +177,8 @@ private void resizeContent(TransparentActivityConfig config) { webLayoutParams.width = config.width; webLayoutParams.height = config.height; webView.setLayoutParams(webLayoutParams); + + Log.d(Countly.TAG, "[TransparentActivity] resizeContent(config), layout params set - relativeLayout: [" + layoutParams.width + "x" + layoutParams.height + "], webView: [" + webLayoutParams.width + "x" + webLayoutParams.height + "]"); } @Override @@ -146,12 +194,48 @@ public void onConfigurationChanged(android.content.res.Configuration newConfig) } private void resizeContent() { - // CHANGE SCREEN SIZE - final DisplayMetrics metrics = UtilsDevice.getDisplayMetrics(this); - int scaledWidth = (int) Math.ceil(metrics.widthPixels / metrics.density); - int scaledHeight = (int) Math.ceil(metrics.heightPixels / metrics.density); - - // refactor in the future to use the resize_me action + TransparentActivityConfig currentConfig = (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) ? configLandscape : configPortrait; + + float density = getResources().getDisplayMetrics().density; + int widthPx, heightPx; + + if (currentConfig != null && currentConfig.useSafeArea) { + Log.d(Countly.TAG, "[TransparentActivity] resizeContent, recalculating safe area dimensions for orientation change"); + + SafeAreaDimensions safeArea = SafeAreaCalculator.calculateSafeAreaDimensions(this, Countly.sharedInstance().L); + + configPortrait.topOffset = safeArea.portraitTopOffset; + configPortrait.leftOffset = safeArea.portraitLeftOffset; + configLandscape.topOffset = safeArea.landscapeTopOffset; + configLandscape.leftOffset = safeArea.landscapeLeftOffset; + + Log.d(Countly.TAG, "[TransparentActivity] resizeContent, updated offsets - Portrait: topOffset=[" + configPortrait.topOffset + "], leftOffset=[" + configPortrait.leftOffset + "]"); + Log.d(Countly.TAG, "[TransparentActivity] resizeContent, updated offsets - Landscape: topOffset=[" + configLandscape.topOffset + "], leftOffset=[" + configLandscape.leftOffset + "]"); + + int topOffset, leftOffset; + if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { + widthPx = safeArea.landscapeWidth; + heightPx = safeArea.landscapeHeight; + topOffset = safeArea.landscapeTopOffset; + leftOffset = safeArea.landscapeLeftOffset; + } else { + widthPx = safeArea.portraitWidth; + heightPx = safeArea.portraitHeight; + topOffset = safeArea.portraitTopOffset; + leftOffset = safeArea.portraitLeftOffset; + } + + Log.d(Countly.TAG, "[TransparentActivity] resizeContent, safe area mode - sending dimensions to webview (px): [" + widthPx + "x" + heightPx + "], (dp): [" + Math.round(widthPx / density) + "x" + Math.round(heightPx / density) + "], density: [" + density + "], topOffset: [" + topOffset + "], leftOffset: [" + leftOffset + "]"); + } else { + final DisplayMetrics metrics = UtilsDevice.getDisplayMetrics(this); + widthPx = metrics.widthPixels; + heightPx = metrics.heightPixels; + + Log.d(Countly.TAG, "[TransparentActivity] resizeContent, immersive mode - sending dimensions to webview (px): [" + widthPx + "x" + heightPx + "], (dp): [" + Math.round(widthPx / density) + "x" + Math.round(heightPx / density) + "], density: [" + density + "]"); + } + + int scaledWidth = Math.round(widthPx / density); + int scaledHeight = Math.round(heightPx / density); webView.loadUrl("javascript:window.postMessage({type: 'resize', width: " + scaledWidth + ", height: " + scaledHeight + "}, '*');"); } @@ -295,15 +379,32 @@ private void resizeMeAction(Map query) { Log.v(Countly.TAG, "[TransparentActivity] resizeMeAction, resize_me JSON: [" + resizeMeJson + "]"); JSONObject portrait = resizeMeJson.getJSONObject("p"); JSONObject landscape = resizeMeJson.getJSONObject("l"); + + boolean portraitUseSafeArea = configPortrait.useSafeArea; + boolean landscapeUseSafeArea = configLandscape.useSafeArea; + int portraitTopOffset = configPortrait.topOffset; + int landscapeTopOffset = configLandscape.topOffset; + int portraitLeftOffset = configPortrait.leftOffset; + int landscapeLeftOffset = configLandscape.leftOffset; + configPortrait.x = (int) Math.ceil(portrait.getInt("x") * density); configPortrait.y = (int) Math.ceil(portrait.getInt("y") * density); configPortrait.width = (int) Math.ceil(portrait.getInt("w") * density); configPortrait.height = (int) Math.ceil(portrait.getInt("h") * density); + configPortrait.useSafeArea = portraitUseSafeArea; + configPortrait.topOffset = portraitTopOffset; + configPortrait.leftOffset = portraitLeftOffset; configLandscape.x = (int) Math.ceil(landscape.getInt("x") * density); configLandscape.y = (int) Math.ceil(landscape.getInt("y") * density); configLandscape.width = (int) Math.ceil(landscape.getInt("w") * density); configLandscape.height = (int) Math.ceil(landscape.getInt("h") * density); + configLandscape.useSafeArea = landscapeUseSafeArea; + configLandscape.topOffset = landscapeTopOffset; + configLandscape.leftOffset = landscapeLeftOffset; + + Log.d(Countly.TAG, "[TransparentActivity] resizeMeAction, updated configs - Portrait: useSafeArea=[" + portraitUseSafeArea + "], topOffset=[" + portraitTopOffset + "], leftOffset=[" + portraitLeftOffset + "]"); + Log.d(Countly.TAG, "[TransparentActivity] resizeMeAction, updated configs - Landscape: useSafeArea=[" + landscapeUseSafeArea + "], topOffset=[" + landscapeTopOffset + "], leftOffset=[" + landscapeLeftOffset + "]"); resizeContentInternal(); } catch (JSONException e) { @@ -433,7 +534,10 @@ private WebView createWebView(TransparentActivityConfig config) { Countly.sharedInstance().moduleContent.notifyAfterContentIsClosed(); } } else { - hideSystemUI(); + TransparentActivityConfig currentConfig = currentOrientation == Configuration.ORIENTATION_LANDSCAPE ? configLandscape : configPortrait; + if (currentConfig != null && !currentConfig.useSafeArea) { + hideSystemUI(); + } webView.setVisibility(View.VISIBLE); } } diff --git a/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java b/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java index 5a764d66f..56791e8c0 100644 --- a/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java +++ b/sdk/src/main/java/ly/count/android/sdk/TransparentActivityConfig.java @@ -8,6 +8,9 @@ class TransparentActivityConfig implements Serializable { Integer width; Integer height; String url; + boolean useSafeArea = false; + int topOffset = 0; + int leftOffset = 0; TransparentActivityConfig(Integer x, Integer y, Integer width, Integer height) { this.x = x; diff --git a/sdk/src/main/java/ly/count/android/sdk/WebViewDisplayOption.java b/sdk/src/main/java/ly/count/android/sdk/WebViewDisplayOption.java new file mode 100644 index 000000000..60c66e90f --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/WebViewDisplayOption.java @@ -0,0 +1,17 @@ +package ly.count.android.sdk; + +/** + * Enum representing the webview display options for Content and Feedback Widgets + */ +public enum WebViewDisplayOption { + /** + * Immersive mode - calculates and sends total area, displays content in immersive mode (hiding system UI) + */ + IMMERSIVE, + + /** + * Safe area mode - calculates dimensions excluding system UI (status bar, navigation bar, cutout), + * displays content without hiding system UI + */ + SAFE_AREA +}