[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:

committed by
Chromium LUCI CQ

parent
335f039762
commit
0c78621a81
content/browser/renderer_host
ui
android
BUILD.gnview_android.ccview_android.hwindow_android.ccwindow_android.h
java
src
org
chromium
ui
junit
src
org
chromium
ui
events
@ -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);
|
||||
|
Reference in New Issue
Block a user