Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android基於wifi的無線HID設備實現

Android基於wifi的無線HID設備實現

編輯:關於Android編程

偶然間突發奇想,想到能不能讓我們的在我們的手機設備上滑動觸摸屏進而控制pc上的鼠標移動,也就說把我們的android設備當成是pc設備的觸摸板呢?要想實現這個目標,首先要想一想android設備和pc設備之間的通訊基礎是什麼?這個通訊技術必須是android和pc同時支持的,目前看來也就是wifi,藍牙。首先說一下藍牙,藍牙是一個提供個人局域網的安全無線電通訊技術,相對於wifi而言,藍牙的功耗相對較低,尤其是BLE技術使得藍牙的功耗可以和zigbee媲美了,並且android也支持了基於藍牙的socket操作。但是pc上的java部分對於藍牙的socket支持就不是很好了,實現起來比較麻煩。但是wifi雖然功耗相對藍牙而言比較高了點,但是實現起來非常容易,就是socket就好了!所以在第一版本中,可以先使用wifi作為傳輸技術。
解決了傳輸技術之後,還需要解決的是都有哪些數據類型,怎麼傳遞數據,使用什麼樣的協議的問題。這些問題很關鍵,這涉及到以後的程序可擴展性問題,如果這部分欠缺考慮的話,那麼後期的修改和擴展將是一個災難。進過仔細考量之後,決定采用google的protobuf來封裝所有的數據,因為protobuf靈活,小巧,高效,正好就是我要的。
進過了幾天的業余時間開發,終於出來了一個可以運行展示的初級版本,這個版本可以滿足基本的需求。目前我已經將這個代碼開源出來了,項目地址是github:https://github.com/CreateChance/WirelessHid

UI的設計

我要做的就是使用手機實現一個touchpad和keyboard,這就決定了UI的設計必須符合我們日常見到的實體touchpad和keyboard的樣式。進過設計之後,touchpad部分設計為一個fragment,它的布局如下:



    

        

            

            

            

        

        

        

        

            

            

            

        

    

    

運行時的效果如下:
這裡寫圖片描述vc+4tNTTwcujrNXisr+31tKyysfSu7j2ZnJhZ21lbnSjrNX7zOWyvL7WyOfPwqO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;">

這個布局中首先放上3個textview在一個LinearLayout這三個textview充當真實鍵盤上的3個led燈:num lock, caps lock, scroll lock。然後就是一個存放實際keyboard布局的LinearLayout容器,這麼做的目的是這樣的:因為手機的屏幕很小,想要放下一個標准鍵盤上的所有的按鍵肯定是不行的,因此需要將鍵盤分區,然後分別展示,這裡的這個容器就是用來存放不同分區,不同布局的鍵盤部分。目前我把鍵盤分成了2個部分:主鍵盤部分,導航部分加上數字部分。其中主鍵盤部分是我們最常使用的部分,這部分包含了26個字母,0~9數字(字母上排),12個功能鍵等,導航部分就是上下左右鍵盤,上頁下頁部分等,數字部分就是數字小鍵盤和一些控制按鍵,我把導航鍵和數字鍵合並在一起了,這兩部分的布局如下:
主鍵盤部分:


    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        
        
        
        
        
        
        
    

導航鍵部分:


    
        
        
        
    
    
        
        
        
    
    
        
        
        
    
    
        
        
        
    
    
        
        
        
    

數字鍵部分:


    
        
        
        
        
    
    
        
        
        
        
    
    
        
        
        
        
    
    
        
        
        
        
    
    
        
        
        
    

這裡的布局需要說明一下,這裡我使用了layout標簽表明,然後使用XmlResourceParser類來解析這個裡面的內容,最後再添加到布局中去。下面貼出兩張鍵盤的運行效果圖:
主鍵盤:
這裡寫圖片描述
從鍵盤(導航鍵和數字鍵):
這裡寫圖片描述

代碼設計

Server端

Server整體代碼就是一個app,內容不是很復雜,這裡我只陳述我的代碼功能和必要的代碼片段,詳細代碼內容有限於篇幅就不貼出來了,可以查看我的github項目主頁(https://github.com/CreateChance/WirelessHid)上的開源代碼。
代碼的基本分布如下:
這裡寫圖片描述
各個類的作用如下:

MainActivity

這是主界面類,基本就是MouseFragment的容器,另外就是監聽用戶點擊回退事件,如果用戶在1.5s之內連續點擊兩次回退就退出app,基本邏輯比較簡單。

WirelessHidService

這是整個app的服務,這個服務是實際將數據發送出去的地方,主要就是通過looper和handler的方式將消息隊列中的數據發送出去。發送部分的邏輯:
一個looper線程

private class DataSendThread extends Thread {

        private OutputStream os = null;

        @Override
        public void run() {
            super.run();

            Looper.prepare();

            try {
                Log.d(TAG, "I'm waiting for connecting.");
                mServerSocket = new ServerSocket(Constant.HID_TCP_PORT);
                mServerSocket.setReuseAddress(true);
                mSocket = mServerSocket.accept();
                os = mSocket.getOutputStream();
                Toast.makeText(getApplicationContext(), "Client connected!",
                        Toast.LENGTH_SHORT).show();
                Log.d(TAG, "client connected!");
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }

            mDataSendHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);

                    // send data here.
                    try {
                        ((WirelessHidProto.HidData)msg.obj).writeDelimitedTo(os);
                    } catch (IOException e) {
                        Log.d(TAG, "IOException, close all resource.");
                        mDataSendHandler = null;
                        if (mListener != null) {
                            mListener.onHandlerChanged(mDataSendHandler);
                        }
                        this.getLooper().quit();
                        sendBroadcast(new Intent(ACTION_RESET_CONNECTION));
                    } finally {
                        try {
                            mServerSocket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };

            if (mListener != null) {
                mListener.onHandlerChanged(mDataSendHandler);
            }

            Looper.loop();
        }
    }

界面Fragment使用Handler和server交互,fragment需要實現server的DataHandlerListener接口,當Handler變化的時候通知Fragment,以便Fragment拿到最新的對象引用:
需要實現的接口

public interface DataHandlerListener {
        void onHandlerChanged(Handler handler);
}

設置listener的接口:

public void setListener(DataHandlerListener listener) {
        this.mListener = listener;
}

fragment也可以主動獲取:

public Handler getDataSendHandler() {
    return this.mDataSendHandler;
}

這裡的發送使用的就是protobuf的序列化接口,關於這個接口的描述這裡就不詳述,可以參考google protobuf的java部分的編程指導:
https://developers.google.com/protocol-buffers/docs/javatutorial

MouseFragment

這個類是fragment類,主要是嵌套在MainActivity類中,主要邏輯功能如下:
1. 捕獲用戶觸摸屏移動,點擊事件

@Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //single and double click handle here.
                        mPrevX = (int) event.getX();
                        mPrevY = (int) event.getY();
                        time = new Date().getTime();
                        break;
                    case MotionEvent.ACTION_UP:
                        if (new Date().getTime() - time < mDoubleClickTimeThreshold) {
                            if ((int) event.getX() - mPrevX < mDoubleClickPosThreshold
                                    && (int) event.getY() - mPrevY < mDoubleClickPosThreshold) {
                                mouseClickPress(Constant.MOUSE_BUTTON_LEFT);
                                mouseClickRelease(Constant.MOUSE_BUTTON_LEFT);
                            }
                        }

                    case MotionEvent.ACTION_MOVE:
                        //mouse move handle here.
                        int x = (int) (event.getX() * mSpeed);
                        int y = (int) (event.getY() * mSpeed);

                        mouseMove(x - mPrevX, y - mPrevY);

                        mPrevX = x;
                        mPrevY = y;
                        break;
                }

                return true;
            }
2. 鼠標右擊,左擊事件(通過button模擬)
// setup buttons
        ViewGroup bar = (ViewGroup) view.findViewById(R.id.buttons);
        int count = bar.getChildCount();

        for (int i = 0; i < count; i++) {
            View child = bar.getChildAt(i);

            try {
                Button button = (Button) child;
                button.setOnTouchListener(new OnTouchListener() {

                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        int which = Integer.valueOf((String) v.getTag());

                        switch (event.getAction()) {
                            case MotionEvent.ACTION_DOWN:
                                //mouse button pressed
                                //para which shows which button is pressed
                                //0 is left button
                                //1 is not used(reserved).
                                //2 is right button
                                if (which == 0) {
                                    mouseClickPress(Constant.MOUSE_BUTTON_LEFT);
                                } else if (which == 1) {
                                    //Do nothing for now.
                                } else if (which == 2) {
                                    mouseClickPress(Constant.MOUSE_BUTTON_RIGHT);
                                }
                                break;

                            case MotionEvent.ACTION_UP:
                                //mouse button released
                                if (which == 0) {
                                    mouseClickRelease(Constant.MOUSE_BUTTON_LEFT);
                                } else if (which == 1) {
                                    //Do nothing for now.
                                } else if (which == 2) {
                                    mouseClickRelease(Constant.MOUSE_BUTTON_RIGHT);
                                }
                                break;
                        }

                        return false;
                    }

                });
            } catch (ClassCastException e) {
                // not a button :)
            }
        }
3. 鼠標滾軸滾動事件
// setup scroll
        mScrollZone = view.findViewById(R.id.scrollzone);
        mScrollZone.setOnTouchListener(new OnTouchListener() {

            private int mPrevY;

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //click scroll handle here.
                        mPrevY = (int) (event.getY() * mScrollSpeed);
                        break;

                    case MotionEvent.ACTION_MOVE:
                        //mouse scroll handle here.
                        int amt = (int) (event.getY() * mScrollSpeed);

                        mouseScroll(mPrevY - amt);

                        mPrevY = amt;
                        break;
                }

                return true;
            }
        });
4. 設置鼠標移動,滾軸滾動速度。
設置移動速度
// setup speed controls
        bar = (ViewGroup) view.findViewById(R.id.speed_control);
        count = bar.getChildCount();

        for (int i = 0; i < count; i++) {
            View child = bar.getChildAt(i);

            try {
                ToggleButton button = (ToggleButton) child;
                button.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View v) {
                        ToggleButton button = (ToggleButton) v;

                        // do not allow to uncheck button
                        if (!button.isChecked()) {
                            button.setChecked(true);
                            return;
                        }

                        updateSpeed(Integer.parseInt((String) button.getTag()));
                    }

                });
            } catch (ClassCastException e) {
                // not a button :)
            }
        }

設置滾動速度

// setup scroll speed controls
        bar = (ViewGroup) view.findViewById(R.id.scroll_speed_control);
        count = bar.getChildCount();

        for (int i = 0; i < count; i++) {
            View child = bar.getChildAt(i);

            try {
                ToggleButton button = (ToggleButton) child;
                button.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View v) {
                        ToggleButton button = (ToggleButton) v;

                        // do not allow to uncheck button
                        if (!button.isChecked()) {
                            button.setChecked(true);
                            return;
                        }

                        updateScrollSpeed(Integer.parseInt((String) button.getTag()));
                    }

                });
            } catch (ClassCastException e) {
                // not a button :)
            }
        }

更新移動速度

private void updateSpeed(int newSpeed) {
        // note: we assume at least button have proper speed-tag so this will
        // check what it should

        mSpeed = newSpeed;

        ViewGroup bar = (ViewGroup) getView().findViewById(R.id.speed_control);
        int count = bar.getChildCount();

        for (int i = 0; i < count; i++) {
            View child = bar.getChildAt(i);

            try {
                ToggleButton button = (ToggleButton) child;

                int speed = Integer.parseInt((String) button.getTag());

                button.setChecked(speed == newSpeed);
            } catch (ClassCastException e) {
                // not a button :)
            }
        }
    }

更行滾動速度:

private void updateScrollSpeed(int newSpeed) {
        // note: we assume at least button have proper speed-tag so this will
        // check what it should

        mScrollSpeed = newSpeed;

        ViewGroup bar = (ViewGroup) getView().findViewById(R.id.scroll_speed_control);
        int count = bar.getChildCount();

        for (int i = 0; i < count; i++) {
            View child = bar.getChildAt(i);

            try {
                ToggleButton button = (ToggleButton) child;

                int speed = Integer.parseInt((String) button.getTag());

                button.setChecked(speed == newSpeed);
            } catch (ClassCastException e) {
                // not a button :)
            }
        }
    }

KeyboardFragment

這是鍵盤的fragment,這其實是一個FragmentActivity,具體的鍵盤通過ViewGroup添加view接口addView添加相應的view:

keyboard = (ViewGroup) this.findViewById(R.id.keyboard);
keyboard.addView(view);

這裡針對兩類鍵盤設計了兩個創建接口:
主鍵盤:

private View creatQwertyKeyboard(Context context) {
    return createKeyboard(context, R.xml.qwerty_keyboard);
}

從鍵盤:

private View createNavigationAndNumericKeyboard(Context context) {
    ViewGroup view = (ViewGroup) View.inflate(context, R.layout.numeric_keyboard, null);
    ViewGroup child;

    child = (ViewGroup) view.findViewById(R.id.navigation_keyboard);
    child.addView(createKeyboard(context, R.xml.navigation_keyboard));

    child = (ViewGroup) view.findViewById(R.id.numeric_keyboard);
    child.addView(createKeyboard(context, R.xml.numeric_keyboard));

    return view;
}

他們都使用了createKeyboard接口創建實際的鍵盤:

private Keyboard createKeyboard(Context context, int xmlResourceID) {

        final Keyboard keyboard = new Keyboard(context, xmlResourceID);
        keyboard.setKeyboardListener(new Keyboard.KeyboardListener() {
            @Override
            public void onKeyUp(int keyCode) {
                Log.d(TAG, "up keycode: " + keyCode);

                if (mDataSendHandler != null) {
                    mDataSendHandler.removeCallbacks(mLongPressCheckTask);
                }
                if (mIsLongPressed) {
                    mIsLongPressed = false;
                    WirelessHidProto.HidData data = WirelessHidProto.HidData.newBuilder()
                            .setType(WirelessHidProto.HidData.DataType.KEYBOARD_LONG_RELEASE)
                            .setKeyboardValue(keyCode).build();
                    if (mDataSendHandler != null) {
                        mDataSendHandler.obtainMessage(0, data).sendToTarget();
                    }
                } else {
                    WirelessHidProto.HidData data = WirelessHidProto.HidData.newBuilder()
                            .setType(WirelessHidProto.HidData.DataType.KEYBOARD_HIT)
                            .setKeyboardValue(keyCode).build();
                    if (mDataSendHandler != null) {
                        mDataSendHandler.obtainMessage(0, data).sendToTarget();
                    }
                }
            }

            @Override
            public void onKeyDown(int keyCode) {
                Log.d(TAG, "key down: " + keyCode);

                if (keyCode == 144) {
                    // 144 means number lock
                    mIsNumLockActive = !mIsNumLockActive;
                    KeyboardFragment.this.findViewById(R.id.led_numlock).
                            setBackgroundColor(getResources().getColor(mIsNumLockActive ? R.color.led_on : R.color.led_off));
                } else if (keyCode == 20) {
                    // 20 means caps lock.
                    mIsCapsLockActive = !mIsCapsLockActive;
                    KeyboardFragment.this.findViewById(R.id.led_capslock).
                            setBackgroundColor(getResources().getColor(mIsCapsLockActive ? R.color.led_on : R.color.led_off));
                } else if (keyCode == 145) {
                    // 145 means scroll lock
                    mIsScrollLockActive = !mIsScrollLockActive;
                    KeyboardFragment.this.findViewById(R.id.led_scrolllock).
                            setBackgroundColor(getResources().getColor(mIsScrollLockActive ? R.color.led_on : R.color.led_off));
                } else if (mDataSendHandler != null) {
                    mLongPressCheckTask.setKeyCode(keyCode);
                    mDataSendHandler.postDelayed(mLongPressCheckTask, 1000);
                }
            }
        });
        return keyboard;
    }

這裡返回一個Keyboard類對象,Keyboard類就是鍵盤實際的類了,這個類是LinearLayout的子類,使用XmlResourceParser來解析剛才我們定義的xml文件去獲得鍵值和創建布局。

private LinearLayout parseKeyLayout(Context context, XmlResourceParser xmlParser)
            throws XmlPullParserException, IOException {
        LinearLayout linearLayout = new LinearLayout(context);
        linearLayout.setLayoutParams(new LayoutParams(
                xmlParser.getAttributeIntValue(null, "width", LayoutParams.MATCH_PARENT),
                xmlParser.getAttributeIntValue(null, "height", 0),
                xmlParser.getAttributeFloatValue(null, "weight", 1.0f)));
        linearLayout.setOrientation(xmlParser.getAttributeIntValue(null, "orientation",
                LinearLayout.HORIZONTAL));

        String tag;
        do {
            xmlParser.next();
            tag = xmlParser.getName();

            if (xmlParser.getEventType() == XmlResourceParser.START_TAG) {
                if (tag.equals(XML_TAG_LAYOUT)) {
                    linearLayout.addView(parseKeyLayout(context, xmlParser));
                } else if (tag.equals(XML_TAG_KEY)) {
                    Key.KeyAttributes attrs = new Key.KeyAttributes();
                    attrs.keyFunction = getStringAttributeValue(xmlParser, "keyFunc", "");
                    attrs.mainLabel = getStringAttributeValue(xmlParser, "keyLabel", "");
                    attrs.shiftLabel = getStringAttributeValue(xmlParser, "shiftLabel", "");
                    attrs.keyCode = xmlParser.getAttributeIntValue(null, "keyCode", 0);

                    Key key = new Key(context, attrs);
                    key.setLayoutParams(new LayoutParams(
                            xmlParser.getAttributeIntValue(null, "width", 0),
                            xmlParser.getAttributeIntValue(null, "height",
                                    LayoutParams.MATCH_PARENT),
                            xmlParser.getAttributeFloatValue(null, "weight", 1)));
                    key.setVisibility(xmlParser.getAttributeBooleanValue(null, "visible", true) ?
                        VISIBLE : INVISIBLE);
                    key.setKeyListener(this);

                    if (attrs.shiftLabel != null & attrs.shiftLabel.length() > 0) {
                        mKeysWithShiftLabel.add(key);
                    }

                    linearLayout.addView(key);
                }
            }
        } while (xmlParser.getEventType() != XmlResourceParser.END_TAG
                || !tag.equals(XML_TAG_LAYOUT));

        return linearLayout;
    }

WirelessHid.proto

這是protobuf的數據定義文件,內容如下:

syntax = "proto2";

option java_package = "com.baniel.wirelesshid";
option java_outer_classname = "WirelessHidProto";

message HidData {
    enum DataType {
        MOUSE_MOVE = 0;
        MOUSE_CLICK_PRESS = 1;
        MOUSE_CLICK_RELEASE = 2;
        MOUSE_SCROLL = 3;
        KEYBOARD_LONG_PRESS = 4;
        KEYBOARD_LONG_RELEASE = 5;
        KEYBOARD_HIT = 6;
    }

    required DataType type = 1;
    optional int32 x_shift = 2;
    optional int32 y_shift = 3;
    optional int32 mouse_key_value = 4;
    optional int32 mouse_scroll = 5;
    optional int32 keyboard_value = 6;
}

這裡我只定義了一個消息類型,那就是HidData這是android需要發送給pc的消息數據。這其中有消息的類型:鼠標移動,鼠標按下,鼠標釋放,鼠標滾軸,鍵盤長按,鍵盤長按釋放,鍵盤單擊。x軸偏移,y軸偏移(pc系統的鼠標移動是以坐標偏移作為參數的;鼠標按鍵鍵值,鼠標滾動值,鍵盤按鍵值。關於protobuf詳細的數據定義語法請見:
https://developers.google.com/protocol-buffers/docs/proto
WirelessHidProto類就是上面這個文件通過protobuf編譯器編譯生成的。

client端

client端的代碼就比較簡單了,這裡我只有兩個類:
這裡寫圖片描述
具體類的說明如下:

WirelessHidClient

這個是client的主類,主要就是從socket讀取來自android的數據,然後通過java Robot類移動鼠標,操作鍵盤輸入操作:
主方法:

public static void main(String[] args) {
        final int HID_TCP_PORT = 34567;
        Socket mSocket = null;
        InputStream is = null;

        HidData data = null;
        try {
            mSocket = new Socket(args[0], HID_TCP_PORT);
            is = mSocket.getInputStream();
            mRobot = new Robot();
            printClientInfo(mSocket);
            while (true) {
                data = HidData.parseDelimitedFrom(is);
                if (data != null) {
                    handleData(data);
                } else {
                    break;
                }
            }
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.exit(-1);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.exit(-1);
        } catch (AWTException e) {
            // TODO: handle exception
            System.exit(-1);
        }

        System.out.println("Connection lost.");
    }

具體操作:

//move mouse pointer to posX,posY of current position.
    private static void doMouseMove(int posX, int posY) {
        mRobot.mouseMove(mPrevX + posX, mPrevY + posY);
        mPrevX += posX;
        mPrevY += posY;
    }

    //handle mouse button press.
    private static void doMousePress(int keyValue) {
        //System.out.println("mouse click value = " + keyValue);
        mRobot.mousePress(keyValue);
    }

    private static void doMouseRelease(int keyValue) {
        mRobot.mouseRelease(keyValue);
    }

    private static void doMouseScroll(int amt) {
        mRobot.mouseWheel(amt);
    }

    private static void doKeyHit(int keyCode) {
        mRobot.keyPress(keyCode);
        mRobot.keyRelease(keyCode);
    }

    private static void doKeyLongPress(int keyCode) {
        mRobot.keyPress(keyCode);
    }

    private static void doKeyLongRelease(int keyCode) {
        mRobot.keyRelease(keyCode);
    }

WirelessHidProto

這個類是protobuf編譯器生成的,主要包含數據類的序列化操作邏輯。

好了,到這裡就完全分析完了我的實現,感興趣的朋友可以從我的github下載編譯好的二進制文件,直接運行感受一下(提醒一下,客戶端最好在linux上運行,windows上有點卡頓,影響體驗,具體的原因以後我會找出並且解決這個問題!或者哪位大神知道可以告訴我哦~~)。下載地址:
https://github.com/CreateChance/WirelessHid/tree/master/bin
運行方式:
server端(android):
直接安裝app,然後啟動即可(前提是你要鏈接到一個局域的wifi,並且你的pc電腦能夠和android設備通訊)
client端(linux或者windows):
命令運行:java -jar WirelessHidClient.jar 你的android設備地址

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved