Skip to content

Commit 34dd8b9

Browse files
fix: tests
1 parent fd8c488 commit 34dd8b9

3 files changed

Lines changed: 65 additions & 31 deletions

File tree

sdk/src/androidTest/java/ly/count/android/sdk/ContentOverlayViewTests.java

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,19 @@ public void createWindowParams_correctTypeAndFlags() {
539539
Assert.assertEquals("Type should be TYPE_APPLICATION",
540540
WindowManager.LayoutParams.TYPE_APPLICATION, params.type);
541541

542+
// Expected base flags match the production set in createWindowParams:
543+
// FLAG_NOT_FOCUSABLE + FLAG_WATCH_OUTSIDE_TOUCH let the host
544+
// activity keep IME focus while still receiving outside-touch
545+
// events the overlay routes back via dispatchTouchEvent.
546+
// FLAG_NOT_TOUCHABLE is added only while content is still loading
547+
// (gates touches until the WebView is visible). The test
548+
// constructs the overlay with about:blank and never waits for
549+
// afterPageFinished, so isContentLoaded stays false here.
542550
int expectedFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
543551
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
544552
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
553+
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
554+
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
545555
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
546556
Assert.assertEquals("Flags should match", expectedFlags, params.flags);
547557

@@ -794,12 +804,21 @@ public void contentUrlAction_noQueryParams_returnsFalse() {
794804
// ===================== Memory leak prevention (issue #556) =====================
795805

796806
/**
797-
* Structural invariant: the overlay's View.mContext must be the Application, not the
798-
* constructing activity. This is what allows the overlay to outlive activity transitions
799-
* without leaking the activity it was first opened in.
807+
* Structural invariant: the overlay's View.mContext must not pin the
808+
* constructing Activity. The overlay outlives activity transitions, and
809+
* View.mContext can never be swapped after construction — if it's the
810+
* Activity, that Activity stays GC-pinned for the overlay's full lifetime.
800811
*
801-
* Regression guard: if anyone changes the constructor's super(...) call back to the
802-
* activity, this test will fail and surface the leak before users do.
812+
* The exact context type is API-dependent (see ContentOverlayView#resolveOverlayContext):
813+
* - Pre-API 31: Application context.
814+
* - API 31+: createConfigurationContext from the Activity — a ContextImpl
815+
* wrapper that holds an IBinder token, not the Activity instance, so
816+
* GC isn't blocked. Required for StrictMode#detectIncorrectContextUse.
817+
*
818+
* In both cases, getApplicationContext() resolves to the same Application.
819+
* The test asserts both that the context is NOT the Activity and that it
820+
* routes back to the right Application — which is the actual leak-avoidance
821+
* contract independent of API level.
803822
*/
804823
@Test
805824
public void constructor_usesApplicationContext_notActivity() {
@@ -811,15 +830,19 @@ public void constructor_usesApplicationContext_notActivity() {
811830
+ "that Activity for the lifetime of the overlay.",
812831
activity, overlay.getContext());
813832
Assert.assertSame(
814-
"ContentOverlayView.mContext must be the Application context.",
815-
activity.getApplicationContext(), overlay.getContext());
833+
"ContentOverlayView.mContext must resolve to the same Application as the "
834+
+ "constructing Activity (Application directly on <API 31, "
835+
+ "ConfigurationContext-of-Activity on API 31+).",
836+
activity.getApplicationContext(),
837+
overlay.getContext().getApplicationContext());
816838
});
817839
}
818840

819841
/**
820-
* Same invariant for the embedded WebView. Even with the wrapper View using App context,
821-
* a WebView constructed with Activity context would still pin the constructing activity
822-
* via its own mContext.
842+
* Same invariant for the embedded WebView. Even with the wrapper View not
843+
* pinning the Activity, a WebView constructed with the Activity directly
844+
* would still pin it via its own mContext. See
845+
* constructor_usesApplicationContext_notActivity for the API-level rationale.
823846
*/
824847
@Test
825848
public void webView_usesApplicationContext_notActivity() {
@@ -830,8 +853,10 @@ public void webView_usesApplicationContext_notActivity() {
830853
"ContentOverlayView's WebView.mContext must not be the constructing Activity.",
831854
activity, overlay.webView.getContext());
832855
Assert.assertSame(
833-
"ContentOverlayView's WebView.mContext must be the Application context.",
834-
activity.getApplicationContext(), overlay.webView.getContext());
856+
"ContentOverlayView's WebView.mContext must resolve to the same Application "
857+
+ "as the constructing Activity.",
858+
activity.getApplicationContext(),
859+
overlay.webView.getContext().getApplicationContext());
835860
});
836861
}
837862
}

sdk/src/androidTest/java/ly/count/android/sdk/CountlyStoreExplicitModeTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ public class CountlyStoreExplicitModeTests {
4343

4444
@Before
4545
public void setUp() {
46+
// Reset the shared Countly singleton — without this, init() state from a prior
47+
// test class in the suite leaks into our new Countly() instances and dirties the
48+
// event/request caches before the "this should perform no write" assertions can
49+
// measure them. The other suites (ContentOverlayViewTests,
50+
// ModuleConfigurationTests, ...) do the same halt+clear in setUp; this test class
51+
// was the odd one out and produced ordering-dependent flakes.
52+
Countly.sharedInstance().halt();
53+
TestUtils.getCountlyStore().clear();
54+
4655
Countly.sharedInstance().setLoggingEnabled(true);
4756
store = new CountlyStore(TestUtils.getContext(), mock(ModuleLog.class), false);
4857
sp = store;

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -139,27 +139,27 @@ private void registerActivityLifecycleCallback(@NonNull Activity activity) {
139139
* is true memory growth:
140140
*
141141
* 1. System-singleton reattachment retention: when the same overlay is shown across
142-
* multiple activity transitions (View.mWindowAttachCount grows), various Android
143-
* system singletons hold a reference to the currently-attached ViewRootImpl. The
144-
* two retainers we've observed are InputMethodManager#mCurRootView (set when the
145-
* user types into the WebView) and WindowManagerGlobal#mRoots (the process-wide
146-
* list of attached ViewRootImpls, always populated for any attached window).
147-
* LeakCanary may report the overlay as "leaking" while its own analyzer also
148-
* reports the View is currently attached — that combination is the heuristic
149-
* false-positive signal (one overlay reused, not N overlays leaked). Both
150-
* references are released by the framework when the overlay's window is detached
151-
* or rebound to another window.
142+
* multiple activity transitions (View.mWindowAttachCount grows), various Android
143+
* system singletons hold a reference to the currently-attached ViewRootImpl. The
144+
* two retainers we've observed are InputMethodManager#mCurRootView (set when the
145+
* user types into the WebView) and WindowManagerGlobal#mRoots (the process-wide
146+
* list of attached ViewRootImpls, always populated for any attached window).
147+
* LeakCanary may report the overlay as "leaking" while its own analyzer also
148+
* reports the View is currently attached — that combination is the heuristic
149+
* false-positive signal (one overlay reused, not N overlays leaked). Both
150+
* references are released by the framework when the overlay's window is detached
151+
* or rebound to another window.
152152
*
153153
* 2. ModuleContent.contentOverlay retention while backgrounded: when no activities
154-
* are visible, ModuleContent.onActivityStopped(count=0) calls detachFromWindow on
155-
* the overlay (to avoid WindowLeaked) but intentionally keeps the contentOverlay
156-
* field non-null so the same instance can be re-attached when the user returns.
157-
* LeakCanary sees a detached View still strongly referenced through the Countly
158-
* singleton and reports a "leak". This is bounded (single field, one overlay
159-
* instance) and released the next time ModuleContent replaces or clears the cached
160-
* overlay reference. Not a growing-over-time leak — intentional caching for the
161-
* user-returns-to-same-content UX. Process kill (SIGKILL) reclaims it along with
162-
* everything else, so persistence-on-kill is a non-concern.
154+
* are visible, ModuleContent.onActivityStopped(count=0) calls detachFromWindow on
155+
* the overlay (to avoid WindowLeaked) but intentionally keeps the contentOverlay
156+
* field non-null so the same instance can be re-attached when the user returns.
157+
* LeakCanary sees a detached View still strongly referenced through the Countly
158+
* singleton and reports a "leak". This is bounded (single field, one overlay
159+
* instance) and released the next time ModuleContent replaces or clears the cached
160+
* overlay reference. Not a growing-over-time leak — intentional caching for the
161+
* user-returns-to-same-content UX. Process kill (SIGKILL) reclaims it along with
162+
* everything else, so persistence-on-kill is a non-concern.
163163
*/
164164
private static final class OverlayLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
165165
private final WeakReference<ContentOverlayView> overlayRef;

0 commit comments

Comments
 (0)