慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

掘金

Win 安装Claude Code FastAPI 的 CORSMiddleware 跨域中间件 Java 自研 ReAct Agent 半年后,我用 LangGraph 验证了这些设计取舍 🚀AI编程工作流终极形态:GitNexus!零Token消耗实现代码知识图谱化!让Claude Code和Codex拥有上帝视角彻底告别盲目改代码,复杂项目重 LeetCode 72. 编辑距离:动态规划经典题解 被The Graph的GraphQL查询坑了三天,我用一个真实DeFi项目把链上数据索引彻底搞懂了 (AI) 编写简单 AI 助手 (ds-agent) 别再让 pnpm 跟着 nvm 跑了!独立安装终极指南 Claude Code 为什么这么顺?Anthropic 最新复盘:真正撑住它的不是模型,而是缓存 从 /simplify 指令深挖 Claude Code 多 Agent 协同机制 Function-Calling与工具使用 新手上路(六):Claude code装上ECC全家桶:38 个子代理、156 个技能、生产级 Hooks 与 Rules 体系 我在 Claude、Kimi、opencode 三个 AI 之间搭了一条自动协作管道 【技能篇】OpenClaw Skill 详解:给 AI 装上"专业外挂" wagmi v2 多链钱包切换:一个 Uniswap 仿盘项目让我踩了三天坑 两周浅学 RAG 我把 Python re 模块比喻成摸金手套 新手上路(三):Claude Code Skills 装了一堆没用?20+ 个 Skill 横向对比 + 三套组合方案,按需抄 K2.6、DeepSeek V4、GPT-5.5 都来了,组合拳打起来 Claude Code 进阶之路:从记忆系统到子代理编排 [java] 编译之后的记录类(Record Classes)长什么样子(上) 国产大模型能力大比拼,社区有话说 我研读了 500 个 Spring Boot 生产级代码库,90% 都犯了这 7 个致命错误 JAVA重点难点 转发-中央网信办部署开展“清朗·整治AI应用乱象”专项行动 合同同步逻辑 【合并已排序数组的三种实现策略,哪一种更可取?】 30天减20斤挑战:少一斤发100红包(2) 我竟然被JavaScript的隐式类型转换坑了三天! 二十五.Electron 初体验与进阶 本地到生产,解决 AI 全栈最后一公里——构建&部署&运维 程序员创业半年:顺的事、不顺的事,和我一直没想清楚的事 UI组件库elementplus 像使用 Redis 一样操作 LocalStorage 向量检索的流程是怎样的?Embedding 和 Rerank 各自的作用? LangChain DeepAgents 速通指南(七)—— DeepAgents使用Agent Skill 为什么越来越多的大厂抛弃MCP,转向CLI? 【节点】[SquareRoot节点]原理解析与实际应用 juejin.cn juejin.cn 从 “存得下” 到 “算得快”:工业物联网需要新一代时序数据平台越来越多工业用户开始意识到一个问题:**数据是存下来了, - 掘金 放弃 Claude 订阅?我用 8 年前的服务器,强跑 Google 最强开源模型 Gemma 4 真实测评! Python开发者狂喜!200+课时FastAPI全栈实战合集,10大模块持续更新中🔥 从 Claw-Code 看 AI 驱动的大型项目开发:2 人 + 10 个自治 Agent 如何产出 48K 行 Rust 代码 秒级创建实例,火山引擎 Milvus Serverless 让 AI Agent 开发更快更省火山引擎MilvusSer MediaPlayer 播放器架构:NuPlayer 的 Source/Decoder/Renderer 三驾马车 juejin.cn juejin.cn juejin.cn juejin.cn
Android Input Spy Window
dalancon · 2026-05-24 · via 掘金

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() / native createInputMonitor() 是另一套 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;
}

行為總結:

  1. 找到調用者 token 對應的當前觸摸窗口。
  2. 取出這個窗口正在接收的 pointer ids。
  3. 給其他正在接收這些 pointer 的窗口合成 ACTION_CANCEL
  4. 將這些 pointer 標記為 pilfering。
  5. 後續同一 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.SPYInputWindow 配置系統可信 overlay 旁聽觸摸
monitorGestureInput() 返回的 InputMonitorJava API,底層是 GestureMonitorSpyWindow返回手勢、Shell 手勢、overlay 外部點擊
monitorInput() global monitornative global monitor channel通常不適合WMS 內部全局 pointer 觀察

簡化選擇:

  • 已經有系統 overlay 窗口,並希望它旁聽觸摸:使用 INPUT_FEATURE_SPY
  • 系統組件要監聽 display 上的手勢,並可能識別後搶佔:使用 InputManager.monitorGestureInput()
  • WMS 內部需要全局觀察輸入,不需要窗口命中語義:使用 monitorInput() global monitor。