diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 9bf43a390d70..1d81be17f580 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -370,4 +370,14 @@ interface IWindowSession { boolean transferEmbeddedTouchFocusToHost(IWindow embeddedWindow); boolean transferHostTouchGestureToEmbedded(IWindow hostWindow, IBinder transferTouchToken); + + /** + * Moves the focus to the adjacent window if there is one in the given direction. This can only + * move the focus to the window in the same leaf task. + * + * @param fromWindow The calling window that the focus is moved from. + * @param direction The {@link android.view.View.FocusDirection} that the new focus should go. + * @return {@code true} if the focus changes. Otherwise, {@code false}. + */ + boolean moveFocusToAdjacentWindow(IWindow fromWindow, int direction); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 350876c828b7..c66f3c8032fd 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -16,6 +16,7 @@ package android.view; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.pm.ActivityInfo.OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS; import static android.graphics.HardwareRenderer.SYNC_CONTEXT_IS_STOPPED; import static android.graphics.HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND; @@ -7236,7 +7237,7 @@ public final class ViewRootImpl implements ViewParent, } private boolean performFocusNavigation(KeyEvent event) { - int direction = 0; + @FocusDirection int direction = 0; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_LEFT: if (event.hasNoModifiers()) { @@ -7288,6 +7289,8 @@ public final class ViewRootImpl implements ViewParent, isFastScrolling)); return true; } + } else if (moveFocusToAdjacentWindow(direction)) { + return true; } // Give the focused view a last chance to handle the dpad key. @@ -7297,12 +7300,26 @@ public final class ViewRootImpl implements ViewParent, } else { if (mView.restoreDefaultFocus()) { return true; + } else if (moveFocusToAdjacentWindow(direction)) { + return true; } } } return false; } + private boolean moveFocusToAdjacentWindow(@FocusDirection int direction) { + if (getConfiguration().windowConfiguration.getWindowingMode() + != WINDOWING_MODE_MULTI_WINDOW) { + return false; + } + try { + return mWindowSession.moveFocusToAdjacentWindow(mWindow, direction); + } catch (RemoteException e) { + return false; + } + } + private boolean performKeyboardGroupNavigation(int direction) { final View focused = mView.findFocus(); if (focused == null && mView.restoreDefaultFocus()) { diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index d6ac56239aed..b95e4595d6b9 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -30,6 +30,7 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.util.Log; import android.util.MergedConfiguration; +import android.view.View.FocusDirection; import android.view.WindowInsets.Type.InsetsType; import android.window.ClientWindowFrames; import android.window.OnBackInvokedCallbackInfo; @@ -665,6 +666,13 @@ public class WindowlessWindowManager implements IWindowSession { return false; } + @Override + public boolean moveFocusToAdjacentWindow(IWindow fromWindow, @FocusDirection int direction) { + Log.e(TAG, "Received request to moveFocusToAdjacentWindow on" + + " WindowlessWindowManager. We shouldn't get here!"); + return false; + } + void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) { IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder(); IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder(); diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index ed54ea8229fe..f10a733040ed 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -75,6 +75,7 @@ import android.view.InsetsState; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.View; +import android.view.View.FocusDirection; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; @@ -1000,6 +1001,17 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } return didTransfer; } + + @Override + public boolean moveFocusToAdjacentWindow(IWindow fromWindow, @FocusDirection int direction) { + final long identity = Binder.clearCallingIdentity(); + try { + return mService.moveFocusToAdjacentWindow(this, fromWindow, direction); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public void generateDisplayHash(IWindow window, Rect boundsInWindow, String hashAlgorithm, RemoteCallback callback) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index c63cc4373472..95448352736f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -287,6 +287,7 @@ import android.view.SurfaceControlViewHost; import android.view.SurfaceSession; import android.view.TaskTransitionSpec; import android.view.View; +import android.view.View.FocusDirection; import android.view.ViewDebug; import android.view.WindowContentFrameStats; import android.view.WindowInsets; @@ -9104,6 +9105,66 @@ public class WindowManagerService extends IWindowManager.Stub win.mClient); } + boolean moveFocusToAdjacentWindow(Session session, IWindow fromWindow, + @FocusDirection int direction) { + synchronized (mGlobalLock) { + final WindowState fromWin = windowForClientLocked(session, fromWindow, false); + if (fromWin == null || !fromWin.isFocused()) { + return false; + } + final TaskFragment fromFragment = fromWin.getTaskFragment(); + if (fromFragment == null) { + return false; + } + final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); + if (adjacentFragment == null || adjacentFragment.asTask() != null) { + // Don't move the focus to another task. + return false; + } + final Rect fromBounds = fromFragment.getBounds(); + final Rect adjacentBounds = adjacentFragment.getBounds(); + switch (direction) { + case View.FOCUS_LEFT: + if (adjacentBounds.left >= fromBounds.left) { + return false; + } + break; + case View.FOCUS_UP: + if (adjacentBounds.top >= fromBounds.top) { + return false; + } + break; + case View.FOCUS_RIGHT: + if (adjacentBounds.right <= fromBounds.right) { + return false; + } + break; + case View.FOCUS_DOWN: + if (adjacentBounds.bottom <= fromBounds.bottom) { + return false; + } + break; + case View.FOCUS_BACKWARD: + case View.FOCUS_FORWARD: + // These are not absolute directions. Skip checking the bounds. + break; + default: + return false; + } + final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity( + true /* focusableOnly */); + if (topRunningActivity == null) { + return false; + } + moveDisplayToTopInternal(topRunningActivity.getDisplayId()); + handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); + if (fromWin.isFocused()) { + return false; + } + } + return true; + } + /** Return whether layer tracing is enabled */ public boolean isLayerTracing() { if (!checkCallingPermission( diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index e9fe4bb91329..22ddf8420121 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -53,6 +53,7 @@ import android.graphics.Rect; import android.os.Binder; import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl; +import android.view.View; import android.window.ITaskFragmentOrganizer; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; @@ -695,4 +696,75 @@ public class TaskFragmentTest extends WindowTestsBase { mTaskFragment.getDimBounds(dimBounds); assertEquals(taskFragmentBounds, dimBounds); } + + @Test + public void testMoveFocusToAdjacentWindow() { + // Setup two activities in ActivityEmbedding split. + final Task task = createTask(mDisplayContent); + final TaskFragment taskFragmentLeft = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .createActivityCount(2) + .setOrganizer(mOrganizer) + .setFragmentToken(new Binder()) + .build(); + final TaskFragment taskFragmentRight = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .createActivityCount(1) + .setOrganizer(mOrganizer) + .setFragmentToken(new Binder()) + .build(); + taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight); + taskFragmentLeft.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + taskFragmentRight.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + task.setBounds(0, 0, 1200, 1000); + taskFragmentLeft.setBounds(0, 0, 600, 1000); + taskFragmentRight.setBounds(600, 0, 1200, 1000); + final ActivityRecord appLeftTop = taskFragmentLeft.getTopMostActivity(); + final ActivityRecord appLeftBottom = taskFragmentLeft.getBottomMostActivity(); + final ActivityRecord appRightTop = taskFragmentRight.getTopMostActivity(); + appLeftTop.setVisibleRequested(true); + appRightTop.setVisibleRequested(true); + final WindowState winLeftTop = createAppWindow(appLeftTop, "winLeftTop"); + final WindowState winLeftBottom = createAppWindow(appLeftBottom, "winLeftBottom"); + final WindowState winRightTop = createAppWindow(appRightTop, "winRightTop"); + winLeftTop.setHasSurface(true); + winRightTop.setHasSurface(true); + + taskFragmentLeft.setResumedActivity(appLeftTop, "test"); + taskFragmentRight.setResumedActivity(appRightTop, "test"); + appLeftTop.setState(RESUMED, "test"); + appRightTop.setState(RESUMED, "test"); + mDisplayContent.mFocusedApp = appRightTop; + + // Make the appLeftTop be the focused activity and ensure the focused app is updated. + appLeftTop.moveFocusableActivityToTop("test"); + assertEquals(winLeftTop, mDisplayContent.mCurrentFocus); + + // Send request from a non-focused window with valid direction. + assertFalse(mWm.moveFocusToAdjacentWindow(null, winLeftBottom.mClient, View.FOCUS_RIGHT)); + // The focus should remain the same. + assertEquals(winLeftTop, mDisplayContent.mCurrentFocus); + + // Send request from the focused window with valid direction. + assertTrue(mWm.moveFocusToAdjacentWindow(null, winLeftTop.mClient, View.FOCUS_RIGHT)); + // The focus should change. + assertEquals(winRightTop, mDisplayContent.mCurrentFocus); + + // Send request from the focused window with invalid direction. + assertFalse(mWm.moveFocusToAdjacentWindow(null, winRightTop.mClient, View.FOCUS_UP)); + // The focus should remain the same. + assertEquals(winRightTop, mDisplayContent.mCurrentFocus); + + // Send request from the focused window with valid direction. + assertTrue(mWm.moveFocusToAdjacentWindow(null, winRightTop.mClient, View.FOCUS_BACKWARD)); + // The focus should change. + assertEquals(winLeftTop, mDisplayContent.mCurrentFocus); + } + + private WindowState createAppWindow(ActivityRecord app, String name) { + final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, app, name, + 0 /* ownerId */, false /* ownerCanAddInternalSystemWindow */, new TestIWindow()); + mWm.mWindowMap.put(win.mClient.asBinder(), win); + return win; + } }