Android Input Spy Window Analysis
本文整理 AOSP 14 中 spy window 的概念、調用鏈、分發原理、pilferPointers() 搶佔邏輯,以及它和 InputMonitor 的關係。重點結論:
spy window是一種InputWindow配置,核心標誌是InputConfig.SPY。InputManager.monitorGestureInput()返回的android.view.InputMonitor在 AOSP 14 中底層由GestureMonitorSpyWindow實現,因此它實際創建的是一個 spy window。InputManagerService.monitorInput()/ nativecreateInputMonitor()是另一套 global monitor 機制,不屬於窗口體系,沒有 Z-order 和 touchable region 概念。
1. Spy Window 的定義
方法: android.os.InputConfig
文件: frameworks/native/libs/input/android/os/InputConfig.aidl
/**
* An input spy window. This window will receive all pointer events within its touchable
* area, but will not stop events from being sent to other windows below it in z-order.
* An input event will be dispatched to all spy windows above the top non-spy window at the
* event's coordinates.
*/
SPY = 1 << 14,
含義:
- spy window 能收到自己 touchable area 內的 pointer event。
- 它不會攔截或阻止 Z-order 下方窗口接收事件。
- 只有位於“命中的最上層非 spy window”之上的 spy windows 會收到事件。
例如 Z-order 從上到下是:
spy1
spy2
appWindow
spy3
如果觸摸點命中 appWindow,則分發目標是:
appWindow + spy1 + spy2
spy3 不會收到,因為它在真正命中的非 spy window 下面。
2. Java API 到 Spy Window 的創建鏈路
2.1 方法: InputManager.monitorGestureInput(String name, int displayId)
文件: frameworks/base/core/java/android/hardware/input/InputManager.java
/**
* Monitor input on the specified display for gestures.
*
* @hide
*/
public InputMonitor monitorGestureInput(String name, int displayId) {
return mGlobal.monitorGestureInput(name, displayId);
}
這是 SystemUI、Shell 等系統組件通常調用的入口。
2.2 方法: InputManagerGlobal.monitorGestureInput(String name, int displayId)
文件: frameworks/base/core/java/android/hardware/input/InputManagerGlobal.java
/**
* @see InputManager#monitorGestureInput(String, int)
*/
public InputMonitor monitorGestureInput(String name, int displayId) {
try {
return mIm.monitorGestureInput(new Binder(), name, displayId);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
這裡通過 Binder 調用 IInputManager.monitorGestureInput(...),進入 system_server 的 InputManagerService。
2.3 方法: InputManagerService.monitorGestureInput(...)
文件: frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
@Override // Binder call
public InputMonitor monitorGestureInput(IBinder monitorToken, @NonNull String requestedName,
int displayId) {
if (!checkCallingPermission(android.Manifest.permission.MONITOR_INPUT,
"monitorGestureInput()")) {
throw new SecurityException("Requires MONITOR_INPUT permission");
}
final SurfaceControl sc = mWindowManagerCallbacks.createSurfaceForGestureMonitor(name,
displayId);
final InputChannel inputChannel = createSpyWindowGestureMonitor(
monitorToken, name, sc, displayId, pid, uid);
return new InputMonitor(inputChannel,
new InputMonitorHost(inputChannel.getToken()),
new SurfaceControl(sc, "IMS.monitorGestureInput"));
}
關鍵點:
- 調用者必須有
MONITOR_INPUT權限。 - WMS 創建一個用於 gesture monitor 的
SurfaceControl。 - IMS 調用
createSpyWindowGestureMonitor(...)創建 spy window。 - 返回給調用方的是
android.view.InputMonitor。
2.4 方法: InputManagerService.createSpyWindowGestureMonitor(...)
文件: frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
@NonNull
private InputChannel createSpyWindowGestureMonitor(IBinder monitorToken, String name,
SurfaceControl sc, int displayId, int pid, int uid) {
final InputChannel channel = createInputChannel(name);
monitorToken.linkToDeath(() -> removeSpyWindowGestureMonitor(channel.getToken()), 0);
synchronized (mInputMonitors) {
mInputMonitors.put(channel.getToken(),
new GestureMonitorSpyWindow(monitorToken, name, displayId, pid, uid, sc,
channel));
}
final InputChannel outInputChannel = new InputChannel();
channel.copyTo(outInputChannel);
return outInputChannel;
}
關鍵點:
- 創建一對 input channel。
- 用
GestureMonitorSpyWindow包裝 channel 和 surface。 - 以 channel token 為 key 保存到
mInputMonitors。 - 返回 client 側
InputChannel給調用者。
2.5 方法: GestureMonitorSpyWindow.GestureMonitorSpyWindow(...)
文件: frameworks/base/services/core/java/com/android/server/input/GestureMonitorSpyWindow.java
GestureMonitorSpyWindow(IBinder token, String name, int displayId, int pid, int uid,
SurfaceControl sc, InputChannel inputChannel) {
mWindowHandle = new InputWindowHandle(mApplicationHandle, displayId);
mWindowHandle.name = name;
mWindowHandle.token = mClientChannel.getToken();
mWindowHandle.layoutParamsType = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
mWindowHandle.ownerPid = pid;
mWindowHandle.ownerUid = uid;
mWindowHandle.replaceTouchableRegionWithCrop(null /* use this surface's bounds */);
mWindowHandle.inputConfig =
InputConfig.NOT_FOCUSABLE | InputConfig.SPY | InputConfig.TRUSTED_OVERLAY;
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.setInputWindowInfo(mInputSurface, mWindowHandle);
t.setLayer(mInputSurface, InputManagerService.INPUT_OVERLAY_LAYER_GESTURE_MONITOR);
t.show(mInputSurface);
t.apply();
}
這是 monitorGestureInput() 和 spy window 關聯的直接證據:
monitorGestureInput()
-> createSpyWindowGestureMonitor()
-> new GestureMonitorSpyWindow()
-> InputConfig.SPY
GestureMonitorSpyWindow 是沒有圖形 buffer 的輸入 surface,但它通過 setInputWindowInfo(...) 進入輸入窗口列表。
3. 直接使用 INPUT_FEATURE_SPY 的窗口路徑
除了 monitorGestureInput() 自動創建 spy window,系統窗口也可以直接設置 WindowManager.LayoutParams.INPUT_FEATURE_SPY。
3.1 方法/字段: WindowManager.LayoutParams.INPUT_FEATURE_SPY
文件: frameworks/base/core/java/android/view/WindowManager.java
/**
* An input spy window. This window will receive all pointer events within its touchable
* area, but will not stop events from being sent to other windows below it in z-order.
* An input event will be dispatched to all spy windows above the top non-spy window at the
* event's coordinates.
*
* @hide
*/
@RequiresPermission(permission.MONITOR_INPUT)
public static final int INPUT_FEATURE_SPY = 1 << 2;
3.2 方法: WindowManagerService.sanitizeSpyWindow(...)
文件: frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
/**
* You need MONITOR_INPUT permission to be able to set INPUT_FEATURE_SPY.
*/
private int sanitizeSpyWindow(int inputFeatures, String windowName, int callingUid,
int callingPid) {
if ((inputFeatures & INPUT_FEATURE_SPY) == 0) {
return inputFeatures;
}
final int permissionResult = mContext.checkPermission(
permission.MONITOR_INPUT, callingPid, callingUid);
if (permissionResult != PackageManager.PERMISSION_GRANTED) {
throw new IllegalArgumentException("Cannot use INPUT_FEATURE_SPY from '" + windowName
+ "' because it doesn't the have MONITOR_INPUT permission");
}
return inputFeatures;
}
直接設置 INPUT_FEATURE_SPY 也必須有 MONITOR_INPUT 權限。
3.3 方法/靜態映射: InputConfigAdapter.INPUT_FEATURE_TO_CONFIG_MAP
文件: frameworks/base/services/core/java/com/android/server/wm/InputConfigAdapter.java
private static final List<FlagMapping> INPUT_FEATURE_TO_CONFIG_MAP = List.of(
new FlagMapping(
LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL,
InputConfig.NO_INPUT_CHANNEL, false /* inverted */),
new FlagMapping(
LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY,
InputConfig.DISABLE_USER_ACTIVITY, false /* inverted */),
new FlagMapping(
LayoutParams.INPUT_FEATURE_SPY,
InputConfig.SPY, false /* inverted */));
這說明 Java 層 LayoutParams.INPUT_FEATURE_SPY 最終會轉換成 native/input 層可見的 InputConfig.SPY。
3.4 示例: UdfpsControllerOverlay.coreLayoutParams
文件: frameworks/base/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
private val coreLayoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
0 /* flags set in computeLayoutParams() */,
PixelFormat.TRANSLUCENT
).apply {
privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
}
}
這是直接把已有系統 overlay 窗口配置為 spy window 的例子。適合屏下指紋這類“overlay 自身需要旁聽觸摸,但不應吞掉普通窗口事件”的場景。
4. Native 側的安全約束
方法: InputDispatcher::setInputWindowsLocked(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
// Ensure all spy windows are trusted overlays
LOG_ALWAYS_FATAL_IF(info.isSpy() &&
!info.inputConfig.test(
WindowInfo::InputConfig::TRUSTED_OVERLAY),
"%s has feature SPY, but is not a trusted overlay.",
window->getName().c_str());
native 側強制所有 spy window 必須同時是 TRUSTED_OVERLAY。這是安全邊界:spy window 能旁聽觸摸流,不能開放給普通應用窗口。
5. InputDispatcher 如何選擇普通窗口和 Spy Window
5.1 方法: InputDispatcher::findTouchedWindowAtLocked(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
std::pair<sp<WindowInfoHandle>, std::vector<InputTarget>>
InputDispatcher::findTouchedWindowAtLocked(int32_t displayId, float x, float y, bool isStylus,
bool ignoreDragWindow) const {
// Traverse windows from front to back to find touched window.
const auto& windowHandles = getWindowHandlesLocked(displayId);
for (const sp<WindowInfoHandle>& windowHandle : windowHandles) {
const WindowInfo& info = *windowHandle->getInfo();
if (!info.isSpy() &&
windowAcceptsTouchAt(info, displayId, x, y, isStylus, getTransformLocked(displayId))) {
return {windowHandle, outsideTargets};
}
}
return {nullptr, {}};
}
普通觸摸目標明確排除 spy window:
!info.isSpy()
因此 spy window 不會成為常規 foreground touch target。
5.2 方法: InputDispatcher::findTouchedSpyWindowsAtLocked(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
std::vector<sp<WindowInfoHandle>> InputDispatcher::findTouchedSpyWindowsAtLocked(
int32_t displayId, float x, float y, bool isStylus) const {
// Traverse windows from front to back and gather the touched spy windows.
std::vector<sp<WindowInfoHandle>> spyWindows;
const auto& windowHandles = getWindowHandlesLocked(displayId);
for (const sp<WindowInfoHandle>& windowHandle : windowHandles) {
const WindowInfo& info = *windowHandle->getInfo();
if (!windowAcceptsTouchAt(info, displayId, x, y, isStylus, getTransformLocked(displayId))) {
continue;
}
if (!info.isSpy()) {
// The first touched non-spy window was found, so return the spy windows touched so far.
return spyWindows;
}
spyWindows.push_back(windowHandle);
}
return spyWindows;
}
這個方法體現了 spy window 的 Z-order 規則:
- 從上到下遍歷窗口。
- 命中的 spy window 會被收集。
- 一旦遇到第一個命中的非 spy window,就停止並返回已經收集的 spy windows。
- 所以非 spy window 下面的 spy window 不會收到該事件。
5.3 方法: InputDispatcher::findTouchedWindowTargetsLocked(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
auto [newTouchedWindowHandle, outsideTargets] =
findTouchedWindowAtLocked(displayId, x, y, isStylus);
std::vector<sp<WindowInfoHandle>> newTouchedWindows =
findTouchedSpyWindowsAtLocked(displayId, x, y, isStylus);
if (newTouchedWindowHandle != nullptr) {
// Process the foreground window first so that it is the first to receive the event.
newTouchedWindows.insert(newTouchedWindows.begin(), newTouchedWindowHandle);
}
for (const sp<WindowInfoHandle>& windowHandle : newTouchedWindows) {
ftl::Flags<InputTarget::Flags> targetFlags = InputTarget::Flags::DISPATCH_AS_IS;
if (canReceiveForegroundTouches(*windowHandle->getInfo())) {
targetFlags |= InputTarget::Flags::FOREGROUND;
}
tempTouchState.addOrUpdateWindow(windowHandle, targetFlags, entry.deviceId, pointerIds,
isDownOrPointerDown
? std::make_optional(entry.eventTime)
: std::nullopt);
}
調用順序是:
findTouchedWindowTargetsLocked()
-> findTouchedWindowAtLocked() // 找真正的 foreground window,排除 spy
-> findTouchedSpyWindowsAtLocked() // 找 foreground window 上方的 spy windows
-> insert foreground at begin // foreground 優先分發
-> addOrUpdateWindow(...) // 都加入 TouchState
因此 spy window 會收到同一條 pointer stream,但不會改變真正的 foreground window 選擇。
6. Spy Window 為什麼不是 Foreground Touch Target
方法: canReceiveForegroundTouches(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
bool canReceiveForegroundTouches(const WindowInfo& info) {
// A non-touchable window can still receive touch events (e.g. in the case of
// STYLUS_INTERCEPTOR), so prevent such windows from receiving foreground events for touches.
return !info.inputConfig.test(gui::WindowInfo::InputConfig::NOT_TOUCHABLE) && !info.isSpy();
}
spy window 可以收事件副本,但不會帶 InputTarget::Flags::FOREGROUND。這保證了系統手勢監聽不會破壞普通 app 的常規觸摸目標選擇。
7. pilferPointers: 從旁聽變成接管
7.1 方法: InputMonitor.pilferPointers()
文件: frameworks/base/core/java/android/view/InputMonitor.java
/**
* Takes all of the current pointer events streams that are currently being sent to this
* monitor and generates appropriate cancellations for the windows that would normally get
* them.
*/
public void pilferPointers() {
try {
mHost.pilferPointers();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
這是 Java InputMonitor 對外暴露的搶佔接口。
7.2 方法: InputManagerService.InputMonitorHost.pilferPointers()
文件: frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
private final class InputMonitorHost extends IInputMonitorHost.Stub {
private final IBinder mInputChannelToken;
@Override
public void pilferPointers() {
mNative.pilferPointers(mInputChannelToken);
}
}
Java 層調用會進入 native InputDispatcher::pilferPointers(...)。
7.3 方法: InputDispatcher::pilferPointersLocked(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
status_t InputDispatcher::pilferPointersLocked(const sp<IBinder>& token) {
auto [statePtr, windowPtr, displayId] = findTouchStateWindowAndDisplayLocked(token);
TouchState& state = *statePtr;
TouchedWindow& window = *windowPtr;
// Send cancel events to all the input channels we're stealing from.
CancelationOptions options(CancelationOptions::Mode::CANCEL_POINTER_EVENTS,
"input channel stole pointer stream");
std::bitset<MAX_POINTER_ID + 1> pointerIds = window.getTouchingPointers(deviceId);
options.pointerIds = pointerIds;
for (const TouchedWindow& w : state.windows) {
const std::shared_ptr<InputChannel> channel =
getInputChannelLocked(w.windowHandle->getToken());
if (channel != nullptr && channel->getConnectionToken() != token) {
synthesizeCancelationEventsForInputChannelLocked(channel, options);
}
}
// Prevent the gesture from being sent to any other windows.
window.addPilferingPointers(deviceId, pointerIds);
state.cancelPointersForWindowsExcept(deviceId, pointerIds, token);
return OK;
}
行為總結:
- 找到調用者 token 對應的當前觸摸窗口。
- 取出這個窗口正在接收的 pointer ids。
- 給其他正在接收這些 pointer 的窗口合成
ACTION_CANCEL。 - 將這些 pointer 標記為 pilfering。
- 後續同一 pointer stream 只繼續發給 pilfering 窗口。
7.4 方法: InputDispatcher::findTouchedWindowTargetsLocked(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
// If a window is already pilfering some pointers, give it this new pointer as well and
// make it pilfering. This will prevent other non-spy windows from getting this pointer,
// which is a specific behaviour that we want.
const int32_t pointerId = entry.pointerProperties[pointerIndex].id;
for (TouchedWindow& touchedWindow : tempTouchState.windows) {
if (touchedWindow.hasTouchingPointer(entry.deviceId, pointerId) &&
touchedWindow.hasPilferingPointers(entry.deviceId)) {
touchedWindow.addPilferingPointer(entry.deviceId, pointerId);
}
}
// Restrict all pilfered pointers to the pilfering windows.
tempTouchState.cancelPointersForNonPilferingWindows();
這段處理多指場景:如果某個 spy window 已經 pilfer 了一部分 pointer,新落下且也命中它的 pointer 會繼續歸它接管。
8. 使用場景和真實代碼
8.1 返回手勢
方法: EdgeBackGestureHandler.onInputEvent(...) 內部處理邏輯
文件: frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
} else if (dx > dy && dx > mTouchSlop) {
if (mAllowGesture) {
mThresholdCrossed = true;
// Capture inputs
mInputMonitor.pilferPointers();
mInputEventReceiver.setBatchingEnabled(true);
}
}
調用鏈:
SystemUI EdgeBackGestureHandler
-> InputManager.monitorGestureInput(...)
-> InputMonitor receives pointer stream through spy window
-> gesture crosses threshold
-> InputMonitor.pilferPointers()
-> InputDispatcher sends ACTION_CANCEL to app
-> remaining MOVE/UP go to SystemUI
適合場景:一開始不能攔截 app,否則邊緣普通點擊會被破壞;只有確認是返回手勢後才搶佔。
8.2 Clipboard Overlay 外部點擊
方法: ClipboardOverlayController.monitorOutsideTouches()
文件: frameworks/base/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
private void monitorOutsideTouches() {
InputManager inputManager = mContext.getSystemService(InputManager.class);
mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
mInputEventReceiver = new InputEventReceiver(
mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
@Override
public void onInputEvent(InputEvent event) {
if (event instanceof MotionEvent) {
MotionEvent motionEvent = (MotionEvent) event;
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (!mView.isInTouchRegion(
(int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
animateOut();
}
}
}
}
};
}
適合場景:overlay 想知道用戶是否點擊了外部區域,用於關閉 UI,但不需要從一開始吞掉 app 的觸摸。
8.3 UDFPS Overlay
方法/屬性: UdfpsControllerOverlay.coreLayoutParams
文件: frameworks/base/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
}
適合場景:已有系統 overlay 窗口需要監聽觸摸,同時不應該阻斷底層窗口的普通輸入。
9. 和 monitorInput Global Monitor 的區別
9.1 方法: InputManagerService.monitorInput(...)
文件: frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
/**
* Creates an input channel that will receive all input from the input dispatcher.
*/
public InputChannel monitorInput(String inputChannelName, int displayId) {
Objects.requireNonNull(inputChannelName, "inputChannelName not be null");
if (displayId < Display.DEFAULT_DISPLAY) {
throw new IllegalArgumentException("displayId must >= 0.");
}
return mNative.createInputMonitor(displayId, inputChannelName, Binder.getCallingPid());
}
這是老式 global monitor 路徑,返回的是 InputChannel,不是 android.view.InputMonitor。
9.2 方法: InputDispatcher::createInputMonitor(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
Result<std::unique_ptr<InputChannel>> InputDispatcher::createInputMonitor(int32_t displayId,
const std::string& name,
gui::Pid pid) {
openInputChannelPair(name, serverChannel, clientChannel);
std::shared_ptr<Connection> connection =
std::make_shared<Connection>(serverChannel, /*monitor=*/true, mIdGenerator);
mConnectionsByToken.emplace(token, connection);
mGlobalMonitorsByDisplay[displayId].emplace_back(serverChannel, pid);
mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, ...);
return clientChannel;
}
它把 channel 放進 mGlobalMonitorsByDisplay,不創建 InputWindowHandle,也不進入窗口 Z-order。
9.3 方法: InputDispatcher::addGlobalMonitoringTargetsLocked(...)
文件: frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
void InputDispatcher::addGlobalMonitoringTargetsLocked(std::vector<InputTarget>& inputTargets,
int32_t displayId) {
auto monitorsIt = mGlobalMonitorsByDisplay.find(displayId);
if (monitorsIt == mGlobalMonitorsByDisplay.end()) return;
for (const Monitor& monitor : selectResponsiveMonitorsLocked(monitorsIt->second)) {
InputTarget target;
target.inputChannel = monitor.inputChannel;
target.flags = InputTarget::Flags::DISPATCH_AS_IS;
inputTargets.push_back(target);
}
}
global monitor 是在 key/motion dispatch 階段額外追加的全局目標,不參與窗口命中測試。
9.4 示例: DisplayContent 創建 PointerEventDispatcher
方法/構造流程: DisplayContent.DisplayContent(...)
文件: frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
final InputChannel inputChannel = mWmService.mInputManager.monitorInput(
"PointerEventDispatcher" + mDisplayId, mDisplayId);
mPointerEventDispatcher = new PointerEventDispatcher(inputChannel);
適合場景:WMS 內部希望觀察 display 上的 pointer event,例如 task tap detection、鼠標位置追蹤等。
10. 對比總結
| 機制 | 本質 | 是否是窗口 | 是否受 Z-order 影響 | 是否受 touchable region 影響 | 是否適合 pilfer | 典型場景 |
|---|---|---|---|---|---|---|
InputConfig.SPY | InputWindow 配置 | 是 | 是 | 是 | 是 | 系統可信 overlay 旁聽觸摸 |
monitorGestureInput() 返回的 InputMonitor | Java API,底層是 GestureMonitorSpyWindow | 是 | 是 | 是 | 是 | 返回手勢、Shell 手勢、overlay 外部點擊 |
monitorInput() global monitor | native global monitor channel | 否 | 否 | 否 | 通常不適合 | WMS 內部全局 pointer 觀察 |
簡化選擇:
- 已經有系統 overlay 窗口,並希望它旁聽觸摸:使用
INPUT_FEATURE_SPY。 - 系統組件要監聽 display 上的手勢,並可能識別後搶佔:使用
InputManager.monitorGestureInput()。 - WMS 內部需要全局觀察輸入,不需要窗口命中語義:使用
monitorInput()global monitor。












