設置焦點時需要
先設置焦點APP
mFo-cusedApp是一個AppWindowToken,在WMS中用來表示當前處于Resume狀態的Activity。它是由AMS在開始啟動一個Activity時調用WMS的setFocusedApp()函數設置的。
考慮以下應用場景,當用戶從Launcher中啟動一個Activity之后,在新Activity的窗口顯示之前便立刻按下了BACK鍵。很明顯,用戶的意圖是關閉剛剛啟動的Activity,而不是退出Launcher。然而,由于BACK鍵按得過快,新Activity尚未創建窗口,因此按照之前討論的焦點窗口的查找條件,Launcher的窗口將會作為焦點窗口而接收BACK鍵,從而使得Launcher被退出。這與用戶的意圖是相悖的。為了解決這個問題,WMS要求焦點窗口必須屬于mFocusedApp,或者位于mFocu-sedApp的窗口之上。再看添加這個限制之后,上面的應用場景會變成什么樣子。當啟動新Activity時,盡管其窗口尚未創建,但這個窗口的AppWindowToken已經被添加到mAppTokens列表的頂部,并通過setFocusedApp()設置為mFocusedApp。此時再更新焦點窗口時發現第一個符合焦點條件的窗口屬于Launcher,但Launcher的AppWindowToken位于mFocusedApp也就是新Activity之下,因此它不能作為焦點窗口。這樣,在新Activity創建窗口之前的一個短暫時間段內,系統處于無焦點窗口的情況。此時到來的BACK鍵在InputDispatcher中會因找不到目標窗口而觸發handleTargetsNotReadyLocked(),從而進入等待重試狀態。隨后新Activity創建了自己的窗口并添加到WMS里,這時WMS可以成功地將這個窗口作為焦點,并通過IMS.setInputWindows()將其更新到InputDispatcher。這個更新動作會喚醒派發線程立刻對BACK鍵進行重試,這次重試便找到了新Activity的窗口作為目標,并將事件發送過去,從而正確地體現用戶的意圖。
void setLastResumedActivityUncheckLocked(ActivityRecord r, String reason) {boolean focusedAppChanged = false;if (!getTransitionController().isTransientCollect(r)) {focusedAppChanged = r.mDisplayContent.setFocusedApp(r);if (focusedAppChanged) {mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,true /*updateInputWindows*/);}}
WindowManagerService.javaboolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "wmUpdateFocus");boolean changed = mRoot.updateFocusedWindowLocked(mode, updateInputWindows);Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);return changed;}
RootWindowContainer.java
boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {mTopFocusedAppByProcess.clear();boolean changed = false;int topFocusedDisplayId = INVALID_DISPLAY;// Go through the children in z-order starting at the top-mostfor (int i = mChildren.size() - 1; i >= 0; --i) {final DisplayContent dc = mChildren.get(i);changed |= dc.updateFocusedWindowLocked(mode, updateInputWindows, topFocusedDisplayId);final WindowState newFocus = dc.mCurrentFocus;if (newFocus != null) {final int pidOfNewFocus = newFocus.mSession.mPid;if (mTopFocusedAppByProcess.get(pidOfNewFocus) == null) {mTopFocusedAppByProcess.put(pidOfNewFocus, newFocus.mActivityRecord);}if (topFocusedDisplayId == INVALID_DISPLAY) {topFocusedDisplayId = dc.getDisplayId();}} else if (topFocusedDisplayId == INVALID_DISPLAY && dc.mFocusedApp != null) {// The top-most display that has a focused app should still be the top focused// display even when the app window is not ready yet (process not attached or// window not added yet).topFocusedDisplayId = dc.getDisplayId();}}if (topFocusedDisplayId == INVALID_DISPLAY) {topFocusedDisplayId = DEFAULT_DISPLAY;}if (mTopFocusedDisplayId != topFocusedDisplayId) {mTopFocusedDisplayId = topFocusedDisplayId;mWmService.mInputManager.setFocusedDisplay(topFocusedDisplayId);mWmService.mPolicy.setTopFocusedDisplay(topFocusedDisplayId);mWmService.mAccessibilityController.setFocusedDisplay(topFocusedDisplayId);ProtoLog.d(WM_DEBUG_FOCUS_LIGHT, "New topFocusedDisplayId=%d", topFocusedDisplayId);}return changed;}
DisplayContent.javaboolean updateFocusedWindowLocked(int mode, boolean updateInputWindows,int topFocusedDisplayId) {// Don't re-assign focus automatically away from a should-keep-focus app window.// `findFocusedWindow` will always grab the transient-launch app since it is "on top" which// would create a mismatch, so just early-out here.if (mCurrentFocus != null && mTransitionController.shouldKeepFocus(mCurrentFocus)// This is only keeping focus, so don't early-out if the focused-app has been// explicitly changed (eg. via setFocusedTask).&& mFocusedApp != null && mCurrentFocus.isDescendantOf(mFocusedApp)&& mCurrentFocus.isVisible() && mCurrentFocus.isFocusable()) {ProtoLog.v(WM_DEBUG_FOCUS, "Current transition prevents automatic focus change");return false;}WindowState newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId);if (mCurrentFocus == newFocus) {return false;}boolean imWindowChanged = false;final WindowState imWindow = mInputMethodWindow;if (imWindow != null) {final WindowState prevTarget = mImeLayeringTarget;final WindowState newTarget = computeImeTarget(true /* updateImeTarget*/);imWindowChanged = prevTarget != newTarget;if (mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS&& mode != UPDATE_FOCUS_WILL_PLACE_SURFACES) {assignWindowLayers(false /* setLayoutNeeded */);}if (imWindowChanged) {mWmService.mWindowsChanged = true;setLayoutNeeded();newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId);}}ProtoLog.d(WM_DEBUG_FOCUS_LIGHT, "Changing focus from %s to %s displayId=%d Callers=%s",mCurrentFocus, newFocus, getDisplayId(), Debug.getCallers(4));final WindowState oldFocus = mCurrentFocus;mCurrentFocus = newFocus;if (newFocus != null) {mWinAddedSinceNullFocus.clear();mWinRemovedSinceNullFocus.clear();if (newFocus.canReceiveKeys()) {// Displaying a window implicitly causes dispatching to be unpaused.// This is to protect against bugs if someone pauses dispatching but// forgets to resume.newFocus.mToken.paused = false;}}getDisplayPolicy().focusChangedLw(oldFocus, newFocus);mAtmService.mBackNavigationController.onFocusChanged(newFocus);if (imWindowChanged && oldFocus != mInputMethodWindow) {// Focus of the input method window changed. Perform layout if needed.if (mode == UPDATE_FOCUS_PLACING_SURFACES) {performLayout(true /*initial*/, updateInputWindows);} else if (mode == UPDATE_FOCUS_WILL_PLACE_SURFACES) {// Client will do the layout, but we need to assign layers// for handleNewWindowLocked() below.assignWindowLayers(false /* setLayoutNeeded */);}}if (mode != UPDATE_FOCUS_WILL_ASSIGN_LAYERS) {// If we defer assigning layers, then the caller is responsible for doing this part.getInputMonitor().setInputFocusLw(newFocus, updateInputWindows);}adjustForImeIfNeeded();updateKeepClearAreas();// We may need to schedule some toast windows to be removed. The toasts for an app that// does not have input focus are removed within a timeout to prevent apps to redress// other apps' UI.scheduleToastWindowsTimeoutIfNeededLocked(oldFocus, newFocus);if (mode == UPDATE_FOCUS_PLACING_SURFACES) {pendingLayoutChanges |= FINISH_LAYOUT_REDO_ANIM;}// Notify the accessibility manager for the change so it has the windows before the newly// focused one starts firing events.// TODO(b/151179149) investigate what info accessibility service needs before input can// dispatch focus to clients.if (mWmService.mAccessibilityController.hasCallbacks()) {mWmService.mH.sendMessage(PooledLambda.obtainMessage(this::updateAccessibilityOnWindowFocusChanged,mWmService.mAccessibilityController));}return true;}
updateFocusedWindowLocked
首先尋找焦點窗口findFocusedWindowIfNeeded
然后將焦點窗口設置到input?:setInputFocusLw
尋找焦點窗口
DisplayContent.javaWindowState findFocusedWindowIfNeeded(int topFocusedDisplayId) {return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY)? findFocusedWindow() : null;}/*** Find the focused window of this DisplayContent. The search takes the state of the display* content into account* @return The focused window, null if none was found.*/WindowState findFocusedWindow() {mTmpWindow = null;// mFindFocusedWindow will populate mTmpWindow with the new focused window when found.forAllWindows(mFindFocusedWindow, true /* traverseTopToBottom */);if (mTmpWindow == null) {ProtoLog.v(WM_DEBUG_FOCUS_LIGHT, "findFocusedWindow: No focusable windows, display=%d",getDisplayId());return null;}return mTmpWindow;}
private final ToBooleanFunction<WindowState> mFindFocusedWindow = w -> {final ActivityRecord focusedApp = mFocusedApp;ProtoLog.v(WM_DEBUG_FOCUS, "Looking for focus: %s, flags=%d, canReceive=%b, reason=%s",w, w.mAttrs.flags, w.canReceiveKeys(),w.canReceiveKeysReason(false /* fromUserTouch */));if (!w.canReceiveKeys()) {return false;}// When switching the app task, we keep the IME window visibility for better// transitioning experiences.// However, in case IME created a child window or the IME selection dialog without// dismissing during the task switching to keep the window focus because IME window has// higher window hierarchy, we don't give it focus if the next IME layering target// doesn't request IME visible.if (w.mIsImWindow && w.isChildWindow() && (mImeLayeringTarget == null|| !mImeLayeringTarget.isRequestedVisible(ime()))) {return false;}if (w.mAttrs.type == TYPE_INPUT_METHOD_DIALOG && mImeLayeringTarget != null&& !mImeLayeringTarget.isRequestedVisible(ime())&& !mImeLayeringTarget.isVisibleRequested()) {return false;}final ActivityRecord activity = w.mActivityRecord;if (focusedApp == null) {ProtoLog.v(WM_DEBUG_FOCUS_LIGHT,"findFocusedWindow: focusedApp=null using new focus @ %s", w);mTmpWindow = w;return true;}if (!focusedApp.windowsAreFocusable()) {// Current focused app windows aren't focusable...ProtoLog.v(WM_DEBUG_FOCUS_LIGHT, "findFocusedWindow: focusedApp windows not"+ " focusable using new focus @ %s", w);mTmpWindow = w;return true;}// Descend through all of the app tokens and find the first that either matches// win.mActivityRecord (return win) or mFocusedApp (return null).if (activity != null && w.mAttrs.type != TYPE_APPLICATION_STARTING) {if (focusedApp.compareTo(activity) > 0) {// App root task below focused app root task. No focus for you!!!ProtoLog.v(WM_DEBUG_FOCUS_LIGHT,"findFocusedWindow: Reached focused app=%s", focusedApp);mTmpWindow = null;return true;}// If the candidate activity is currently being embedded in the focused task, the// activity cannot be focused unless it is on the same TaskFragment as the focusedApp's.TaskFragment parent = activity.getTaskFragment();if (parent != null && parent.isEmbedded()) {if (activity.getTask() == focusedApp.getTask()&& activity.getTaskFragment() != focusedApp.getTaskFragment()) {return false;}}}ProtoLog.v(WM_DEBUG_FOCUS_LIGHT, "findFocusedWindow: Found new focus @ %s", w);mTmpWindow = w;return true;};
該方法中,將依次根據如下條件獲得焦點窗口:
- 如果WindowState不能接收Input事件,則不能作為焦點窗口;
- 如果沒有前臺Activity,則當前WindowState作為焦點窗口返回;
- 如果前臺Activity是不可獲焦狀態,則當前WindowState作為焦點窗口返回;
- 如果當前WindowState由ActivityRecord管理,且該WindowState不是Staring Window類型,那么當前臺Activity在當前WindowState所屬Activity之上時,不存在焦點窗口;
- 處于focusedApp之下的窗口不能成為焦點窗口
- 如果以上條件都不滿足,則當前WindowState作為焦點窗口返回;
- 由此可以得出影響窗口焦點的動作有以下幾個:
- □窗口的增刪操作。
- □窗口次序的調整。
- □Activity的啟動與退出。
- □DisplayContent的增刪操作。
- 因此當這些動作發生時,WMS都會觸發對updateFocusedWindowLocked()的調用,并更新窗口的布局信息到輸入系統,以實現按鍵事件正確派發。
-
public boolean canReceiveKeys(boolean fromUserTouch) {if (mActivityRecord != null && mTransitionController.shouldKeepFocus(mActivityRecord)) {// During transient launch, the transient-hide windows are not visibleRequested// or on-top but are kept focusable and thus can receive keys.return true;}final boolean canReceiveKeys = isVisibleRequestedOrAdding()&& (mViewVisibility == View.VISIBLE) && !mRemoveOnExit&& ((mAttrs.flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0)&& (mActivityRecord == null || mActivityRecord.windowsAreFocusable(fromUserTouch))// can it receive touches&& (mActivityRecord == null || mActivityRecord.getTask() == null|| !mActivityRecord.getTask().getRootTask().shouldIgnoreInput());if (!canReceiveKeys) {return false;}// Do not allow untrusted virtual display to receive keys unless user intentionally// touches the display.return fromUserTouch || getDisplayContent().isOnTop()|| getDisplayContent().isTrusted();}
如果一個WindowState可以接受Input事件,需要同時滿足多個條件:
-
- isVisibleRequestedOrAdding方法為true,表示該WindowState可見或處于添加過程中:
- mViewVisibility屬性為View.VISIBLE,表示客戶端View可見;
- mRemoveOnExit為false,表示WindowState的退出動畫不存在;
- mAttrs.flags中不存在FLAG_NOT_FOCUSABLE標記,該標記如果設置,表示該窗口為不可獲焦窗口;
- mActivityRecord為null或者mActivityRecord可獲焦;
- cantReceiveTouchInput()方法為false,表示可以接受Touch事件。
boolean isVisibleRequestedOrAdding() {final ActivityRecord atoken = mActivityRecord;return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE))&& isVisibleByPolicy() && !isParentWindowHidden()&& (atoken == null || atoken.isVisibleRequested())&& !mAnimatingExit && !mDestroying;}
然后將焦點窗口設置到input?:setInputFocusLw
查看Android14 InputManager-InputWindow的更新過程