设置焦点时需要
先设置焦点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.java
boolean 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-most
for (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.java
boolean 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.java
WindowState 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;
}