0

[PointerLockOnAndroid] Add auto releasing the pointer and route capture change event to blink

Changes:
- Listen on the capturing view focus changes and release the pointer when it goes out of focus
- Route the `onPointerCaptureChange` to WindowAndroid & RWHVA to update the capturing view state accordingly

Bug: b:395839333
Change-Id: Ifa62f8aedf0cc4eba52655358c78e49f3f9bad78
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6317445
Reviewed-by: Mustaq Ahmed <mustaq@chromium.org>
Commit-Queue: Abdelrahman Eed <abdoeed@google.com>
Reviewed-by: Bo Liu <boliu@chromium.org>
Reviewed-by: Robert Flack <flackr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1431402}
This commit is contained in:
Abdelrahman Eed
2025-03-12 03:22:32 -07:00
committed by Chromium LUCI CQ
parent 335f039762
commit 0c78621a81
11 changed files with 183 additions and 13 deletions

@ -2374,6 +2374,10 @@ void RenderWidgetHostViewAndroid::UnlockPointer() {
host_->LostPointerLock();
}
void RenderWidgetHostViewAndroid::OnPointerLockRelease() {
host_->LostPointerLock();
}
// Methods called from the host to the render
void RenderWidgetHostViewAndroid::SendKeyEvent(

@ -260,6 +260,7 @@ class CONTENT_EXPORT RenderWidgetHostViewAndroid
std::optional<base::TimeDelta> deadline_override) override;
void NotifyVirtualKeyboardOverlayRect(
const gfx::Rect& keyboard_rect) override;
void OnPointerLockRelease() override;
// ui::ViewAndroidObserver implementation:
void OnAttachedToWindow() override;

@ -675,6 +675,7 @@ robolectric_library("ui_android_junit_tests") {
"junit/src/org/chromium/ui/base/EventOffsetHandlerTest.java",
"junit/src/org/chromium/ui/base/LocalizationUtilsTest.java",
"junit/src/org/chromium/ui/base/MimeTypeUtilsTest.java",
"junit/src/org/chromium/ui/base/PointerLockTest.java",
"junit/src/org/chromium/ui/base/SelectFileDialogTest.java",
"junit/src/org/chromium/ui/display/DisplayAndroidTest.java",
"junit/src/org/chromium/ui/display/DisplayUtilTest.java",

@ -4,6 +4,8 @@
package org.chromium.ui.base;
import static androidx.annotation.VisibleForTesting.PRIVATE;
import static org.chromium.build.NullUtil.assumeNonNull;
import android.animation.Animator;
@ -134,10 +136,10 @@ public class WindowAndroid
private boolean mHasFocus = true;
private @Nullable OverlayTransformApiHelper mOverlayTransformApiHelper;
// TODO(crbug.com/395839333): make sure that these references are cleared when the pointer
// capturing view goes out of focus
private @Nullable View mPointerLockingView;
private @Nullable View mPointerLockChangeView;
private View.@Nullable OnFocusChangeListener mPointerLockingViewFocusChangeListener;
private View.@Nullable OnFocusChangeListener mPointerLockingViewPrvFocusChangeListener;
// The information required to draw a replica of the progress bar drawn in
// java UI in composited UI.
@ -1290,7 +1292,8 @@ public class WindowAndroid
}
@CalledByNative
private boolean requestPointerLock(View view) {
@VisibleForTesting(otherwise = PRIVATE)
public boolean requestPointerLock(View view) {
assert mPointerLockChangeView == null;
assert mPointerLockingView == null;
@ -1313,8 +1316,12 @@ public class WindowAndroid
decorViewGroup.addView(mPointerLockChangeView);
}
// TODO(crbug.com/395839333): Listen on view focus changes for the capturing view & release
// the pointer if it goes out of focus
mPointerLockingViewFocusChangeListener =
(view2, hasFocus) -> onPointerLockingViewFocusChange(hasFocus);
mPointerLockingViewPrvFocusChangeListener = view.getOnFocusChangeListener();
view.setOnFocusChangeListener(mPointerLockingViewFocusChangeListener);
// Pointer lock API equivalent on Android is called pointer capture
view.requestPointerCapture();
mPointerLockingView = view;
@ -1322,29 +1329,67 @@ public class WindowAndroid
}
@CalledByNative
private void releasePointerLock(View view) {
assert mPointerLockingView != null;
assert view == mPointerLockingView;
mPointerLockingView.releasePointerCapture();
removePointerLockViews();
@VisibleForTesting(otherwise = PRIVATE)
public void releasePointerLock(View view) {
releasePointerLockHelper(view, true, false);
}
private void onPointerLockChangeEvent(boolean hasLock) {
// TODO(crbug.com/395839333): Forward lock change event to the pointer locking view
assert mPointerLockingView != null;
if (!hasLock) {
removePointerLockViews();
releasePointerLockHelper(mPointerLockingView, false, true);
}
}
private void onPointerLockingViewFocusChange(boolean hasFocus) {
assert mPointerLockingView != null;
if (mPointerLockingViewPrvFocusChangeListener != null) {
mPointerLockingViewPrvFocusChangeListener.onFocusChange(mPointerLockingView, hasFocus);
}
if (!hasFocus) {
releasePointerLockHelper(mPointerLockingView, true, true);
}
}
private void releasePointerLockHelper(
View view, boolean callReleasePointerForView, boolean callbackNativeWindow) {
assert mPointerLockingView != null;
assert view == mPointerLockingView;
if (callReleasePointerForView) {
mPointerLockingView.releasePointerCapture();
}
if (callbackNativeWindow && mNativeWindowAndroid != 0) {
WindowAndroidJni.get().onWindowPointerLockRelease(mNativeWindowAndroid);
}
removePointerLockViews();
}
private void removePointerLockViews() {
var decorView = getDecorView();
if (mPointerLockChangeView != null && decorView instanceof ViewGroup decorViewGroup) {
decorViewGroup.removeView(mPointerLockChangeView);
}
if (mPointerLockingView != null) {
assert mPointerLockingViewFocusChangeListener != null;
if (mPointerLockingView.getOnFocusChangeListener()
!= mPointerLockingViewFocusChangeListener) {
Log.w(TAG, "Pointer locking view focus listener was changed");
} else {
mPointerLockingView.setOnFocusChangeListener(
mPointerLockingViewPrvFocusChangeListener);
}
}
mPointerLockChangeView = null;
mPointerLockingView = null;
mPointerLockingViewFocusChangeListener = null;
mPointerLockingViewPrvFocusChangeListener = null;
}
@NativeMethods
@ -1375,5 +1420,7 @@ public class WindowAndroid
void onOverlayTransformUpdated(long nativeWindowAndroid, WindowAndroid caller);
void sendUnfoldLatencyBeginTimestamp(long nativeWindowAndroid, long beginTimestampMs);
void onWindowPointerLockRelease(long nativeWindowAndroid);
}
}

@ -0,0 +1,95 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui.base;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.view.View;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
@RunWith(BaseRobolectricTestRunner.class)
public class PointerLockTest {
@Mock private View mPointerLockView;
@Mock private View mView;
private WindowAndroid mWindowAndroid;
@Before
public void setup() {
mWindowAndroid = new WindowAndroid(ContextUtils.getApplicationContext(), false);
mPointerLockView = mock(View.class);
mView = mock(View.class);
}
@After
public void tearDown() {
mWindowAndroid.destroy();
}
@Test
public void testLockPointerViewAndWindowInFocus() {
when(mPointerLockView.hasFocus()).thenReturn(true);
assertTrue(mWindowAndroid.requestPointerLock(mPointerLockView));
}
@Test
public void testLockPointerViewNotInFocus() {
when(mPointerLockView.hasFocus()).thenReturn(false);
assertFalse(mWindowAndroid.requestPointerLock(mPointerLockView));
}
@Test
public void testLockPointerWindowNotInFocus() {
when(mPointerLockView.hasFocus()).thenReturn(true);
mWindowAndroid.onWindowFocusChanged(false);
assertFalse(mWindowAndroid.requestPointerLock(mPointerLockView));
}
@Test
public void testLockAndUnlockPointer() {
when(mPointerLockView.hasFocus()).thenReturn(true);
assertTrue(mWindowAndroid.requestPointerLock(mPointerLockView));
mWindowAndroid.releasePointerLock(mPointerLockView);
}
@Test
public void testLockAndUnlockPointerWrongView() {
when(mPointerLockView.hasFocus()).thenReturn(true);
assertTrue(mWindowAndroid.requestPointerLock(mPointerLockView));
Assert.assertThrows(AssertionError.class, () -> mWindowAndroid.releasePointerLock(mView));
}
@Test
public void testLockAndUnlockAndRelockPointer() {
when(mPointerLockView.hasFocus()).thenReturn(true);
assertTrue(mWindowAndroid.requestPointerLock(mPointerLockView));
mWindowAndroid.releasePointerLock(mPointerLockView);
assertTrue(mWindowAndroid.requestPointerLock(mPointerLockView));
}
@Test
public void testLockPointerTwiceInARow() {
when(mPointerLockView.hasFocus()).thenReturn(true);
assertTrue(mWindowAndroid.requestPointerLock(mPointerLockView));
Assert.assertThrows(
AssertionError.class, () -> mWindowAndroid.requestPointerLock(mPointerLockView));
}
}

@ -738,4 +738,10 @@ const ViewAndroid* ViewAndroid::GetTopMostChildForTesting() const {
return children_.back();
}
void ViewAndroid::OnPointerLockRelease() {
if (event_handler_) {
event_handler_->OnPointerLockRelease();
}
}
} // namespace ui

@ -245,6 +245,8 @@ class UI_ANDROID_EXPORT ViewAndroid {
protected:
void RemoveAllChildren(bool attached_to_window);
void OnPointerLockRelease();
raw_ptr<ViewAndroid> parent_;
private:
@ -257,6 +259,7 @@ class UI_ANDROID_EXPORT ViewAndroid {
FRIEND_TEST_ALL_PREFIXES(ViewAndroidBoundsTest, OnSizeChanged);
friend class EventForwarder;
friend class ViewAndroidBoundsTest;
friend class WindowAndroid;
bool OnDragEvent(const DragEventAndroid& event);
bool OnTouchEvent(const MotionEventAndroid& event);

@ -358,6 +358,12 @@ void WindowAndroid::ReleasePointerLock(ViewAndroid& view_android) {
view_android.GetContainerView());
}
void WindowAndroid::OnWindowPointerLockRelease(JNIEnv* env) {
DCHECK(pointer_locking_view_);
pointer_locking_view_->OnPointerLockRelease();
pointer_locking_view_ = nullptr;
}
void WindowAndroid::SetTestHooks(TestHooks* hooks) {
test_hooks_ = hooks;
if (!test_hooks_)

@ -109,6 +109,8 @@ class UI_ANDROID_EXPORT WindowAndroid : public ViewAndroid {
const base::android::JavaParamRef<jobject>& obj);
void SendUnfoldLatencyBeginTimestamp(JNIEnv* env, jlong begin_time);
void OnWindowPointerLockRelease(JNIEnv* env);
void ShowToast(const std::string text);
// Return whether the specified Android permission is granted.

@ -60,4 +60,6 @@ void EventHandlerAndroid::OnControlsResizeViewChanged() {}
void EventHandlerAndroid::NotifyVirtualKeyboardOverlayRect(
const gfx::Rect& keyboard_rect) {}
void EventHandlerAndroid::OnPointerLockRelease() {}
} // namespace ui

@ -39,6 +39,9 @@ class EVENTS_EXPORT EventHandlerAndroid {
std::optional<base::TimeDelta> deadline_override);
virtual void OnBrowserControlsHeightChanged();
virtual void OnControlsResizeViewChanged();
// OnPointerLockRelease is only called on the view requesting pointer lock,
// not the entire view tree
virtual void OnPointerLockRelease();
virtual bool OnGenericMotionEvent(const MotionEventAndroid& event);
virtual bool OnKeyUp(const KeyEventAndroid& event);