天天看點

Android Keyboard/Touch Panel分析

分析一下 Android 是如何讀取按鍵及Touch Panel 的驅動的。主要在 $(ANDROID_DIR)/frameworks/base/libs/ui/EventHub.cpp 這個檔案中,這是在 HAL 層,将一步步分析 Android 上層是如何接受事件的。   一, 先看一下 Android HAL Class EventHub 在  $(ANDROID_DIR)/frameworks/base/include/ui/eventhub.h 定義.   i. scan_dir(const char *dirname) // dirname = "/dev/input"     掃描 dirname 目錄, 該目錄下有 event0, event1 ..., 等裝置.   ii.  open_device(devname);   打開 /dev/input/event0, /dev/input/event1 等裝置.   這裡以打開 /dev/input/event0 裝置為例, 分析按鍵的底層處理.       for (attempt = 0; attempt < 10; attempt++) {

        fd = open(deviceName, O_RDWR);

        if (fd >= 0) break;

        usleep(100);

    }     首先會打開傳進來的裝置. 然後會擷取version, id等資訊.       if(ioctl(fd, EVIOCGNAME(sizeof(name) - 1), &name) < 1) {

    //fprintf(stderr, "could not get device name for %s, %s/n", deviceName, strerror(errno));

        name[0] = '/0';

    }

    擷取 driver name, 在這裡也就是 /dev/input/evevnt0, 也就是要到 Driver 裡面去讀取.     這個名字很重要, 之後要與 keyboard map 相比對.     這裡傳回的值是: name = "wayland_m_ebook_key_input"     為什麼會傳回這個值? 請看 event0 的 linux driver.     wayland_m_ebook_keypad_probe() 函數中,有以下語句:     gpio_key_input->name = "wayland_m_ebook_key_input".     是以這個值是在這個時候設定的。         int devid = 0;

    while (devid < mNumDevicesById) {

        if (mDevicesById[devid].device == NULL) {

            break;

        }

        devid++;

    }

    if (devid >= mNumDevicesById) {

        device_ent* new_devids = (device_ent*)realloc(mDevicesById,

                sizeof(mDevicesById[0]) * (devid + 1));

        if (new_devids == NULL) {

            LOGE("out of memory");

            return -1;

        }

        mDevicesById = new_devids;

        mNumDevicesById = devid+1;

        mDevicesById[devid].device = NULL;

        mDevicesById[devid].seq = 0;

    }       配置設定 new device, 将 device 資訊儲存至 mDeviceById[] 數組中.     mNumDevicesById: device 的數量     mDevicesById: devive 的資訊         new_mFDs = (pollfd*)realloc(mFDs, sizeof(mFDs[0]) * (mFDCount + 1));

    new_devices = (device_t**)realloc(mDevices, sizeof(mDevices[0]) * (mFDCount + 1));

    為 new_mFDs, mFDs 配置設定空間, 以備之後儲存每個 event(x) 的fd.       mFDs[mFDCount].fd = fd;

    mFDs[mFDCount].events = POLLIN;

    将 fd 放到 mFDs 數組中.           // See if this is a keyboard, and classify it.

    uint8_t key_bitmask[(KEY_MAX+1)/8];

    memset(key_bitmask, 0, sizeof(key_bitmask));

    LOGV("Getting keys...");

    if (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bitmask)), key_bitmask) >= 0) {

        //LOGI("MAP/n");

        //for (int i=0; i<((KEY_MAX+1)/8); i++) {

        //    LOGI("%d: 0x%02x/n", i, key_bitmask[i]);

        //}

        for (int i=0; i<((BTN_MISC+7)/8); i++) {

            if (key_bitmask[i] != 0) {

                device->classes |= CLASS_KEYBOARD;

                break;

            }

        }

        if ((device->classes & CLASS_KEYBOARD) != 0) {

            device->keyBitmask = new uint8_t[sizeof(key_bitmask)];

            if (device->keyBitmask != NULL) {

                memcpy(device->keyBitmask, key_bitmask, sizeof(key_bitmask));

            } else {

                delete device;

                LOGE("out of memory allocating key bitmask");

                return -1;

            }

        }

    }

    如果是 keyboard 的裝置.             if (test_bit(BTN_TOUCH, key_bitmask)) {

        uint8_t abs_bitmask[(ABS_MAX+1)/8];

        memset(abs_bitmask, 0, sizeof(abs_bitmask));

        LOGV("Getting absolute controllers...");

        if (ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(abs_bitmask)), abs_bitmask) >= 0)

        {

            if (test_bit(ABS_X, abs_bitmask) && test_bit(ABS_Y, abs_bitmask)) {

                device->classes |= CLASS_TOUCHSCREEN;

            }

        }     }           繼續分析 Android 是如何進行 keyboard 映射的:         char tmpfn[sizeof(name)];

        char keylayoutFilename[300];         // a more descriptive name

        device->name = name;         // replace all the spaces with underscores

        strcpy(tmpfn, name);

        for (char *p = strchr(tmpfn, ' '); p && *p; p = strchr(tmpfn, ' '))

            *p = '_';         // find the .kl file we need for this device

        const char* root = getenv("ANDROID_ROOT");

        snprintf(keylayoutFilename, sizeof(keylayoutFilename),

                 "%s/usr/keylayout/%s.kl", root, tmpfn);

        bool defaultKeymap = false;

        if (access(keylayoutFilename, R_OK)) {

            snprintf(keylayoutFilename, sizeof(keylayoutFilename),

                     "%s/usr/keylayout/%s", root, "qwerty.kl");

            defaultKeymap = true;

        }         ANDROID_ROOT 一般都設定為 /system         tmpfn 也就是 name 中的内容。而 name = wayland_m_ebook_key_input         是以 keylayoutFilename = /system/usr/keylayout/wayland_m_ebook_key_input.kl.         這個檔案儲存有按鍵的資訊。 可以在這個檔案中修改按鍵的鍵值。         如果沒有這個檔案則會去讀 define 的 qwerty.kl.                   device->layoutMap->load(keylayoutFilename);         load /system/usr/keylayout/wayland_m_ebook_key_input.kl 這個檔案進行分析。         KeyLayoutMap::load 在 KeyLayoutMap.cpp 中實作。         把按鍵的映射關系儲存在 :KeyedVector<int32_t,Key> m_keys; 中。       iii. EventHub::getEvent()      主要通過 read() 函數讀取按鍵事件 及進行 Map 鍵值映射。  

 二、再來看看 jni 層     com_android_server_KeyInputQueue.cpp   看看其中幾行的代碼: static JNINativeMethod gInputMethods[] = {

    { "readEvent",       "(Landroid/view/RawInputEvent;)Z",

            (void*) android_server_KeyInputQueue_readEvent },     可以看出, 上層(即 framework層)調用的接口是 readEvent, 實作函數是本檔案的android_server_KeyInputQueue_readEvent().   這個函數調用了 getEvent 讀取事件。也就是 EventHub.cpp 中的 EventHub::getEvent().   readEvent調用hub->getEvent讀了取事件,然後轉換成JAVA的結構。   三、再看看 jni 層對應的 java 層(經常封裝成 .jar 檔案)    KeyInputQueue.java      在frameworks/base/services/java/com/android/server/KeyInputQueue.java 裡建立了一個  InputDeviceReader 線程,它循環的讀取事件,然後把事件放入事件隊列裡,包括 key / touch panel. 

  四、事件分發

輸入事件分發線程 在frameworks/base/services/java/com/android/server/WindowManagerService.java裡建立了一個輸入事件分發線程,它負責把事件分發到相應的視窗上去。

按鍵觸摸屏流程分析:

    WindowManagerService類的構造函數

    WindowManagerService()

    mQueue = new KeyQ();

因為 WindowManagerService.java (frameworks/base/services/java/com/android/server)中有:

private class KeyQ extends KeyInputQueue implements KeyInputQueue.FilterCallback

KeyQ 是抽象類 KeyInputQueue 的實作,是以 new KeyQ類的時候實際上在 KeyInputQueue 類中建立了一個線程 InputDeviceReader 專門用來從裝置讀取按鍵事件.

代碼:

Thread mThread = new Thread("InputDeviceReader" ) { 

    public void  run() { 

        // 在循環中調用:      readEvent(ev); 

        ... 

        send =  preprocessEvent(di, ev); 

           //實際調用的是 KeyQ 類的 preprocessEvent 函數 

         ... 

        int keycode =  rotateKeyCodeLocked(ev.keycode); 

          int[] map =  mKeyRotationMap; 

        for (int i=0; i<N; i+=2 ) { 

            if (map ==  keyCode)  

            return map[i+1 ]; 

        } // 

        addLocked(di, curTime, ev.flags,RawInputEvent.CLASS_KEYBOARD,

                  newKeyEvent(di, di.mDownTime, curTime, down,keycode,  0 , scancode,...)); 

        QueuedEvent ev =  obtainLocked(device, when, flags, classType, event); 

    } 

}; 

readEvent() 實際上調用的是 com_android_server_KeyInputQueue.cpp (frameworks/base/services/jni)中的

static jboolean android_server_KeyInputQueue_readEvent(JNIEnv* env, jobject clazz,jobject event) 來讀取事件,

bool res = hub->getEvent(&deviceId, &type, &scancode, &keycode,&flags, &value, &when)調用的是EventHub.cpp (frameworks/base/libs/ui)中的:

    bool EventHub::getEvent (int32_t* outDeviceId, int32_t* outType,

            int32_t* outScancode, int32_t* outKeycode, uint32_t *outFlags,

            int32_t* outValue, nsecs_t* outWhen)

在函數中調用了讀裝置操作:res = read(mFDs.fd, &iev, sizeof(iev));

在構造函數 WindowManagerService()調用 new KeyQ() 以後接着調用了:

    mInputThread = new InputDispatcherThread();       

    ...     

    mInputThread.start();

來啟動一個線程 InputDispatcherThread

    run()

    process(); 

    QueuedEvent ev = mQueue.getEvent(...)

因為WindowManagerService類中: final KeyQ mQueue;

是以實際上 InputDispatcherThread 線程實際上從 KeyQ 的事件隊列中讀取按鍵事件,在process() 方法中進行處理事件。

    switch (ev.classType) 

    case RawInputEvent.CLASS_KEYBOARD: 

        ... 

        dispatchKey((KeyEvent)ev.event, 0, 0); 

        mQueue.recycleEvent(ev); 

        break;

    case RawInputEvent.CLASS_TOUCHSCREEN: 

        //Log.i(TAG, "Read next event " + ev); 

        dispatchPointer(ev, (MotionEvent)ev.event, 0, 0); 

        break;

  case RawInputEvent.CLASS_TRACKBALL:

        dispatchTrackball(ev, (MotionEvent)ev.event, 0, 0);

        break;

===============================================================

補充一些内容:

在寫程式時,需要捕獲KEYCODE_HOME、KEYCODE_ENDCALL、KEYCODE_POWER這幾個按鍵,但是這幾個按鍵系統做了特殊處理,

在進行dispatch之前做了一些操作,HOME除了Keygaurd之外,不分發給任何其他APP,ENDCALL和POWER也類似,是以需要我們系統

處理之前進行處理。

我的做法是自己定義一個FLAG,在自己的程式中添加此FLAG,然後在WindowManagerServices.java中擷取目前視窗的FLAG屬性,如果是我

們自己設定的那個FLAG,則不進行特殊處理,直接分發按鍵消息到我們的APP當中,由APP自己處理。

這部分代碼最好添加在

@Override

boolean preprocessEvent(InputDevice device, RawInputEvent event)

方法中,這個方法是KeyInputQueue中的一個虛函數,在處理按鍵事件之前的一個“預處理”。

PS:對HOME鍵的處理好像必需要修改PhoneWindowManager.java中的interceptKeyTi方法,具體可以參考對KeyGuard程式的處理。