Skip to content

Commit bb26fd5

Browse files
feat: strict mode fix context usage violations
1 parent 18e4bd4 commit bb26fd5

3 files changed

Lines changed: 56 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## XX.XX.XX
22
* Improved user properties auto-save conditions to flush event queue with every user property call.
33

4+
* Mitigated StrictMode `IncorrectContextUseViolation` warnings logged when the SDK retrieved device display metrics and constructed the content overlay view from a non-UI context.
5+
46
## 26.1.2
57
* 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.
68
* Added `CountlyConfig.setInitialActivity(Activity)` as an explicit way for wrapper SDKs to provide the host activity during initialization.

sdk/src/main/java/ly/count/android/sdk/ContentOverlayView.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,36 @@ class ContentOverlayView extends FrameLayout {
5656
private ComponentCallbacks orientationCallback;
5757
private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks;
5858

59+
// Returns a Context suitable for constructing the overlay's Views without retaining
60+
// a strong Java reference to the constructing Activity:
61+
// - Pre-API 31: Application context (current behavior; no StrictMode UI-context check exists).
62+
// - API 31+: createConfigurationContext from the Activity. The returned ContextImpl has
63+
// mIsUiContext=true (inherited from Activity), satisfying detectIncorrectContextUse,
64+
// but holds no Java reference back to the Activity — only an IBinder activity token,
65+
// which does not pin the Activity for GC.
66+
@NonNull
67+
private static Context resolveOverlayContext(@NonNull Activity activity) {
68+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
69+
try {
70+
return activity.createConfigurationContext(activity.getResources().getConfiguration());
71+
} catch (Throwable ignored) {
72+
// Fall back to Application context if config-context creation fails.
73+
}
74+
}
75+
return activity.getApplicationContext();
76+
}
77+
5978
@SuppressLint("SetJavaScriptEnabled") ContentOverlayView(@NonNull Activity activity,
6079
@NonNull TransparentActivityConfig portrait,
6180
@NonNull TransparentActivityConfig landscape,
6281
int orientation,
6382
@Nullable ContentCallback callback,
6483
@NonNull Runnable onClose) {
65-
super(activity);
84+
// View.mContext must not pin the constructing activity (overlay outlives activity
85+
// transitions; window attachment uses currentHostActivity). On API 31+ we additionally
86+
// need a UI context to satisfy StrictMode#detectIncorrectContextUse — see
87+
// resolveOverlayContext above.
88+
super(resolveOverlayContext(activity));
6689

6790
this.configPortrait = portrait;
6891
this.configLandscape = landscape;
@@ -900,7 +923,10 @@ private void cleanupWebView() {
900923

901924
@SuppressLint("SetJavaScriptEnabled")
902925
private WebView createWebView(@NonNull Activity activity, @NonNull TransparentActivityConfig config) {
903-
WebView wv = new CountlyWebView(activity);
926+
// WebView's mContext must not retain the constructing activity, since the overlay
927+
// (and its WebView) outlives activity transitions. Activity-specific operations route
928+
// through currentHostActivity. See resolveOverlayContext for the API 31+ UI-context handling.
929+
WebView wv = new CountlyWebView(resolveOverlayContext(activity));
904930
wv.setVisibility(View.INVISIBLE);
905931
LayoutParams webLayoutParams = new LayoutParams(
906932
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

sdk/src/main/java/ly/count/android/sdk/UtilsDevice.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ private UtilsDevice() {
2626

2727
@NonNull
2828
static DisplayMetrics getDisplayMetrics(@NonNull final Context context) {
29-
final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
29+
final WindowManager wm = obtainWindowManager(context);
3030
final DisplayMetrics metrics = new DisplayMetrics();
3131

3232
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -37,6 +37,31 @@ static DisplayMetrics getDisplayMetrics(@NonNull final Context context) {
3737
return metrics;
3838
}
3939

40+
// On API 31+, getSystemService(WINDOW_SERVICE) from a non-UI context trips
41+
// StrictMode#detectIncorrectContextUse. Prefer a UI context when one is
42+
// available (held foreground Activity, then createWindowContext fallback)
43+
// and only resolve WindowManager from it.
44+
@NonNull
45+
private static WindowManager obtainWindowManager(@NonNull Context context) {
46+
if (context instanceof Activity) {
47+
return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
48+
}
49+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
50+
Activity held = CountlyActivityHolder.getInstance().getActivity();
51+
if (held != null) {
52+
return (WindowManager) held.getSystemService(Context.WINDOW_SERVICE);
53+
}
54+
try {
55+
Context uiContext = context.createWindowContext(
56+
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, null);
57+
return (WindowManager) uiContext.getSystemService(Context.WINDOW_SERVICE);
58+
} catch (Throwable ignored) {
59+
// Fall through to original context if window context creation is rejected.
60+
}
61+
}
62+
return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
63+
}
64+
4065
@TargetApi(Build.VERSION_CODES.R)
4166
private static void applyWindowMetrics(@NonNull Context context,
4267
@NonNull WindowManager wm,

0 commit comments

Comments
 (0)