diff --git a/CHANGELOG.md b/CHANGELOG.md index 0132a231a..922b9c698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## XX.XX.XX +* Mitigated an issue where content sizing was handled incorrectly on API level 35 and above. + ## 25.4.3 * Improved Health Check metric information. * Improved Content display mechanics. diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java index c9fe0621b..8a98efaa3 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ConnectionProcessorTests.java @@ -126,6 +126,10 @@ public void setUp() { @Override public int getBOMDuration() { return 60; } + + @Override public boolean getUseCutoutArea() { + return false; + } }; Countly.sharedInstance().setLoggingEnabled(true); diff --git a/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java b/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java index 870f0f3c1..9b258ba10 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConfigContent.java @@ -4,6 +4,7 @@ public class ConfigContent { int zoneTimerInterval = 30; ContentCallback globalContentCallback = null; + Boolean cutoutArea = false; /** * Set the interval for the automatic content update calls @@ -30,4 +31,16 @@ public synchronized ConfigContent setGlobalContentCallback(ContentCallback callb this.globalContentCallback = callback; return this; } + + /** + * Enable cutout area support for content + * When enabled, SDK will use cutout area to show content + * + * @return config content to chain calls + * @apiNote This is an EXPERIMENTAL feature, and it can have breaking changes + */ + public synchronized ConfigContent useCutoutArea() { + this.cutoutArea = true; + return this; + } } diff --git a/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java index bdb3ec50b..a390b1a4f 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConfigurationProvider.java @@ -29,4 +29,6 @@ interface ConfigurationProvider { int getBOMRequestAge(); int getBOMDuration(); + + boolean getUseCutoutArea(); } diff --git a/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java b/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java index 29e2b08e6..dc8432f6e 100644 --- a/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java +++ b/sdk/src/main/java/ly/count/android/sdk/DeviceInfo.java @@ -38,8 +38,6 @@ of this software and associated documentation files (the "Software"), to deal import android.os.StatFs; import android.telephony.TelephonyManager; import android.util.DisplayMetrics; -import android.view.Display; -import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; @@ -129,19 +127,10 @@ public String getResolution(@NonNull final Context context) { return resolution; } - /** - * Return the display metrics collected from the WindowManager in the specified context. - * @param context context to use to retrieve the current WindowManager - * @return the display metrics of the current default display - */ @NonNull @Override public DisplayMetrics getDisplayMetrics(@NonNull final Context context) { - final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - final Display display = wm.getDefaultDisplay(); - final DisplayMetrics metrics = new DisplayMetrics(); - display.getMetrics(metrics); - return metrics; + return UtilsDevice.getDisplayMetrics(context); } /** diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java index a3b2f137a..e6c7ecb59 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleConfiguration.java @@ -494,4 +494,8 @@ public boolean getTrackingEnabled() { @Override public int getBOMDuration() { return currentVBOMDuration; } + + @Override public boolean getUseCutoutArea() { + return _cly.config_.content.cutoutArea; + } } 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 47bc6df8f..838dfe474 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -94,6 +94,7 @@ void fetchContentsInternal(@NonNull String[] categories) { intent.putExtra(TransparentActivity.CONFIGURATION_LANDSCAPE, placementCoordinates.get(Configuration.ORIENTATION_LANDSCAPE)); intent.putExtra(TransparentActivity.CONFIGURATION_PORTRAIT, placementCoordinates.get(Configuration.ORIENTATION_PORTRAIT)); intent.putExtra(TransparentActivity.ORIENTATION, _cly.context_.getResources().getConfiguration().orientation); + intent.putExtra(TransparentActivity.USE_CUTOUT, configProvider.getUseCutoutArea()); Long id = System.currentTimeMillis(); intent.putExtra(TransparentActivity.ID_CALLBACK, id); @@ -226,7 +227,6 @@ 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)); config.url = content; return config; 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 49c4635c7..386dbe09d 100644 --- a/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java +++ b/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java @@ -2,15 +2,14 @@ import android.annotation.SuppressLint; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Color; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; -import android.view.Display; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -33,6 +32,7 @@ public class TransparentActivity extends Activity { static final String ORIENTATION = "orientation"; static final String WIDGET_INFO = "widget_info"; static final String ID_CALLBACK = "id_callback"; + static final String USE_CUTOUT = "use_cutout"; int currentOrientation = 0; long ID = -1; TransparentActivityConfig configLandscape = null; @@ -47,7 +47,6 @@ protected void onCreate(Bundle savedInstanceState) { // there is a stripe at the top of the screen for contents // we eliminate it with hiding the system ui - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); super.onCreate(savedInstanceState); overridePendingTransition(0, 0); @@ -72,13 +71,19 @@ protected void onCreate(Bundle savedInstanceState) { // Configure window layout parameters WindowManager.LayoutParams params = new WindowManager.LayoutParams(); - params.gravity = Gravity.TOP | Gravity.LEFT; // try out START + params.gravity = Gravity.TOP | Gravity.START; // try out START params.x = config.x; params.y = config.y; params.height = config.height; params.width = config.width; params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; + + boolean useCutoutArea = intent.getBooleanExtra(USE_CUTOUT, false); + if (useCutoutArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + // If this is disabled, UtilsDevice line 61 needs to be changed to subtract cutout always + } getWindow().setAttributes(params); getWindow().setBackgroundDrawableResource(android.R.color.transparent); @@ -94,10 +99,7 @@ protected void onCreate(Bundle savedInstanceState) { } private TransparentActivityConfig setupConfig(@Nullable TransparentActivityConfig config) { - final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - final Display display = wm.getDefaultDisplay(); - final DisplayMetrics metrics = new DisplayMetrics(); // this gets all - display.getMetrics(metrics); + 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"); @@ -148,12 +150,12 @@ public void onConfigurationChanged(android.content.res.Configuration newConfig) currentOrientation = newConfig.orientation; } - // CHANGE SCREEN SIZE - final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - final Display display = wm.getDefaultDisplay(); - final DisplayMetrics metrics = new DisplayMetrics(); - display.getMetrics(metrics); + resizeContent(); + } + 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); @@ -161,6 +163,26 @@ public void onConfigurationChanged(android.content.res.Configuration newConfig) webView.loadUrl("javascript:window.postMessage({type: 'resize', width: " + scaledWidth + ", height: " + scaledHeight + "}, '*');"); } + @Override + public void onDestroy() { + close(new HashMap<>()); + + if (Countly.sharedInstance().isInitialized()) { + Countly.sharedInstance().moduleContent.notifyAfterContentIsClosed(); + } + super.onDestroy(); + } + + private void hideSystemUI() { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN); + } + private void resizeContentInternal() { switch (currentOrientation) { case Configuration.ORIENTATION_LANDSCAPE: @@ -274,11 +296,7 @@ private void resizeMeAction(Map query) { return; } try { - final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - final Display display = wm.getDefaultDisplay(); - final DisplayMetrics metrics = new DisplayMetrics(); - display.getMetrics(metrics); - + final DisplayMetrics metrics = UtilsDevice.getDisplayMetrics(this); float density = metrics.density; JSONObject resizeMeJson = (JSONObject) resizeMe; @@ -414,15 +432,18 @@ private WebView createWebView(TransparentActivityConfig config) { return false; } }); - client.afterPageFinished = (closeIt) -> { - if (closeIt) { - close(new HashMap<>()); + client.afterPageFinished = new WebViewPageLoadedListener() { + @Override public void onPageLoaded(boolean timedOut) { + if (timedOut) { + close(new HashMap<>()); - if (Countly.sharedInstance().isInitialized()) { - Countly.sharedInstance().moduleContent.notifyAfterContentIsClosed(); + if (Countly.sharedInstance().isInitialized()) { + Countly.sharedInstance().moduleContent.notifyAfterContentIsClosed(); + } + } else { + hideSystemUI(); + webView.setVisibility(View.VISIBLE); } - } else { - webView.setVisibility(View.VISIBLE); } }; webView.setWebViewClient(client); diff --git a/sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java b/sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java new file mode 100644 index 000000000..3c18a47af --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java @@ -0,0 +1,109 @@ +package ly.count.android.sdk; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.graphics.Insets; +import android.graphics.Rect; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import androidx.annotation.NonNull; + +final class UtilsDevice { + private UtilsDevice() { + } + + @NonNull + static DisplayMetrics getDisplayMetrics(@NonNull final Context context) { + final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + final DisplayMetrics metrics = new DisplayMetrics(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + applyWindowMetrics(context, wm, metrics); + } else { + applyLegacyMetrics(wm, metrics); + } + return metrics; + } + + @TargetApi(Build.VERSION_CODES.R) + private static void applyWindowMetrics(@NonNull Context context, + @NonNull WindowManager wm, + @NonNull DisplayMetrics outMetrics) { + final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); + final WindowInsets windowInsets = windowMetrics.getWindowInsets(); + + boolean useCutoutArea = false; + + if (Countly.sharedInstance().isInitialized()) { + useCutoutArea = Countly.sharedInstance().config_.configProvider.getUseCutoutArea(); + } + + // Always respect status bar & cutout (they affect safe area even in fullscreen) + int types = 0; + boolean usePhysicalScreenSize = !(context instanceof Activity); + + // If not activity, we can't know system UI visibility, so always use physical screen size + if (!usePhysicalScreenSize) { + // Only subtract navigation bar insets when navigation bar is actually visible + if (windowInsets.isVisible(WindowInsets.Type.navigationBars())) { + types |= WindowInsets.Type.navigationBars(); + } + + if (windowInsets.isVisible(WindowInsets.Type.statusBars())) { + types |= WindowInsets.Type.statusBars(); + } + + if (useCutoutArea) { + boolean drawUnderCutout; + WindowManager.LayoutParams params = ((Activity) context).getWindow().getAttributes(); + drawUnderCutout = params.layoutInDisplayCutoutMode + == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + + // Only subtract display cutout insets when not allowed to draw under the cutout + if (!drawUnderCutout && windowInsets.isVisible(WindowInsets.Type.displayCutout())) { + types |= WindowInsets.Type.displayCutout(); + } + + // Only subtract display cutout insets when not allowed to draw under the cutout + if (windowInsets.isVisible(WindowInsets.Type.displayCutout())) { + types |= WindowInsets.Type.displayCutout(); + } + } + } + + if (!useCutoutArea) { + // Cutout is always respected as safe area for now even in fullscreen mode + if (windowInsets.isVisible(WindowInsets.Type.displayCutout())) { + types |= WindowInsets.Type.displayCutout(); + } + } + + final Insets insets = windowInsets.getInsets(types); + final Rect bounds = windowMetrics.getBounds(); + final int width = bounds.width() - insets.left - insets.right; + final int height = bounds.height() - insets.top - insets.bottom; + + outMetrics.widthPixels = width; + outMetrics.heightPixels = height; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + outMetrics.density = windowMetrics.getDensity(); + } else { + // Fallback: use resource-based density + outMetrics.density = context.getResources().getDisplayMetrics().density; + } + } + + @SuppressWarnings("deprecation") + private static void applyLegacyMetrics(@NonNull WindowManager wm, + @NonNull DisplayMetrics outMetrics) { + final Display display = wm.getDefaultDisplay(); + display.getRealMetrics(outMetrics); + //getMetrics gives us size minus navigation bar + } +}