blob: 841527261e29ff525b774ddf1f4537d3aa8e2058 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility;
import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.view.IWindow;
import android.view.WindowInfo;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
import com.android.server.accessibility.AccessibilitySecurityPolicy.AccessibilityUserManager;
import com.android.server.wm.WindowManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* This class implements {@link WindowManagerInternal.WindowsForAccessibilityCallback} to receive
* {@link WindowInfo}s from window manager when there's an accessibility change in window. It also
* provides APIs for accessibility manager to manage {@link AccessibilityWindowInfo}s and
* {@link WindowInfo}s.
*/
public class AccessibilityWindowManager
implements WindowManagerInternal.WindowsForAccessibilityCallback {
private static final String LOG_TAG = "AccessibilityWindowManager";
private static final boolean DEBUG = false;
private static int sNextWindowId;
private final Object mLock;
private final Handler mHandler;
private final WindowManagerInternal mWindowManagerInternal;
private final AccessibilityEventSender mAccessibilityEventSender;
private final AccessibilitySecurityPolicy mSecurityPolicy;
private final AccessibilityUserManager mAccessibilityUserManager;
private final SparseArray<AccessibilityWindowInfo> mA11yWindowInfoById = new SparseArray<>();
private final SparseArray<WindowInfo> mWindowInfoById = new SparseArray<>();
// Connections and window tokens for cross-user windows
private final SparseArray<RemoteAccessibilityConnection>
mGlobalInteractionConnections = new SparseArray<>();
private final SparseArray<IBinder> mGlobalWindowTokens = new SparseArray<>();
// Connections and window tokens for per-user windows, indexed as one sparse array per user
private final SparseArray<SparseArray<RemoteAccessibilityConnection>>
mInteractionConnections = new SparseArray<>();
private final SparseArray<SparseArray<IBinder>> mWindowTokens = new SparseArray<>();
private final List<WindowInfo> mCachedWindowInfos = new ArrayList<>();
private List<AccessibilityWindowInfo> mWindows;
private RemoteAccessibilityConnection mPictureInPictureActionReplacingConnection;
private int mActiveWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
private int mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
private int mAccessibilityFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
private long mAccessibilityFocusNodeId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
private boolean mTouchInteractionInProgress;
private boolean mHasWatchOutsideTouchWindow;
private boolean mTrackingWindows = false;
/**
* Interface to send {@link AccessibilityEvent}.
*/
public interface AccessibilityEventSender {
/**
* Send {@link AccessibilityEvent} for current user.
*/
void sendAccessibilityEventForCurrentUserLocked(AccessibilityEvent event);
}
/**
* Wrapper of accessibility interaction connection for window.
*/
final class RemoteAccessibilityConnection implements IBinder.DeathRecipient {
private final int mUid;
private final String mPackageName;
private final int mWindowId;
private final int mUserId;
private final IAccessibilityInteractionConnection mConnection;
RemoteAccessibilityConnection(int windowId,
IAccessibilityInteractionConnection connection,
String packageName, int uid, int userId) {
mWindowId = windowId;
mPackageName = packageName;
mUid = uid;
mUserId = userId;
mConnection = connection;
}
int getUid() {
return mUid;
}
String getPackageName() {
return mPackageName;
}
IAccessibilityInteractionConnection getRemote() {
return mConnection;
}
void linkToDeath() throws RemoteException {
mConnection.asBinder().linkToDeath(this, 0);
}
void unlinkToDeath() {
mConnection.asBinder().unlinkToDeath(this, 0);
}
@Override
public void binderDied() {
unlinkToDeath();
synchronized (mLock) {
removeAccessibilityInteractionConnectionLocked(mWindowId, mUserId);
}
}
}
/**
* Constructor for AccessibilityManagerService.
*/
public AccessibilityWindowManager(@NonNull Object lock, @NonNull Handler handler,
@NonNull WindowManagerInternal windowManagerInternal,
@NonNull AccessibilityEventSender accessibilityEventSender,
@NonNull AccessibilitySecurityPolicy securityPolicy,
@NonNull AccessibilityUserManager accessibilityUserManager) {
mLock = lock;
mHandler = handler;
mWindowManagerInternal = windowManagerInternal;
mAccessibilityEventSender = accessibilityEventSender;
mSecurityPolicy = securityPolicy;
mAccessibilityUserManager = accessibilityUserManager;
}
/**
* Callbacks from window manager when there's an accessibility change in windows.
*
* @param forceSend Send the windows for accessibility even if they haven't changed.
* @param windows The windows of current display for accessibility.
*/
@Override
public void onWindowsForAccessibilityChanged(boolean forceSend,
@NonNull List<WindowInfo> windows) {
synchronized (mLock) {
if (DEBUG) {
Slog.i(LOG_TAG, "Windows changed: " + windows);
}
if (shouldUpdateWindowsLocked(forceSend, windows)) {
cacheWindows(windows);
// Let the policy update the focused and active windows.
updateWindowsLocked(mAccessibilityUserManager.getCurrentUserIdLocked(), windows);
// Someone may be waiting for the windows - advertise it.
mLock.notifyAll();
}
}
}
private boolean shouldUpdateWindowsLocked(boolean forceSend,
@NonNull List<WindowInfo> windows) {
if (forceSend) {
return true;
}
final int windowCount = windows.size();
// We computed the windows and if they changed notify the client.
if (mCachedWindowInfos.size() != windowCount) {
// Different size means something changed.
return true;
} else if (!mCachedWindowInfos.isEmpty() || !windows.isEmpty()) {
// Since we always traverse windows from high to low layer
// the old and new windows at the same index should be the
// same, otherwise something changed.
for (int i = 0; i < windowCount; i++) {
WindowInfo oldWindow = mCachedWindowInfos.get(i);
WindowInfo newWindow = windows.get(i);
// We do not care for layer changes given the window
// order does not change. This brings no new information
// to the clients.
if (windowChangedNoLayer(oldWindow, newWindow)) {
return true;
}
}
}
return false;
}
private void cacheWindows(List<WindowInfo> windows) {
final int oldWindowCount = mCachedWindowInfos.size();
for (int i = oldWindowCount - 1; i >= 0; i--) {
mCachedWindowInfos.remove(i).recycle();
}
final int newWindowCount = windows.size();
for (int i = 0; i < newWindowCount; i++) {
WindowInfo newWindow = windows.get(i);
mCachedWindowInfos.add(WindowInfo.obtain(newWindow));
}
}
private boolean windowChangedNoLayer(WindowInfo oldWindow, WindowInfo newWindow) {
if (oldWindow == newWindow) {
return false;
}
if (oldWindow == null) {
return true;
}
if (newWindow == null) {
return true;
}
if (oldWindow.type != newWindow.type) {
return true;
}
if (oldWindow.focused != newWindow.focused) {
return true;
}
if (oldWindow.token == null) {
if (newWindow.token != null) {
return true;
}
} else if (!oldWindow.token.equals(newWindow.token)) {
return true;
}
if (oldWindow.parentToken == null) {
if (newWindow.parentToken != null) {
return true;
}
} else if (!oldWindow.parentToken.equals(newWindow.parentToken)) {
return true;
}
if (oldWindow.activityToken == null) {
if (newWindow.activityToken != null) {
return true;
}
} else if (!oldWindow.activityToken.equals(newWindow.activityToken)) {
return true;
}
if (!oldWindow.boundsInScreen.equals(newWindow.boundsInScreen)) {
return true;
}
if (oldWindow.childTokens != null && newWindow.childTokens != null
&& !oldWindow.childTokens.equals(newWindow.childTokens)) {
return true;
}
if (!TextUtils.equals(oldWindow.title, newWindow.title)) {
return true;
}
if (oldWindow.accessibilityIdOfAnchor != newWindow.accessibilityIdOfAnchor) {
return true;
}
if (oldWindow.inPictureInPicture != newWindow.inPictureInPicture) {
return true;
}
if (oldWindow.hasFlagWatchOutsideTouch != newWindow.hasFlagWatchOutsideTouch) {
return true;
}
if (oldWindow.displayId != newWindow.displayId) {
return true;
}
return false;
}
/**
* Start tracking windows changes from window manager.
*/
public void startTrackingWindows() {
synchronized (mLock) {
if (!mTrackingWindows) {
// Turn on the flag before setup the callback.
// In some cases, onWindowsForAccessibilityChanged will be called immediately in
// setWindowsForAccessibilityCallback. We'll lost windows if flag is false.
mTrackingWindows = true;
mWindowManagerInternal.setWindowsForAccessibilityCallback(this);
}
}
}
/**
* stop tracking windows changes from window manager, and clear all windows info.
*/
public void stopTrackingWindows() {
synchronized (mLock) {
if (mTrackingWindows) {
mWindowManagerInternal.setWindowsForAccessibilityCallback(null);
mTrackingWindows = false;
clearWindowsLocked();
}
}
}
/**
* Returns true if windows changes tracking.
*
* @return true if windows changes tracking
*/
public boolean isTrackingWindowsLocked() {
return mTrackingWindows;
}
/**
* Clears all {@link AccessibilityWindowInfo}s and {@link WindowInfo}s.
*/
private void clearWindowsLocked() {
final List<WindowInfo> windows = Collections.emptyList();
final int activeWindowId = mActiveWindowId;
// userId is useless in updateWindowsLocked, when we update a empty window list. Just pass
// current userId here.
updateWindowsLocked(mAccessibilityUserManager.getCurrentUserIdLocked(), windows);
// Do not reset mActiveWindowId here. mActiveWindowId will be clear after accessibility
// interaction connection removed.
mActiveWindowId = activeWindowId;
mWindows = null;
}
/**
* Update windows info according to specified userId and windows.
*
* @param userId The userId to update
* @param windows The windows to update
*/
private void updateWindowsLocked(int userId, @NonNull List<WindowInfo> windows) {
if (mWindows == null) {
mWindows = new ArrayList<>();
}
final List<AccessibilityWindowInfo> oldWindowList = new ArrayList<>(mWindows);
final SparseArray<AccessibilityWindowInfo> oldWindowsById = mA11yWindowInfoById.clone();
mWindows.clear();
mA11yWindowInfoById.clear();
for (int i = 0; i < mWindowInfoById.size(); i++) {
mWindowInfoById.valueAt(i).recycle();
}
mWindowInfoById.clear();
mHasWatchOutsideTouchWindow = false;
mFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
if (!mTouchInteractionInProgress) {
mActiveWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
}
// If the active window goes away while the user is touch exploring we
// reset the active window id and wait for the next hover event from
// under the user's finger to determine which one is the new one. It
// is possible that the finger is not moving and the input system
// filters out such events.
boolean activeWindowGone = true;
final int windowCount = windows.size();
// We'll clear accessibility focus if the window with focus is no longer visible to
// accessibility services
boolean shouldClearAccessibilityFocus =
mAccessibilityFocusedWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
if (windowCount > 0) {
for (int i = 0; i < windowCount; i++) {
final WindowInfo windowInfo = windows.get(i);
final AccessibilityWindowInfo window;
if (isTrackingWindowsLocked()) {
window = populateReportedWindowLocked(userId, windowInfo);
} else {
window = null;
}
if (window != null) {
// Flip layers in list to be consistent with AccessibilityService#getWindows
window.setLayer(windowCount - 1 - window.getLayer());
final int windowId = window.getId();
if (window.isFocused()) {
mFocusedWindowId = windowId;
if (!mTouchInteractionInProgress) {
mActiveWindowId = windowId;
window.setActive(true);
} else if (windowId == mActiveWindowId) {
activeWindowGone = false;
}
}
if (!mHasWatchOutsideTouchWindow && windowInfo.hasFlagWatchOutsideTouch) {
mHasWatchOutsideTouchWindow = true;
}
mWindows.add(window);
mA11yWindowInfoById.put(windowId, window);
mWindowInfoById.put(windowId, WindowInfo.obtain(windowInfo));
}
}
if (mTouchInteractionInProgress && activeWindowGone) {
mActiveWindowId = mFocusedWindowId;
}
// Focused window may change the active one, so set the
// active window once we decided which it is.
final int accessibilityWindowCount = mWindows.size();
for (int i = 0; i < accessibilityWindowCount; i++) {
final AccessibilityWindowInfo window = mWindows.get(i);
if (window.getId() == mActiveWindowId) {
window.setActive(true);
}
if (window.getId() == mAccessibilityFocusedWindowId) {
window.setAccessibilityFocused(true);
shouldClearAccessibilityFocus = false;
}
}
}
sendEventsForChangedWindowsLocked(oldWindowList, oldWindowsById);
final int oldWindowCount = oldWindowList.size();
for (int i = oldWindowCount - 1; i >= 0; i--) {
oldWindowList.remove(i).recycle();
}
if (shouldClearAccessibilityFocus) {
clearAccessibilityFocusLocked(mAccessibilityFocusedWindowId);
}
}
/**
* Returns accessibility windows.
*/
@Nullable
public List<AccessibilityWindowInfo> getWindowListLocked() {
return mWindows;
}
/**
* Adds accessibility interaction connection according to given window token, package name and
* window token.
*
* @param windowToken The window token of accessibility interaction connection
* @param connection The accessibility interaction connection
* @param packageName The package name
* @param userId The userId
* @return The windowId of added connection
* @throws RemoteException
*/
public int addAccessibilityInteractionConnection(@NonNull IWindow windowToken,
@NonNull IAccessibilityInteractionConnection connection, @NonNull String packageName,
int userId) throws RemoteException {
final int windowId;
synchronized (mLock) {
// We treat calls from a profile as if made by its parent as profiles
// share the accessibility state of the parent. The call below
// performs the current profile parent resolution.
final int resolvedUserId = mSecurityPolicy
.resolveCallingUserIdEnforcingPermissionsLocked(userId);
final int resolvedUid = UserHandle.getUid(resolvedUserId, UserHandle.getCallingAppId());
// Make sure the reported package is one the caller has access to.
packageName = mSecurityPolicy.resolveValidReportedPackageLocked(
packageName, UserHandle.getCallingAppId(), resolvedUserId);
windowId = sNextWindowId++;
// If the window is from a process that runs across users such as
// the system UI or the system we add it to the global state that
// is shared across users.
if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) {
RemoteAccessibilityConnection wrapper = new RemoteAccessibilityConnection(
windowId, connection, packageName, resolvedUid, UserHandle.USER_ALL);
wrapper.linkToDeath();
mGlobalInteractionConnections.put(windowId, wrapper);
mGlobalWindowTokens.put(windowId, windowToken.asBinder());
if (DEBUG) {
Slog.i(LOG_TAG, "Added global connection for pid:" + Binder.getCallingPid()
+ " with windowId: " + windowId + " and token: "
+ windowToken.asBinder());
}
} else {
RemoteAccessibilityConnection wrapper = new RemoteAccessibilityConnection(
windowId, connection, packageName, resolvedUid, resolvedUserId);
wrapper.linkToDeath();
getInteractionConnectionsForUserLocked(userId).put(windowId, wrapper);
getWindowTokensForUserLocked(userId).put(windowId, windowToken.asBinder());
if (DEBUG) {
Slog.i(LOG_TAG, "Added user connection for pid:" + Binder.getCallingPid()
+ " with windowId: " + windowId
+ " and token: " + windowToken.asBinder());
}
}
}
mWindowManagerInternal.computeWindowsForAccessibility();
return windowId;
}
/**
* Removes accessibility interaction connection according to given window token.
*
* @param window The window token of accessibility interaction connection
*/
public void removeAccessibilityInteractionConnection(@NonNull IWindow window) {
synchronized (mLock) {
// We treat calls from a profile as if made by its parent as profiles
// share the accessibility state of the parent. The call below
// performs the current profile parent resolution.
mSecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked(
UserHandle.getCallingUserId());
IBinder token = window.asBinder();
final int removedWindowId = removeAccessibilityInteractionConnectionInternalLocked(
token, mGlobalWindowTokens, mGlobalInteractionConnections);
if (removedWindowId >= 0) {
onAccessibilityInteractionConnectionRemovedLocked(removedWindowId);
if (DEBUG) {
Slog.i(LOG_TAG, "Removed global connection for pid:" + Binder.getCallingPid()
+ " with windowId: " + removedWindowId + " and token: "
+ window.asBinder());
}
return;
}
final int userCount = mWindowTokens.size();
for (int i = 0; i < userCount; i++) {
final int userId = mWindowTokens.keyAt(i);
final int removedWindowIdForUser =
removeAccessibilityInteractionConnectionInternalLocked(token,
getWindowTokensForUserLocked(userId),
getInteractionConnectionsForUserLocked(userId));
if (removedWindowIdForUser >= 0) {
onAccessibilityInteractionConnectionRemovedLocked(removedWindowIdForUser);
if (DEBUG) {
Slog.i(LOG_TAG, "Removed user connection for pid:" + Binder.getCallingPid()
+ " with windowId: " + removedWindowIdForUser + " and userId:"
+ userId + " and token: " + window.asBinder());
}
return;
}
}
}
}
/**
* Resolve a connection wrapper for a window id
*
* @param userId The user id for any user-specific windows
* @param windowId The id of the window of interest
*
* @return a connection to the window
*/
@Nullable
public RemoteAccessibilityConnection getConnectionLocked(int userId, int windowId) {
if (DEBUG) {
Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
}
RemoteAccessibilityConnection connection = mGlobalInteractionConnections.get(windowId);
if (connection == null && isValidUserForInteractionConnectionsLocked(userId)) {
connection = getInteractionConnectionsForUserLocked(userId).get(windowId);
}
if (connection != null && connection.getRemote() != null) {
return connection;
}
if (DEBUG) {
Slog.e(LOG_TAG, "No interaction connection to window: " + windowId);
}
return null;
}
private int removeAccessibilityInteractionConnectionInternalLocked(IBinder windowToken,
SparseArray<IBinder> windowTokens, SparseArray<RemoteAccessibilityConnection>
interactionConnections) {
final int count = windowTokens.size();
for (int i = 0; i < count; i++) {
if (windowTokens.valueAt(i) == windowToken) {
final int windowId = windowTokens.keyAt(i);
windowTokens.removeAt(i);
RemoteAccessibilityConnection wrapper = interactionConnections.get(windowId);
wrapper.unlinkToDeath();
interactionConnections.remove(windowId);
return windowId;
}
}
return -1;
}
/**
* Removes accessibility interaction connection according to given windowId and userId.
*
* @param windowId The windowId of accessibility interaction connection
* @param userId The userId to remove
*/
private void removeAccessibilityInteractionConnectionLocked(int windowId, int userId) {
if (userId == UserHandle.USER_ALL) {
mGlobalWindowTokens.remove(windowId);
mGlobalInteractionConnections.remove(windowId);
} else {
if (isValidUserForWindowTokensLocked(userId)) {
getWindowTokensForUserLocked(userId).remove(windowId);
}
if (isValidUserForInteractionConnectionsLocked(userId)) {
getInteractionConnectionsForUserLocked(userId).remove(windowId);
}
}
onAccessibilityInteractionConnectionRemovedLocked(windowId);
if (DEBUG) {
Slog.i(LOG_TAG, "Removing interaction connection to windowId: " + windowId);
}
}
/**
* Invoked when accessibility interaction connection of window is removed.
*
* @param windowId Removed windowId
*/
private void onAccessibilityInteractionConnectionRemovedLocked(int windowId) {
// Active window will not update, if windows callback is unregistered.
// Update active window to invalid, when its a11y interaction connection is removed.
if (!isTrackingWindowsLocked() && windowId >= 0 && mActiveWindowId == windowId) {
mActiveWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
}
}
/**
* Gets window token according to given userId and windowId.
*
* @param userId The userId
* @param windowId The windowId
* @return The window token
*/
@Nullable
public IBinder getWindowTokenForUserAndWindowIdLocked(int userId, int windowId) {
IBinder windowToken = mGlobalWindowTokens.get(windowId);
if (windowToken == null && isValidUserForWindowTokensLocked(userId)) {
windowToken = getWindowTokensForUserLocked(userId).get(windowId);
}
return windowToken;
}
/**
* Return the userId that owns the given window token, {@link UserHandle#USER_NULL}
* if not found.
*
* @param windowToken The winodw token
* @return The userId
*/
public int getWindowOwnerUserId(@NonNull IBinder windowToken) {
return mWindowManagerInternal.getWindowOwnerUserId(windowToken);
}
/**
* Returns windowId of given userId and window token.
*
* @param userId The userId
* @param token The window token
* @return The windowId
*/
public int findWindowIdLocked(int userId, @NonNull IBinder token) {
final int globalIndex = mGlobalWindowTokens.indexOfValue(token);
if (globalIndex >= 0) {
return mGlobalWindowTokens.keyAt(globalIndex);
}
if (isValidUserForWindowTokensLocked(userId)) {
final int userIndex = getWindowTokensForUserLocked(userId).indexOfValue(token);
if (userIndex >= 0) {
return getWindowTokensForUserLocked(userId).keyAt(userIndex);
}
}
return -1;
}
private void sendEventsForChangedWindowsLocked(List<AccessibilityWindowInfo> oldWindows,
SparseArray<AccessibilityWindowInfo> oldWindowsById) {
List<AccessibilityEvent> events = new ArrayList<>();
// Send events for all removed windows
final int oldWindowsCount = oldWindows.size();
for (int i = 0; i < oldWindowsCount; i++) {
final AccessibilityWindowInfo window = oldWindows.get(i);
if (mA11yWindowInfoById.get(window.getId()) == null) {
events.add(AccessibilityEvent.obtainWindowsChangedEvent(
window.getId(), AccessibilityEvent.WINDOWS_CHANGE_REMOVED));
}
}
// Look for other changes
final int newWindowCount = mWindows.size();
for (int i = 0; i < newWindowCount; i++) {
final AccessibilityWindowInfo newWindow = mWindows.get(i);
final AccessibilityWindowInfo oldWindow = oldWindowsById.get(newWindow.getId());
if (oldWindow == null) {
events.add(AccessibilityEvent.obtainWindowsChangedEvent(
newWindow.getId(), AccessibilityEvent.WINDOWS_CHANGE_ADDED));
} else {
int changes = newWindow.differenceFrom(oldWindow);
if (changes != 0) {
events.add(AccessibilityEvent.obtainWindowsChangedEvent(
newWindow.getId(), changes));
}
}
}
final int numEvents = events.size();
for (int i = 0; i < numEvents; i++) {
mAccessibilityEventSender.sendAccessibilityEventForCurrentUserLocked(events.get(i));
}
}
/**
* Computes partial interactive region of given windowId.
*
* @param windowId The windowId
* @param outRegion The output to which to write the bounds.
* @return true if outRegion is not empty.
*/
public boolean computePartialInteractiveRegionForWindowLocked(int windowId,
@NonNull Region outRegion) {
if (mWindows == null) {
return false;
}
// Windows are ordered in z order so start from the bottom and find
// the window of interest. After that all windows that cover it should
// be subtracted from the resulting region. Note that for accessibility
// we are returning only interactive windows.
Region windowInteractiveRegion = null;
boolean windowInteractiveRegionChanged = false;
final int windowCount = mWindows.size();
final Rect currentWindowBounds = new Rect();
for (int i = windowCount - 1; i >= 0; i--) {
AccessibilityWindowInfo currentWindow = mWindows.get(i);
if (windowInteractiveRegion == null) {
if (currentWindow.getId() == windowId) {
currentWindow.getBoundsInScreen(currentWindowBounds);
outRegion.set(currentWindowBounds);
windowInteractiveRegion = outRegion;
continue;
}
} else if (currentWindow.getType()
!= AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY) {
currentWindow.getBoundsInScreen(currentWindowBounds);
if (windowInteractiveRegion.op(currentWindowBounds, Region.Op.DIFFERENCE)) {
windowInteractiveRegionChanged = true;
}
}
}
return windowInteractiveRegionChanged;
}
/**
* Updates active windowId and accessibility focused windowId according to given accessibility
* event and action.
*
* @param userId The userId
* @param windowId The windowId of accessibility event
* @param nodeId The accessibility node id of accessibility event
* @param eventType The accessibility event type
* @param eventAction The accessibility event action
*/
public void updateActiveAndAccessibilityFocusedWindowLocked(int userId, int windowId,
long nodeId, int eventType, int eventAction) {
// The active window is either the window that has input focus or
// the window that the user is currently touching. If the user is
// touching a window that does not have input focus as soon as the
// the user stops touching that window the focused window becomes
// the active one. Here we detect the touched window and make it
// active. In updateWindowsLocked() we update the focused window
// and if the user is not touching the screen, we make the focused
// window the active one.
switch (eventType) {
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
// If no service has the capability to introspect screen,
// we do not register callback in the window manager for
// window changes, so we have to ask the window manager
// what the focused window is to update the active one.
// The active window also determined events from which
// windows are delivered.
synchronized (mLock) {
if (!isTrackingWindowsLocked()) {
mFocusedWindowId = findFocusedWindowId(userId);
if (windowId == mFocusedWindowId) {
mActiveWindowId = windowId;
}
}
}
} break;
case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: {
// Do not allow delayed hover events to confuse us
// which the active window is.
synchronized (mLock) {
if (mTouchInteractionInProgress && mActiveWindowId != windowId) {
setActiveWindowLocked(windowId);
}
}
} break;
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
synchronized (mLock) {
if (mAccessibilityFocusedWindowId != windowId) {
clearAccessibilityFocusLocked(mAccessibilityFocusedWindowId);
setAccessibilityFocusedWindowLocked(windowId);
mAccessibilityFocusNodeId = nodeId;
}
}
} break;
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
synchronized (mLock) {
if (mAccessibilityFocusNodeId == nodeId) {
mAccessibilityFocusNodeId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
}
// Clear the window with focus if it no longer has focus and we aren't
// just moving focus from one view to the other in the same window
if ((mAccessibilityFocusNodeId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID)
&& (mAccessibilityFocusedWindowId == windowId)
&& (eventAction != AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS)) {
mAccessibilityFocusedWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
}
}
} break;
}
}
/**
* Callbacks from AccessibilityManagerService when touch explorer turn on and
* motion down detected.
*/
public void onTouchInteractionStart() {
synchronized (mLock) {
mTouchInteractionInProgress = true;
}
}
/**
* Callbacks from AccessibilityManagerService when touch explorer turn on and
* gesture or motion up detected.
*/
public void onTouchInteractionEnd() {
synchronized (mLock) {
mTouchInteractionInProgress = false;
// We want to set the active window to be current immediately
// after the user has stopped touching the screen since if the
// user types with the IME he should get a feedback for the
// letter typed in the text view which is in the input focused
// window. Note that we always deliver hover accessibility events
// (they are a result of user touching the screen) so change of
// the active window before all hover accessibility events from
// the touched window are delivered is fine.
final int oldActiveWindow = mActiveWindowId;
setActiveWindowLocked(mFocusedWindowId);
// If there is no service that can operate with interactive windows
// then we keep the old behavior where a window loses accessibility
// focus if it is no longer active. This still changes the behavior
// for services that do not operate with interactive windows and run
// at the same time as the one(s) which does. In practice however,
// there is only one service that uses accessibility focus and it
// is typically the one that operates with interactive windows, So,
// this is fine. Note that to allow a service to work across windows
// we have to allow accessibility focus stay in any of them. Sigh...
final boolean accessibilityFocusOnlyInActiveWindow = !isTrackingWindowsLocked();
if (oldActiveWindow != mActiveWindowId
&& mAccessibilityFocusedWindowId == oldActiveWindow
&& accessibilityFocusOnlyInActiveWindow) {
clearAccessibilityFocusLocked(oldActiveWindow);
}
}
}
/**
* Get the id of the current active window.
*
* @return The userId
*/
public int getActiveWindowId(int userId) {
if (mActiveWindowId == AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
&& !mTouchInteractionInProgress) {
mActiveWindowId = findFocusedWindowId(userId);
}
return mActiveWindowId;
}
private void setActiveWindowLocked(int windowId) {
if (mActiveWindowId != windowId) {
mAccessibilityEventSender.sendAccessibilityEventForCurrentUserLocked(
AccessibilityEvent.obtainWindowsChangedEvent(
mActiveWindowId, AccessibilityEvent.WINDOWS_CHANGE_ACTIVE));
mActiveWindowId = windowId;
if (mWindows != null) {
final int windowCount = mWindows.size();
for (int i = 0; i < windowCount; i++) {
AccessibilityWindowInfo window = mWindows.get(i);
if (window.getId() == windowId) {
window.setActive(true);
mAccessibilityEventSender.sendAccessibilityEventForCurrentUserLocked(
AccessibilityEvent.obtainWindowsChangedEvent(windowId,
AccessibilityEvent.WINDOWS_CHANGE_ACTIVE));
} else {
window.setActive(false);
}
}
}
}
}
private void setAccessibilityFocusedWindowLocked(int windowId) {
if (mAccessibilityFocusedWindowId != windowId) {
mAccessibilityEventSender.sendAccessibilityEventForCurrentUserLocked(
AccessibilityEvent.obtainWindowsChangedEvent(
mAccessibilityFocusedWindowId,
WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED));
mAccessibilityFocusedWindowId = windowId;
if (mWindows != null) {
final int windowCount = mWindows.size();
for (int i = 0; i < windowCount; i++) {
AccessibilityWindowInfo window = mWindows.get(i);
if (window.getId() == windowId) {
window.setAccessibilityFocused(true);
mAccessibilityEventSender.sendAccessibilityEventForCurrentUserLocked(
AccessibilityEvent.obtainWindowsChangedEvent(
windowId, WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED));
} else {
window.setAccessibilityFocused(false);
}
}
}
}
}
/**
* Returns accessibility window info according to given windowId.
*
* @param windowId The windowId
* @return The accessibility window info
*/
@Nullable
public AccessibilityWindowInfo findA11yWindowInfoById(int windowId) {
return mA11yWindowInfoById.get(windowId);
}
/**
* Returns the window info according to given windowId.
*
* @param windowId The windowId
* @return The window info
*/
@Nullable
public WindowInfo findWindowInfoById(int windowId) {
return mWindowInfoById.get(windowId);
}
/**
* Returns focused windowId or accessibility focused windowId according to given focusType.
*
* @param focusType {@link AccessibilityNodeInfo#FOCUS_INPUT} or
* {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}
* @return The focused windowId
*/
public int getFocusedWindowId(int focusType) {
if (focusType == AccessibilityNodeInfo.FOCUS_INPUT) {
return mFocusedWindowId;
} else if (focusType == AccessibilityNodeInfo.FOCUS_ACCESSIBILITY) {
return mAccessibilityFocusedWindowId;
}
return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
}
/**
* Returns {@link AccessibilityWindowInfo} of PIP window.
*
* @return PIP accessibility window info
*/
@Nullable
public AccessibilityWindowInfo getPictureInPictureWindow() {
if (mWindows != null) {
final int windowCount = mWindows.size();
for (int i = 0; i < windowCount; i++) {
final AccessibilityWindowInfo window = mWindows.get(i);
if (window.isInPictureInPictureMode()) {
return window;
}
}
}
return null;
}
/**
* Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture
* window.
*/
public void setPictureInPictureActionReplacingConnection(
@Nullable IAccessibilityInteractionConnection connection) throws RemoteException {
synchronized (mLock) {
if (mPictureInPictureActionReplacingConnection != null) {
mPictureInPictureActionReplacingConnection.unlinkToDeath();
mPictureInPictureActionReplacingConnection = null;
}
if (connection != null) {
RemoteAccessibilityConnection wrapper = new RemoteAccessibilityConnection(
AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID,
connection, "foo.bar.baz", Process.SYSTEM_UID, UserHandle.USER_ALL);
mPictureInPictureActionReplacingConnection = wrapper;
wrapper.linkToDeath();
}
}
}
/**
* Returns accessibility interaction connection for picture-in-picture window.
*/
@Nullable
public RemoteAccessibilityConnection getPictureInPictureActionReplacingConnection() {
return mPictureInPictureActionReplacingConnection;
}
/**
* Invokes {@link IAccessibilityInteractionConnection#notifyOutsideTouch()} for windows that
* have watch outside touch flag and its layer is upper than target window.
*/
public void notifyOutsideTouch(int userId, int targetWindowId) {
final List<Integer> outsideWindowsIds;
final List<RemoteAccessibilityConnection> connectionList = new ArrayList<>();
synchronized (mLock) {
outsideWindowsIds = getWatchOutsideTouchWindowIdLocked(targetWindowId);
for (int i = 0; i < outsideWindowsIds.size(); i++) {
connectionList.add(getConnectionLocked(userId, outsideWindowsIds.get(i)));
}
}
for (int i = 0; i < connectionList.size(); i++) {
final RemoteAccessibilityConnection connection = connectionList.get(i);
if (connection != null) {
try {
connection.getRemote().notifyOutsideTouch();
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling notifyOutsideTouch()");
}
}
}
}
}
private List<Integer> getWatchOutsideTouchWindowIdLocked(int targetWindowId) {
final WindowInfo targetWindow = mWindowInfoById.get(targetWindowId);
if (targetWindow != null && mHasWatchOutsideTouchWindow) {
final List<Integer> outsideWindowsId = new ArrayList<>();
for (int i = 0; i < mWindowInfoById.size(); i++) {
final WindowInfo window = mWindowInfoById.valueAt(i);
if (window != null && window.layer < targetWindow.layer
&& window.hasFlagWatchOutsideTouch) {
outsideWindowsId.add(mWindowInfoById.keyAt(i));
}
}
return outsideWindowsId;
}
return Collections.emptyList();
}
/**
* Gets current input focused window token from window manager, and returns its windowId.
*
* @param userId The userId
* @return The input focused windowId, or -1 if not found
*/
private int findFocusedWindowId(int userId) {
final IBinder token = mWindowManagerInternal.getFocusedWindowToken();
synchronized (mLock) {
return findWindowIdLocked(userId, token);
}
}
private AccessibilityWindowInfo populateReportedWindowLocked(int userId, WindowInfo window) {
final int windowId = findWindowIdLocked(userId, window.token);
if (windowId < 0) {
return null;
}
final AccessibilityWindowInfo reportedWindow = AccessibilityWindowInfo.obtain();
reportedWindow.setId(windowId);
reportedWindow.setType(getTypeForWindowManagerWindowType(window.type));
reportedWindow.setLayer(window.layer);
reportedWindow.setFocused(window.focused);
reportedWindow.setBoundsInScreen(window.boundsInScreen);
reportedWindow.setTitle(window.title);
reportedWindow.setAnchorId(window.accessibilityIdOfAnchor);
reportedWindow.setPictureInPicture(window.inPictureInPicture);
reportedWindow.setDisplayId(window.displayId);
final int parentId = findWindowIdLocked(userId, window.parentToken);
if (parentId >= 0) {
reportedWindow.setParentId(parentId);
}
if (window.childTokens != null) {
final int childCount = window.childTokens.size();
for (int i = 0; i < childCount; i++) {
final IBinder childToken = window.childTokens.get(i);
final int childId = findWindowIdLocked(userId, childToken);
if (childId >= 0) {
reportedWindow.addChild(childId);
}
}
}
return reportedWindow;
}
private int getTypeForWindowManagerWindowType(int windowType) {
switch (windowType) {
case WindowManager.LayoutParams.TYPE_APPLICATION:
case WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA:
case WindowManager.LayoutParams.TYPE_APPLICATION_PANEL:
case WindowManager.LayoutParams.TYPE_APPLICATION_STARTING:
case WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL:
case WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL:
case WindowManager.LayoutParams.TYPE_BASE_APPLICATION:
case WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION:
case WindowManager.LayoutParams.TYPE_PHONE:
case WindowManager.LayoutParams.TYPE_PRIORITY_PHONE:
case WindowManager.LayoutParams.TYPE_TOAST:
case WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG: {
return AccessibilityWindowInfo.TYPE_APPLICATION;
}
case WindowManager.LayoutParams.TYPE_INPUT_METHOD:
case WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG: {
return AccessibilityWindowInfo.TYPE_INPUT_METHOD;
}
case WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG:
case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR:
case WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL:
case WindowManager.LayoutParams.TYPE_SEARCH_BAR:
case WindowManager.LayoutParams.TYPE_STATUS_BAR:
case WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL:
case WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL:
case WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY:
case WindowManager.LayoutParams.TYPE_SYSTEM_ALERT:
case WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG:
case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR:
case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY:
case WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY:
case WindowManager.LayoutParams.TYPE_SCREENSHOT: {
return AccessibilityWindowInfo.TYPE_SYSTEM;
}
case WindowManager.LayoutParams.TYPE_DOCK_DIVIDER: {
return AccessibilityWindowInfo.TYPE_SPLIT_SCREEN_DIVIDER;
}
case TYPE_ACCESSIBILITY_OVERLAY: {
return AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY;
}
default: {
return -1;
}
}
}
private boolean isValidUserForInteractionConnectionsLocked(int userId) {
return mInteractionConnections.indexOfKey(userId) >= 0;
}
private boolean isValidUserForWindowTokensLocked(int userId) {
return mWindowTokens.indexOfKey(userId) >= 0;
}
private SparseArray<RemoteAccessibilityConnection> getInteractionConnectionsForUserLocked(
int userId) {
SparseArray<RemoteAccessibilityConnection> connection = mInteractionConnections.get(
userId);
if (connection == null) {
connection = new SparseArray<>();
mInteractionConnections.put(userId, connection);
}
return connection;
}
private SparseArray<IBinder> getWindowTokensForUserLocked(int userId) {
SparseArray<IBinder> windowTokens = mWindowTokens.get(userId);
if (windowTokens == null) {
windowTokens = new SparseArray<>();
mWindowTokens.put(userId, windowTokens);
}
return windowTokens;
}
private void clearAccessibilityFocusLocked(int windowId) {
mHandler.sendMessage(obtainMessage(
AccessibilityWindowManager::clearAccessibilityFocusMainThread,
AccessibilityWindowManager.this,
mAccessibilityUserManager.getCurrentUserIdLocked(), windowId));
}
private void clearAccessibilityFocusMainThread(int userId, int windowId) {
final RemoteAccessibilityConnection connection;
synchronized (mLock) {
connection = getConnectionLocked(userId, windowId);
if (connection == null) {
return;
}
}
try {
connection.getRemote().clearAccessibilityFocus();
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling clearAccessibilityFocus()");
}
}
}
/**
* Dump all {@link AccessibilityWindowInfo}s here.
*/
public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
if (mWindows != null) {
final int windowCount = mWindows.size();
for (int j = 0; j < windowCount; j++) {
if (j > 0) {
pw.append(',');
pw.println();
}
pw.append("Window[");
AccessibilityWindowInfo window = mWindows.get(j);
pw.append(window.toString());
pw.append(']');
}
pw.println();
}
}
}