Skip to content

Instantly share code, notes, and snippets.

@felix021
Created August 25, 2026 03:03
Show Gist options
  • Select an option

  • Save felix021/814897ec38b40245fbda55603b6ff80c to your computer and use it in GitHub Desktop.

Select an option

Save felix021/814897ec38b40245fbda55603b6ff80c to your computer and use it in GitHub Desktop.
OnePlus 15 智慧键自定义:短按切换输入法(方案、效果与踩坑)

OnePlus 15 智慧键(Action Button)自定义:短按切换输入法

环境:OnePlus 15 (codename corvette) / ColorOS 16 / Android 16 / Magisk root / Debian chroot 目标:左侧实体键短按 = 循环切换输入法;长按 = 保留系统原生功能(相机/免打扰等) 状态:已上线运行,随 chroot 服务开机自启

一、效果

手势 行为
短按 系统弹一个 2 秒的状态胶囊(无法去除,见坑 1)+ 循环切换默认输入法
长按 执行系统设置里配置的原生功能(我配的"打开相机"),不影响输入法

输入法循环顺序 = enabled_input_methods 列表顺序,自动跳过选不上的幽灵条目(见坑 6)。

二、背景:这个键为什么难搞

OnePlus 15 取消了经典三段式滑块,换成可按压的"智慧键"。逆向结论(设备树 + 内核 + logcat 实证):

  • 设备树节点:/sys/firmware/devicetree/base/soc/gpio-hall-sensor/oplus-magic-buttom,GPIO 48-51,DT 里 keycode 735
  • 不产生任何 Linux input 事件/dev/input/event* 十几个设备全监听,按压零事件;音量下键是独立的 gpio_keys,别认错)
  • 不产生任何 uevent(netlink KOBJECT_UEVENT 监听为空)
  • GPIO 电平也不直接反映按压(debugfs gpio 只见 48-51 四脚 func0/func1 整组切换,是厂商驱动的重配置动作)
  • 按键由 OPLUS 私有栈处理:com.oplus.gesture (ActionButtonSettingsActivity / ActionKeyStartApp)
  • 手势语义:短按只弹状态胶囊(Fluid Cloud),长按才执行功能——早期测试全部失败就是因为一直短按
  • 功能档位配置:settings get/put system oplus_action_button_switch_state,取值如 ring_mode / no_disturb / camera
  • 手势分发时会打日志:logcat tag ActionKeyStartAppsingleTap / longPressStartApp 明确区分手势,且与档位无关——这就是最终方案的信叽数据源

三、方案

核心思路:旁路监听 OPLUS gesture app 的日志,短按(singleTap)时通过 ime set 循环切换输入法。不 hook、不修改系统,纯观察者。

物理短按
  └─ com.oplus.gesture 私有处理(无 input 事件)
       ├─ 原生动作: 状态胶囊
       └─ logcat: "ActionKeyStartApp: singleTap, ..."
            ▲ adb shell 流式读取
  chroot 守护进程 (python3, 开机自启)
       ├─ 正则匹配 singleTap → 1.2s 去抖
       └─ cycle_ime():
            settings get secure enabled_input_methods  (剥离 ";subtype" 后缀)
            settings get secure default_input_method   (当前)
            从下一个开始逐个 ime set X:
              "Unknown input method" → 幽灵条目,跳过
              成功 → 结束
  容错: adb 流断开 → 5s 重连; logcat -T 1 从最新行开始防止重放旧日志

守护进程核心(/usr/local/bin/ime-trigger.py)

ADB = ["/usr/bin/adb", "-s", "127.0.0.1:<port>"]
LOGCAT_CMD = "logcat -v time -T 1 -s ActionKeyStartApp:D"
TAP_RE = re.compile(r"ActionKeyStartApp\(.*\):\s+singleTap")
DEBOUNCE_S = 1.2

def cycle_ime() -> None:
    raw = shell("settings get secure enabled_input_methods")
    enabled = [e.split(";")[0] for e in raw.split(":") if e.split(";")[0]]
    current = shell("settings get secure default_input_method")
    if current not in enabled:
        return
    start = enabled.index(current)
    for offset in range(1, len(enabled)):
        candidate = enabled[(start + offset) % len(enabled)]
        result = shell(f"ime set {candidate} 2>&1")   # 关键: 2>&1
        if result and "Unknown input method" in result:
            continue                                    # 幽灵条目跳过
        log.info("IME %s -> %s", current, candidate)
        return

# 主循环: Popen(ADB + ["shell", LOGCAT_CMD]) 逐行读, 命中 TAP_RE 且过 debounce 即 cycle_ime()
# 流断开(proc.stdout EOF) → wait + sleep 5 → adb connect → 重开流

开机自启(chroot-init 服务 /etc/chroot-init.d/07-ime-trigger.sh)

#!/bin/bash
# Service: ime-trigger
# Description: watch action button singleTap via adb logcat; cycle default IME

start() {
    # 以普通用户跑, 保证 adb key 是已授权的那份
    nohup su - <user> -c 'exec /usr/bin/python3 /usr/local/bin/ime-trigger.py' >> /var/log/ime-trigger.log 2>&1 &
    echo $! > "$PID_FILE"
}

stop() {
    if [ -f "$PID_FILE" ]; then
        kill "$(cat "$PID_FILE")" 2>/dev/null
        rm -f "$PID_FILE"
    fi
    pkill -f 'python3 /usr/local/bin/ime-trigger.py' 2>/dev/null
}

依赖:本机 adbd 监听 TCP(adb connect 127.0.0.1:<port>),chroot 与 Android 同内核,/dev/input、settings 命令均可用。

备用链路(app 侧,默认关闭)

输入法切换 app 里加了个 manifest 注册的 RINGER_MODE_CHANGED receiver(响铃模式变化 → 直接 Settings.Secure.putString(DEFAULT_INPUT_METHOD),需要 WRITE_SECURE_SETTINGS)。默认关闭,原因见坑 2 和坑 4;留给未来 ROM 行为变化时手动启用。

四、踩坑记录(按信号源演进顺序)

  1. 没有 input 事件可用。event0-11 全监听 + uevent netlink + debugfs GPIO 轮询全空。结论:这个键在 Linux 侧不可见,一切 key remap 类工具(Key Mapper / Button Mapper)原理性失效。唯一出口是"状态变化的侧面观察"或厂商日志。
  2. RINGER_MODE_CHANGED 不投递给 manifest receiver(ColorOS 16 / targetSdk 35 的隐式广播限制)。app 侧常驻方案需要前台服务/无障碍等重权限,放弃。
  3. 响铃模式轮询方案的两个信号坑:a) 档位为 no_disturb 时只有 zen_mode 动、mode_ringer 不动;b) 短按根本不改任何状态——轮询天然只能抓到长按副作用。
  4. pm grant 权限坑:adb shell 无 GRANT_RUNTIME_PERMISSIONS(ColorOS 收紧);su -c pm grant 返回 0 但权限表不落盘。WRITE_SECURE_SETTINGS 走不通,app 直接切换不可行,改用 shell 侧 ime set
  5. 焦点在按键自己的设置页时,按压事件被设置页消费,只做预览,不执行功能,也不写日志。测试时务必退到桌面。
  6. 幽灵输入法条目enabled_input_methods 里残留已卸载/改名的开发中输入法(如 fcitx5 debug build),ime set 对它们报 "Unknown input method"——错误在 stderr,只看 stdout 会误判成功(我就这样翻过车:日志全是"(ok)",用户端键盘纹丝不动)。修复:2>&1 合并 + 失败自动试下一个 + 一定让用户肉体验收。
  7. 自测技巧:shell 可以伪造日志注入事件,不用每次都按实体键: adb shell 'log -t ActionKeyStartApp -p d "singleTap, fluidTxet=selftest"' (注意 -p d 指定级别,-d 是无效参数。)
  8. 附赠:本地 arm64 构建 APK 的坑(chroot on aarch64):AGP 从 Maven 拉的 aapt2 是 x86_64,经 binfmt/box64 运行时 argv 错位(子命令变成程序路径),daemon 模式必挂。解法:gradle.propertiesandroid.aapt2FromMavenOverride 指向一个自编译的 aarch64 ELF 启动器(exec box64.real + 真 aapt2),文件名必须叫 aapt2(AGP 会校验文件名和 ELF magic,shell 脚本不行)。

五、维护

sudo chroot-init status ime-trigger     # 服务状态
tail -f /var/log/ime-trigger.log        # 触发日志(含切换与跳过记录)
adb shell 'log -t ActionKeyStartApp -p d "singleTap"'   # 无线自测
  • 换循环列表:无需改代码,enabled 列表变了自动跟随;卸载残留会自动跳过
  • 换触发手势:把 TAP_RE 改成匹配 longPressStartApp 即可
  • 档位建议:配"长按=相机、短按=仅提示"类档位,短按原生副作用最小
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment