diff --git a/CHANGELOG.md b/CHANGELOG.md index 373b53d97..1dd6be92a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Added gradle configuration cache support to upload symbols plugin. * Improved user properties auto-save conditions to flush event queue with every user property call. +* Mitigated StrictMode `IncorrectContextUseViolation` warnings logged when the SDK retrieved device display metrics and constructed the content overlay view from a non-UI context. * Mitigated an issue where content overlays and feedback widgets prevented keyboard input on the underlying activity's text fields while displayed. * Mitigated a memory retention issue where content overlays and feedback widgets could be briefly held in memory after closing, surfacing under repeated open/close cycles. * Mitigated a memory leak where the content overlay retained the activity it was first opened in across subsequent activity transitions. diff --git a/sdk/src/main/java/ly/count/android/sdk/ContentOverlayView.java b/sdk/src/main/java/ly/count/android/sdk/ContentOverlayView.java index ea7539262..fe300f658 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ContentOverlayView.java +++ b/sdk/src/main/java/ly/count/android/sdk/ContentOverlayView.java @@ -57,16 +57,36 @@ class ContentOverlayView extends FrameLayout { private ComponentCallbacks orientationCallback; private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks; + // Returns a Context suitable for constructing the overlay's Views without retaining + // a strong Java reference to the constructing Activity: + // - Pre-API 31: Application context (current behavior; no StrictMode UI-context check exists). + // - API 31+: createConfigurationContext from the Activity. The returned ContextImpl has + // mIsUiContext=true (inherited from Activity), satisfying detectIncorrectContextUse, + // but holds no Java reference back to the Activity — only an IBinder activity token, + // which does not pin the Activity for GC. + @NonNull + private static Context resolveOverlayContext(@NonNull Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + return activity.createConfigurationContext(activity.getResources().getConfiguration()); + } catch (Throwable ignored) { + // Fall back to Application context if config-context creation fails. + } + } + return activity.getApplicationContext(); + } + @SuppressLint("SetJavaScriptEnabled") ContentOverlayView(@NonNull Activity activity, @NonNull TransparentActivityConfig portrait, @NonNull TransparentActivityConfig landscape, int orientation, @Nullable ContentCallback callback, @NonNull Runnable onClose) { - // Use Application context so View.mContext does not pin the constructing activity for - // the overlay's lifetime. The overlay is designed to outlive activity transitions; - // window attachment uses currentHostActivity (dynamically updated in attachToActivity). - super(activity.getApplicationContext()); + // View.mContext must not pin the constructing activity (overlay outlives activity + // transitions; window attachment uses currentHostActivity). On API 31+ we additionally + // need a UI context to satisfy StrictMode#detectIncorrectContextUse — see + // resolveOverlayContext above. + super(resolveOverlayContext(activity)); this.configPortrait = portrait; this.configLandscape = landscape; @@ -997,9 +1017,10 @@ private void cleanupWebView() { @SuppressLint("SetJavaScriptEnabled") private WebView createWebView(@NonNull Activity activity, @NonNull TransparentActivityConfig config) { - // Application context: WebView's mContext must not retain the constructing activity, since the overlay - // (and its WebView) outlives activity transitions. Activity-specific operations route through currentHostActivity. - WebView wv = new CountlyWebView(activity.getApplicationContext()); + // WebView's mContext must not retain the constructing activity, since the overlay + // (and its WebView) outlives activity transitions. Activity-specific operations route + // through currentHostActivity. See resolveOverlayContext for the API 31+ UI-context handling. + WebView wv = new CountlyWebView(resolveOverlayContext(activity)); wv.setVisibility(View.INVISIBLE); LayoutParams webLayoutParams = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); diff --git a/sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java b/sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java index cc155d0ac..794a4f19e 100644 --- a/sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java +++ b/sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java @@ -26,7 +26,7 @@ private UtilsDevice() { @NonNull static DisplayMetrics getDisplayMetrics(@NonNull final Context context) { - final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + final WindowManager wm = obtainWindowManager(context); final DisplayMetrics metrics = new DisplayMetrics(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -37,6 +37,31 @@ static DisplayMetrics getDisplayMetrics(@NonNull final Context context) { return metrics; } + // On API 31+, getSystemService(WINDOW_SERVICE) from a non-UI context trips + // StrictMode#detectIncorrectContextUse. Prefer a UI context when one is + // available (held foreground Activity, then createWindowContext fallback) + // and only resolve WindowManager from it. + @NonNull + private static WindowManager obtainWindowManager(@NonNull Context context) { + if (context instanceof Activity) { + return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Activity held = CountlyActivityHolder.getInstance().getActivity(); + if (held != null) { + return (WindowManager) held.getSystemService(Context.WINDOW_SERVICE); + } + try { + Context uiContext = context.createWindowContext( + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null); + return (WindowManager) uiContext.getSystemService(Context.WINDOW_SERVICE); + } catch (Throwable ignored) { + // Fall through to original context if window context creation is rejected. + } + } + return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } + @TargetApi(Build.VERSION_CODES.R) private static void applyWindowMetrics(@NonNull Context context, @NonNull WindowManager wm,