安卓设备监听全部输入信号

前言:

最近团队收到一个产品需求,需要监听安卓设备上用户是否有输入行为,以免定制推荐的时候打搅到用户。这里指的是设备上所有应用的输入行为,而不是单指某一个应用。

这个需求还是蛮有挑战性的,需要涉及到很多FW层的知识,所以围绕着这个需求,定制了多个方案,并且也找了许多人进行讨论,总算有了一个相对可行的方案,因此,通过本文记录一下,也分享给有同样需求的后来者。

这里先介绍一下大背景,我们是定制的设备,设备上有很多的APP,每个APP都是不同的团队来负责的。甚至于系统侧的代码和整体集成,也是不同的来团队负责的。

该需求高度依赖事件分发流程的原理,所以算是对事件分发流程的一个实践。

方案选择

方案1:APP集成SDK的方案

我们可以出一个SDK,让每个APP去集成。因为SDK是作用于APP中的,所以可以在SDK中,去注册相应的输入监听。

举个例子,事件分发流程中,Activity的事件分发都会走到Activity.onTouchEvent方法,方法如下:

public boolean onTouchEvent(MotionEvent event) {if (mWindow.shouldCloseOnTouch(this, event)) {finish();return true;}return false;
}

这里涉及到一个成员变量mWindow,而这个我们可以在监听到Activity启动后,通过hook的方式构建一个代理类hookWindow,替换调原有的mWindow,从而实现输入事件的监听。

这个方案技术来讲,实现难度比较低,可行性较高。但是从业务角度上,就需要上百个APP都接入这个SDK,就是近百个APP的接入成本,并且需要个十几个团队打交道去沟通,甚至有的团队还是外部的,所以这个方案从业务角度上,可行性是极低的,提出来后甚至没有和产品商量,直接被我们技术内部否决了。

方案2:改framework方案

事件分发流程中,大体流程如下图所示:

 

如图所示,InputDispatcher收到了输入信号之后,负责找到对应的window,然后再把输入事件分发给对应的window。所以,如果我们能在InputDispatcher中做一个钩子,每当有信号过来的时候,通过这个钩子向外界分发,我们就可以知道用户是否有输入的行为了。

这样做的优势就是不需要任何APP的修改,对接团队的数量大幅的降低了,而且纯看代码,好像要写的代码也不是很多,只需要在dispatchMotionLocked方法被调用的时候,向外界发出一个通知就可以了。

但是经过考虑,还是放弃了这个方案,原因有以下几个:

1.需要对FW层进行修改,而且是主流程的代码,一旦写的有问题,造成的后果将不可想象,甚至会导致用户所有的输入事件全部失效。

2.涉及到了多个团队,因为我们不同的设备的系统是不同的团队来负责的。需要他们配合才能修改,又涉及到了外部沟通。一旦沟通,那么排期,上线就是变的遥不可及,甚至于人家处于第一个原因根本不愿意配合去做。

方案3:监听底层输入源

如下图所示,整个事件分发流程中,最底层的来源是EventHub。

 

那么EventHub监听的是什么呢?既然EventHub可以监听,那么我们是否可以做一个同样的监听呢?

EventHub监听的其实是dev/input文件夹下的几个文件,比如event0,event1,event2这样,分别代表不同的输入来源。其实event0也不能称之为文件,他们其实属于驱动文件,并不能直接读的。

这样做的优势有如下几个:

1.不依赖外部团队。完全不依赖任何的外部团队,只要装了APP就可以生效。

2.安全性。因为不涉及到FW层修改,所以对系统的危险性大大降低。

3.可回退性。APP是可发版可热更新的,不像是FW层,一旦改坏了,就得重新刷ROM了。

所以总结了一下几个实现方案,3无疑是效果最好的,所以把实现方向,主要定位到第三个上。

可行性分析

如果我们直接通过adb命令获取event文件,发现是不可行的,说明这不是一个具体的文件。

adb pull dev/input/event0

如果我们通过FileObserver的方式添加监听,发现也是不可行的,会提示错误:

  I  type=1400 audit(0.0:2293): avc: denied { read } for name="event0" dev="tmpfs" ino=540 scontext=u:r:system_app:s0 tcontext=u:object_r:input_device:s0 tclass=chr_file permissive=1

所以,最简单的两个监听方案,很自然是走不通的。

接下来,我们通过ps看一下system_server进程,发现其只是一个system级别的应用,而我们也是一个system级别的应用,所以既然system_server可以读取event信号,那么我们理论上也是可以的。

所以,我们就需要EventHub中的代码,来看看EventHub是怎么取值的,我们可以参考着其中的实现而实现我们的需求。

EventHub实现原理

我们首先看一下EventHub创建时的代码:

EventHub::EventHub(void){mEpollFd = epoll_create1(EPOLL_CLOEXEC);mINotifyFd = inotify_init1(IN_CLOEXEC);struct epoll_event eventItem = {};eventItem.events = EPOLLIN | EPOLLWAKEUP;eventItem.data.fd = mINotifyFd;int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);...
}

构造函数中,注册了一个mINotifyFd,然后通过epoll_ctl绑定添加事件,也就是说如果mINotifyFd如果新的添加事件时,会通过mEpollFd向其注册者发送信号,并且携带eventItem对象。

那么就会有两个问题:

1.mINotifyFd绑定的是哪个文件?

2.epoll被唤醒后,通知的是谁?

第一个问题,答案在addDeviceInputInotify方法中,这个方法中,绑定了DEVICE_INPUT_PATH目录,也就是说,DEVICE_INPUT_PATH目录中,如果有文件添加或者删除,则会发出通知。

而这里的DEVICE_INPUT_PATH="/dev/input"。

void EventHub::addDeviceInputInotify() {mDeviceInputWd = inotify_add_watch(mINotifyFd, DEVICE_INPUT_PATH, IN_DELETE | IN_CREATE);
}

第二个问题,答案在getEvents方法中

size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {...int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);...
}

epoll_wait被唤醒后,传递过来的epoll_event对象将会被添加到mPendingEventItems集合中。接下来,我们就可以遍历mPendingEventItems集合进行依次处理了。

搜寻资料,得知inotify是用来监视文件系统事件的机制,当有事件发生时,inotify文件描述符会可读。我猜测这也就是为什么之前我们直接监听文件失败的原因(很遗憾,猜错了)。

 

实施方案

所以,参考EventHub中的实现,我们就可以完成我们的需求了。

我们可以也注册一个inotify,然后通过inotify_add_watch添加观察文件目录,也观察"/dev/input"文件夹。然后通过epoll_ctl绑定监听,当有事件输入时进行唤醒,唤醒后读取mINotifyFd描述符中的文件内容。

其实,因为我们的需求只是观察用户是否有输入行为,而不是观察用户输入了什么,所以我们深知都不需要解析mINotifyFd描述符中的内容,只需要发生了,就认为有输入。

分为两个方法,方法createListenerInput主要用于创建native层对象,并且初始化相关的成员变量,以及开启监听。

方法readLastInputTime则负责读取native对象中的最近输入时间这个属性值。

createListenerInput方法相关的实现代码如下:

void ListenerInput::registerWatchInputTime() {LOGI("%s%d", "registerWatchInputTime,mINotifyFd:", mINotifyFd);int mDeviceInputWd = inotify_add_watch(mINotifyFd, DEVICE_INPUT_PATH, IN_DELETE | IN_CREATE);LOGI("%s%d", "registerWatchInputTime,mDeviceInputWd:", mDeviceInputWd);struct epoll_event eventItem = {};eventItem.events = EPOLLIN | EPOLLWAKEUP;eventItem.data.fd = mINotifyFd;int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);LOGI("%s%d", "epoll_ctl.result:", result);startThread();
}void ListenerInput::listenerInput() {for (;;) {int pollResult = epoll_wait(mEpollFd, mPendingEventItems, 16, 10000L);LOGI("%s%d", "epoll_wait.pollResult:", pollResult);if (pollResult == 0) {// Timed out.break;}}
}void ListenerInput::startThread() {std::thread myThread(&ListenerInput::listenerInput, this);myThread.detach();
}JNIEXPORT jlong JNICALL
Java_com_beantechs_watchinput_WatchInput_createListenerInput(JNIEnv *env, jobject instance) {LOGI("%s", "Java_com_beantechs_watchinput_WatchInput_createListenerInput start");ListenerInput *listenerInput = new ListenerInput();listenerInput->registerWatchInputTime();LOGI("%s", "Java_com_beantechs_watchinput_WatchInput_createListenerInput end");return reinterpret_cast<long>(listenerInput);
}

readLastInputTime方法相关的实现代码如下:

long ListenerInput::readLastInputTime() {LOGI("%s%ld", "readLastInputTime",mLastInputTime);return mLastInputTime;
}JNIEXPORT jlong JNICALL
Java_com_beantechs_watchinput_WatchInput_readLastInputTime(JNIEnv *env, jobject instance,jlong ptr) {LOGI("%s", "Java_com_beantechs_watchinput_WatchInput_readLastInputTime start");LOGI("%s%lld", "ptr:", ptr);long nativeLongValue = static_cast<long>(ptr);ListenerInput *listenerInput = reinterpret_cast<ListenerInput *>(nativeLongValue);long inputTime = listenerInput->readLastInputTime();LOGI("%s%ld", "Java_com_beantechs_watchinput_WatchInput_readLastInputTime end,inputTime:",inputTime);return 1l;
}

但是实际运行的时候,发现又被权限管理给限制掉了,提示错误:

type=1400 audit(0.0:7273): avc: denied { read } for name="input" dev="tmpfs" ino=10275 scontext=u:r:system_app:s0 tcontext=u:object_r:input_device:s0 tclass=dir permissive=1

哪怕关掉了SElinux,仍然提示同样的错误。

inputflinger归属system_server进程,而system_server进程属于system级别的应用。而我的应用也是system级别的,所以为什么system_server可以,我的应用不行,这块的原因还未找到,还处于排查中。

声明

本技术方案仅供参考,严禁用于任何非法目的商业活动。

本方案只是一个方向性的探索,并没有真正的去实现,最终是否能够实现也是一个未知数。这篇文章只是做一个初步的分享,当然欢迎有类似方向或者需求的人一起讨论或者给予指引。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/27927.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue新增删除内容排序问题解决处理

本次答题选项的删除添加是个人最初比较头疼的地方。比如ABCD四个选项&#xff0c;删除c选项后&#xff0c;点击【新增答题类型】选项按钮&#xff0c;则默认创建是E选项。再或者就是ABCD四个选项位置删除任意一个后&#xff0c;顺序被打乱等&#xff0c;最后解决了&#xff0c;…

【状态估计】基于UKF法、AUKF法的电力系统三相状态估计研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【项目 进程2】2.3 进程创建 2.4父子进程虚拟地址空间 2.5GDB多进程调试

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 2.3 进程创建2.4 父子进程虚拟地址空间父子进程之间的关系&#xff1a; 2.5 GDB多进程调试 2.3 进程创建 系统允许一个进程创建新进程&#xff0c;新进程即为子进程…

Vue2 ➔ Vue3 都做了哪些改变?

不是吧&#xff0c;兄弟&#xff0c;Vue3 都出来多久了&#xff0c;你还对这个感兴趣&#xff0c;说&#xff01;是不是没好好卷&#xff1f;&#x1f60f; 俺也一样 &#x1f602;&#xff0c;Vue3 出来之后只是简单了解了一下&#xff0c;然后还是转头一直在写 Vue2。当然&a…

基于weka手工实现多层感知机(BPNet)

一、BP网络 1.1 单层感知机 单层感知机&#xff0c;就是只有一层神经元&#xff0c;它的模型结构如下1&#xff1a; 对于权重 w w w的更新&#xff0c;我们采用如下公式&#xff1a; w i w i Δ w i Δ w i η ( y − y ^ ) x i (1) w_iw_i\Delta w_i \\ \Delta w_i\eta(y…

Maven —— 项目管理工具

前言 在这篇文章中&#xff0c;荔枝会介绍如何在项目工程中借助Maven的力量来开发&#xff0c;主要涉及Maven的下载安装、环境变量的配置、IDEA中的Maven的路径配置和信息修改以及通过Maven来快速构建项目。希望能对需要配置的小伙伴们有帮助哈哈哈哈~~~ 文章目录 前言 一、初…

设计模式-组合模式在Java中的使用示例-杀毒软件针对文件和文件夹进行杀毒

场景 组合模式 组合模式(Composite Pattern)&#xff1a; 组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。 组合模式对单个对象&#xff08;即叶子对象&#xff09;和组合对象&#xff08;即容器对象&#xff09;的使用具有一致性&#xff0c; 组合模式…

排序算法之冒泡排序详解-python版

冒泡排序&#xff1a;通过比较2个相邻元素之间的大小&#xff0c;交换元素顺序&#xff0c;从而达到排序目的。 从百度百科摘抄下来的冒泡排序原理如下&#xff1a; 比较相邻的元素。如果第一个比第二个大&#xff0c;就交换他们两个。 对每一对相邻元素做同样的工作&#xf…

elementUI 非表单格式的校验

在普通表单中对输入框、选择框都有校验案例。 但是在自定义非空中如何进行校验官网并没有说明 关键代码 clearValidate 方法清除校验 this.$refs.formValue.clearValidate(signinimg) 使用案例 <template><div class"stylebg"><Tabs icons"el-…

MySQL原理探索——31 误删数据后除了跑路,还能怎么办

在前面几篇文章中&#xff0c;介绍了 MySQL 的高可用架构。当然&#xff0c;传统的高可用架构是不能预防误删数据的&#xff0c;因为主库的一个 drop table 命令&#xff0c;会通过 binlog 传给所有从库和级联从库&#xff0c;进而导致整个集群的实例都会执行这个命令。 虽然我…

blender 阵列修改器

效果 tab 键进入编辑模式&#xff0c;全选制作好的模型&#xff0c;gx 移动模型置于游标原点&#xff1b; 阵列修改器&#xff1a; 相对偏移&#xff1a;以物体的长宽高为比例&#xff0c;调整x y z 的数值&#xff0c;在 x y z 方向上做不同比例的偏移&#xff1b; 恒定偏移…

C#安装.Net平台科学计算库Math.Net Numerics

工作的时候需要使用到C#的Math.Net库来进行计算。 Math.Net库涵盖的主题包括特殊函数&#xff0c;线性代数&#xff0c;概率模型&#xff0c;随机数&#xff0c;插值&#xff0c;积分&#xff0c;回归&#xff0c;优化问题等。 这里记录一下&#xff0c;安装Math.Net库的过程…