From bb26fd534006861ab7cae23823641fe103a1fe45 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 1 May 2026 17:01:52 +0300 Subject: [PATCH] feat: strict mode fix context usage violations --- CHANGELOG.md | 2 ++ .../count/android/sdk/ContentOverlayView.java | 30 +++++++++++++++++-- .../ly/count/android/sdk/UtilsDevice.java | 27 ++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 118c24a9f..99624468b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## XX.XX.XX * 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. + ## 26.1.2 * Added `CountlyInitProvider` ContentProvider to register activity lifecycle callbacks before `Application.onCreate()`. This ensures the SDK captures the current activity in single-activity frameworks (Flutter, React Native) and apps with deferred initialization. * Added `CountlyConfig.setInitialActivity(Activity)` as an explicit way for wrapper SDKs to provide the host activity during initialization. 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 18507be95..0a5ba7edc 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ContentOverlayView.java +++ b/sdk/src/main/java/ly/count/android/sdk/ContentOverlayView.java @@ -56,13 +56,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) { - super(activity); + // 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; @@ -900,7 +923,10 @@ private void cleanupWebView() { @SuppressLint("SetJavaScriptEnabled") private WebView createWebView(@NonNull Activity activity, @NonNull TransparentActivityConfig config) { - WebView wv = new CountlyWebView(activity); + // 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,