Android device monitors all input signals

Foreword:

Recently, the team received a product request and needed to monitor whether the user has input behavior on the Android device, so as not to disturb the user when customizing the recommendation. This refers to the input behavior of all applications on the device, not just a certain application.

This requirement is quite challenging, and requires a lot of FW layer knowledge, so around this requirement, multiple solutions were customized and discussed with many people. Finally, a relatively feasible solution was found. Therefore, Record it through this article and share it with those who have the same needs.

Let me introduce the general background here first. We are a customized device with many apps on it. Each app is in charge of a different team. Even the code and overall integration on the system side are the responsibility of different teams.

This requirement is highly dependent on the principle of the event distribution process, so it can be regarded as a practice of the event distribution process.

plan selection

Solution 1: The solution of APP integrating SDK

We can create an SDK for each APP to integrate. Because the SDK works in the APP, you can register the corresponding input monitor in the SDK.

For example, in the event distribution process, the event distribution of Activity will go to the Activity.onTouchEvent method, the method is as follows:

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

    return false;
}

This involves a member variable mWindow, and this we can build a proxy class hookWindow through hook after monitoring the start of the Activity, and replace the original mWindow, so as to realize the monitoring of input events.

Technically speaking, this solution is less difficult to implement and more feasible. But from a business point of view, hundreds of apps need to be connected to this SDK, which is the access cost of nearly a hundred apps, and more than a dozen teams are required to deal with and communicate, and some teams are even external, so this solution From a business point of view, the feasibility is extremely low. After it was proposed, it was directly rejected by our technical staff without even discussing with the product.

Solution 2: Change the framework solution

In the event distribution process, the general process is shown in the following figure:

 

As shown in the figure, after InputDispatcher receives the input signal, it is responsible for finding the corresponding window, and then distributes the input event to the corresponding window. Therefore, if we can make a hook in InputDispatcher, whenever a signal comes, we can distribute it to the outside world through this hook, and we can know whether the user has input behavior.

The advantage of this is that there is no need for any APP modification, the number of docking teams has been greatly reduced, and purely looking at the code, it seems that there is not a lot of code to be written. It only needs to send a notification to the outside world when the dispatchMotionLocked method is called. That's it.

However, after consideration, this solution was abandoned for the following reasons:

1. It is necessary to modify the FW layer, and it is the code of the main process. Once there is a problem with the writing, the consequences will be unimaginable, and even all the input events of the user will be invalid.

2. Multiple teams are involved, because different teams are responsible for the systems of our different equipment. It needs their cooperation to modify, and it involves external communication. Once communicated, scheduling and going online will become out of reach, and people are even unwilling to cooperate because of the first reason.

Solution 3: Listening to the underlying input source

As shown in the figure below, in the entire event distribution process, the bottom source is EventHub.

 

So what is EventHub listening to? Since EventHub can listen, can we do the same?

What EventHub listens to is actually several files under the dev/input folder, such as event0, event1, and event2, which represent different input sources. In fact, event0 cannot be called a file. They are actually driver files and cannot be read directly.

The advantages of doing this are as follows:

1. Not relying on external teams. It does not rely on any external team at all, as long as the APP is installed, it will take effect.

2. Security. Because it does not involve modification of the FW layer, the danger to the system is greatly reduced.

3. Rollback. The APP can be released and hot-updated, unlike the FW layer, once it is broken, you have to flash the ROM again.

So I summarized several implementation plans, 3 is undoubtedly the best effect, so the realization direction is mainly positioned on the third one.

Feasibility Analysis

If we directly obtain the event file through the adb command, it is found that it is not feasible, indicating that this is not a specific file.

adb pull dev/input/event0

If we add monitoring through FileObserver, it is found that it is not feasible, and an error will be prompted:

  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

Therefore, the simplest two monitoring solutions are naturally unworkable.

Next, we look at the system_server process through ps and find that it is only a system-level application, and we are also a system-level application, so since system_server can read event signals, then we can theoretically.

Therefore, we need the code in EventHub to see how EventHub takes values, and we can refer to the implementation in it to realize our needs.

EventHub implementation principle

Let's first look at the code when EventHub was created:

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);
    ...
}

In the constructor, a mINotifyFd is registered, and then the event is added through epoll_ctl binding, that is to say, if the mINotifyFd adds an event, it will send a signal to its registrant through mEpollFd and carry the eventItem object.

Then there will be two questions:

1. Which file is bound to mINotifyFd?

2. After epoll is awakened, who is notified?

The answer to the first question is in the addDeviceInputInotify method. In this method, the DEVICE_INPUT_PATH directory is bound, that is, if there are files added or deleted in the DEVICE_INPUT_PATH directory, a notification will be issued.

And DEVICE_INPUT_PATH="/dev/input" here.

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

The second question, the answer is in the getEvents method

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

After epoll_wait is woken up, the passed epoll_event object will be added to the mPendingEventItems collection. Next, we can traverse the mPendingEventItems collection for sequential processing.

Searching for information, I learned that inotify is a mechanism for monitoring file system events. When an event occurs, the inotify file descriptor will be readable. I guess this is why we failed to listen directly to the file before (unfortunately, I guessed wrong).

 

implementation plan

Therefore, referring to the implementation in EventHub, we can complete our requirements.

We can also register an inotify, and then add the watch file directory through inotify_add_watch, and also watch the "/dev/input" folder. Then bind and monitor through epoll_ctl, wake up when there is an event input, and read the file content in the mINotifyFd descriptor after waking up.

In fact, because our requirement is only to observe whether the user has input behavior, not to observe what the user has input, so we know that there is no need to parse the content in the mINotifyFd descriptor, as long as it occurs, it is considered to have input.

It is divided into two methods. The method createListenerInput is mainly used to create native layer objects, initialize related member variables, and enable listening.

The method readLastInputTime is responsible for reading the property value of the latest input time in the native object.

The implementation code related to the createListenerInput method is as follows:

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);
}

The implementation code related to the readLastInputTime method is as follows:

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;
}

But when actually running, it was found that it was restricted by the authority management, and an error was prompted:

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

Even if SElinux is turned off, it still prompts the same error.

inputflinger belongs to the system_server process, and the system_server process belongs to the system-level application. And my application is also at the system level, so why system_server is OK, but my application is not, the reason for this has not been found yet, and it is still under investigation.

statement

This technical solution is for reference only, and it is strictly prohibited to be used for any commercial activities for illegal purposes.

This plan is only a directional exploration, and has not been truly realized. Whether it can be realized in the end is still unknown. This article is just a preliminary sharing. Of course, people with similar directions or needs are welcome to discuss or give guidance.

Guess you like

Origin blog.csdn.net/AA5279AA/article/details/131770972