Skip to content

Commit b1768d6

Browse files
fix: direct key events to background view for contents
1 parent 18e4bd4 commit b1768d6

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

CHANGELOG.md

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

4+
* Mitigated an issue where content overlays and feedback widgets prevented keyboard input on the underlying activity's text fields while displayed.
5+
* 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.
6+
47
## 26.1.2
58
* 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.
69
* 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: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import android.util.Log;
1616
import android.view.Gravity;
1717
import android.view.KeyEvent;
18+
import android.view.MotionEvent;
1819
import android.view.View;
1920
import android.view.ViewGroup;
2021
import android.view.ViewTreeObserver;
@@ -187,6 +188,50 @@ public boolean dispatchKeyEvent(KeyEvent event) {
187188
return super.dispatchKeyEvent(event);
188189
}
189190

191+
@Override
192+
public boolean dispatchTouchEvent(MotionEvent ev) {
193+
// Dynamically transfer IME focus between the overlay and the host activity:
194+
// - ACTION_OUTSIDE (delivered because of FLAG_WATCH_OUTSIDE_TOUCH): user
195+
// touched outside the overlay's bounds; the touch already passed through
196+
// to the activity via FLAG_NOT_TOUCH_MODAL, but we additionally restore
197+
// FLAG_NOT_FOCUSABLE so the activity's EditText can claim IME focus.
198+
// - ACTION_DOWN (touch inside the overlay's bounds): clear FLAG_NOT_FOCUSABLE
199+
// so the WebView's form fields can bring up the keyboard.
200+
// Consumes nothing; existing touch handling (WebView, etc.) continues normally.
201+
if (ev.getAction() == MotionEvent.ACTION_OUTSIDE) {
202+
setWindowFocusable(false);
203+
return true;
204+
}
205+
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
206+
setWindowFocusable(true);
207+
}
208+
return super.dispatchTouchEvent(ev);
209+
}
210+
211+
private void setWindowFocusable(boolean focusable) {
212+
if (!isAddedToWindow || windowManager == null) {
213+
return;
214+
}
215+
try {
216+
WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
217+
if (lp == null) {
218+
return;
219+
}
220+
boolean alreadyFocusable = (lp.flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0;
221+
if (alreadyFocusable == focusable) {
222+
return;
223+
}
224+
if (focusable) {
225+
lp.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
226+
} else {
227+
lp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
228+
}
229+
windowManager.updateViewLayout(this, lp);
230+
} catch (Exception e) {
231+
Log.w(Countly.TAG, "[ContentOverlayView] setWindowFocusable, failed to update flags", e);
232+
}
233+
}
234+
190235
private TransparentActivityConfig getCurrentConfig() {
191236
if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
192237
return configLandscape;
@@ -210,9 +255,19 @@ private WindowManager.LayoutParams createWindowParams(@NonNull Activity activity
210255
}
211256
}
212257

258+
// FLAG_NOT_FOCUSABLE: overlay does NOT grab IME focus by default, so the
259+
// underlying Activity's EditText can receive keyboard input even while the
260+
// overlay is shown. Toggled off in dispatchTouchEvent on inside touches so
261+
// the WebView's <input>/<textarea> can still bring up the keyboard.
262+
// FLAG_WATCH_OUTSIDE_TOUCH: deliver an ACTION_OUTSIDE event to the overlay
263+
// when the user touches outside its bounds, so we can return focus to the
264+
// non-focusable state. The actual touch still passes through to the host
265+
// activity via FLAG_NOT_TOUCH_MODAL.
213266
int flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
214267
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
215-
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
268+
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
269+
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
270+
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
216271

217272
if (!isContentLoaded) {
218273
flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
@@ -358,6 +413,25 @@ private void enableTouchInteraction() {
358413

359414
private void removeFromWindow() {
360415
if (isAddedToWindow && windowManager != null) {
416+
// Expedite any pending scrollbar-fade Runnables: scrolling inside the WebView
417+
// schedules a ScrollabilityCache fade Runnable on the main MessageQueue. If
418+
// the View is detached before that Runnable fires, the pending Message keeps
419+
// ViewRootImpl alive (and through it, this overlay) for ~550ms — visible as
420+
// a transient leak under repeated show/close cycles. Calling awakenScrollBars(0)
421+
// re-schedules the existing fade with zero delay, so the Message drains on the
422+
// next message-loop iteration (~16ms) and the View becomes GC-eligible promptly.
423+
// No-op if mScrollCache wasn't created (no scrolling occurred).
424+
try {
425+
// CountlyWebView#expediteScrollbarFade exposes protected View#awakenScrollBars(int)
426+
// (only callable through inheritance, hence the wrapper on the subclass).
427+
if (webView instanceof CountlyWebView) {
428+
((CountlyWebView) webView).expediteScrollbarFade();
429+
}
430+
awakenScrollBars(0);
431+
} catch (Exception ignored) {
432+
// Public API, but defensive against any edge-case throws during teardown.
433+
}
434+
361435
try {
362436
// Use removeViewImmediate for synchronous removal to prevent WindowLeaked.
363437
// WindowManager.removeView() is async (posts MSG_DIE), so the view may still

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,14 @@ public CountlyWebView(Context context) {
1515
public boolean onCheckIsTextEditor() {
1616
return true;
1717
}
18+
19+
/**
20+
* Expedites any pending scrollbar-fade Runnable by re-scheduling it with zero delay.
21+
* Used by ContentOverlayView before detach to drain MessageQueue entries that would
22+
* otherwise hold the ViewRootImpl alive for ~550ms (transient leak under stress).
23+
* No-op if no scroll cache exists. Calls protected View#awakenScrollBars(int).
24+
*/
25+
void expediteScrollbarFade() {
26+
awakenScrollBars(0);
27+
}
1828
}

0 commit comments

Comments
 (0)