Android基于wifi的无线HID设备实现

    本文地址:http://tongxinmao.com/Article/Detail/id/402

    偶然间突发奇想,想到能不能让我们的在我们的手机设备上滑动触摸屏进而控制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,它的布局如下:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <code class="hljs xml"><linearlayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical" xmlns:android="https://schemas.android.com/apk/res/android" xmlns:tools="https://schemas.android.com/tools">
     
        <linearlayout android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent" android:orientation="horizontal">
     
            <linearlayout android:id="@+id/speed_control" android:layout_height="match_parent" android:layout_width="65dip" android:orientation="vertical">
     
                <togglebutton android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent" android:tag="1" android:textoff="@string/speed_slow" android:texton="@string/speed_slow">
     
                <togglebutton android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent" android:tag="3" android:textoff="@string/speed_medium" android:texton="@string/speed_medium">
     
                <togglebutton android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent" android:tag="5" android:textoff="@string/speed_fast" android:texton="@string/speed_fast">
     
            </togglebutton></togglebutton></togglebutton></linearlayout>
     
            <view android:id="@+id/touchpad" android:layout_height="match_parent" android:layout_weight="1" android:layout_width="0dp">
     
            <view android:background="#2b2b2b" android:id="@+id/scrollzone" android:layout_height="match_parent" android:layout_width="65dip">
     
            <linearlayout android:id="@+id/scroll_speed_control" android:layout_height="match_parent" android:layout_width="65dip" android:orientation="vertical">
     
                <togglebutton android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent" android:tag="1" android:textoff="@string/speed_slow" android:texton="@string/speed_slow">
     
                <togglebutton android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent" android:tag="3" android:textoff="@string/speed_medium" android:texton="@string/speed_medium">
     
                <togglebutton android:layout_height="0dp" android:layout_weight="1" android:layout_width="match_parent" android:tag="5" android:textoff="@string/speed_fast" android:texton="@string/speed_fast">
     
            </togglebutton></togglebutton></togglebutton></linearlayout>
     
        </view></view></linearlayout>
     
        <linearlayout android:id="@+id/buttons" android:layout_height="65dip" android:layout_width="match_parent" android:orientation="horizontal"><button android:id="@+id/left_button" android:layout_height="match_parent" android:layout_weight="1" android:layout_width="0dp" android:tag="0" android:text="left"></button><button android:layout_height="match_parent" android:layout_weight="1" android:layout_width="0dp" android:tag="1"></button></linearlayout></linearlayout></code><button android:id="@+id/right_button" android:layout_height="match_parent" android:layout_weight="1" android:layout_width="0dp" android:tag="2" android:text="right"><code class="hljs xml">
     
         
     
    </code></button>

    运行时的效果如下:
    <img alt="这里写图片描述" src="https://www.2cto.com/uploadfile/Collfiles/20160614/20160614140910113.png" title="" kf="" ware="" vc="" "="" target="_blank" class="keylink" style="border-width: 0px; padding: 0px; margin: 0px auto; list-style: none; display: block; max-width: 630px; cursor: pointer; width: 630px; height: 354.375px;">vc+4tNTTwcujrNXisr+31tKyysfSu7j2ZnJhZ21lbnSjrNX7zOWyvL7WyOfPwqO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;">

    这个布局中首先放上3个textview在一个LinearLayout这三个textview充当真实键盘上的3个led灯:num lock, caps lock, scroll lock。然后就是一个存放实际keyboard布局的LinearLayout容器,这么做的目的是这样的:因为手机的屏幕很小,想要放下一个标准键盘上的所有的按键肯定是不行的,因此需要将键盘分区,然后分别展示,这里的这个容器就是用来存放不同分区,不同布局的键盘部分。目前我把键盘分成了2个部分:主键盘部分,导航部分加上数字部分。其中主键盘部分是我们最常使用的部分,这部分包含了26个字母,0~9数字(字母上排),12个功能键等,导航部分就是上下左右键盘,上页下页部分等,数字部分就是数字小键盘和一些控制按键,我把导航键和数字键合并在一起了,这两部分的布局如下:
    主键盘部分:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    <code class="hljs xml"><keyboard>
        <layout>
            <key keycode="27" keylabel="Esc">
            <key keycode="112" keylabel="F1">
            <key keycode="113" keylabel="F2">
            <key keycode="114" keylabel="F3">
            <key keycode="115" keylabel="F4">
            <key keycode="116" keylabel="F5">
            <key keycode="117" keylabel="F6">
            <key keycode="118" keylabel="F7">
            <key keycode="119" keylabel="F8">
            <key keycode="120" keylabel="F9">
            <key keycode="121" keylabel="F10">
            <key keycode="122" keylabel="F11">
            <key keycode="123" keylabel="F12">
            <key keycode="127" keylabel="Del">
        </key></key></key></key></key></key></key></key></key></key></key></key></key></key></layout>
        <layout>
            <key keycode="192" keylabel="'" shiftlabel="~">
            <key keycode="49" keylabel="1" shiftlabel="!">
            <key keycode="50" keylabel="2" shiftlabel="\@">
            <key keycode="51" keylabel="3" shiftlabel="\#">
            <key keycode="52" keylabel="4" shiftlabel="$">
            <key keycode="53" keylabel="5" shiftlabel="%">
            <key keycode="54" keylabel="6" shiftlabel="^">
            <key keycode="55" keylabel="7" shiftlabel="&">
            <key keycode="56" keylabel="8" shiftlabel="*">
            <key keycode="57" keylabel="9" shiftlabel="(">
            <key keycode="48" keylabel="0" shiftlabel=")">
            <key keycode="45" keylabel="-" shiftlabel="_">
            <key keycode="61" keylabel="=" shiftlabel="+">
            <key keycode="8" keylabel="Backspace ←" weight="1.5">
        </key></key></key></key></key></key></key></key></key></key></key></key></key></key></layout>
        <layout>
            <key keycode="9" keylabel="Tab ↹" weight="1.5">
            <key keycode="81" keylabel="Q">
            <key keycode="87" keylabel="W">
            <key keycode="69" keylabel="E">
            <key keycode="82" keylabel="R">
            <key keycode="84" keylabel="T">
            <key keycode="89" keylabel="Y">
            <key keycode="85" keylabel="U">
            <key keycode="73" keylabel="I">
            <key keycode="79" keylabel="O">
            <key keycode="80" keylabel="P">
            <key keycode="91" keylabel="[" shiftlabel="{">
            <key keycode="93" keylabel="]" shiftlabel="}">
            <key keycode="92" keylabel="\\" shiftlabel="|">
        </key></key></key></key></key></key></key></key></key></key></key></key></key></key></layout>
        <layout>
            <key keycode="20" keylabel="Caps Lock" weight="1.5">
            <key keycode="65" keylabel="A">
            <key keycode="83" keylabel="S">
            <key keycode="68" keylabel="D">
            <key keycode="70" keylabel="F">
            <key keycode="71" keylabel="G">
            <key keycode="72" keylabel="H">
            <key keycode="74" keylabel="J">
            <key keycode="75" keylabel="K">
            <key keycode="76" keylabel="L">
            <key keycode="59" keylabel=";" shiftlabel=":">
            <key keycode="44" keylabel="'" shiftlabel=""">
            <key keycode="10" keylabel="Enter ↵" weight="3.0">
        </key></key></key></key></key></key></key></key></key></key></key></key></key></layout>
        <layout>
            <key keycode="16" keyfunc="Shift" keylabel="Shift ⇧" weight="1.5">
            <key keycode="90" keylabel="Z">
            <key keycode="88" keylabel="X">
            <key keycode="67" keylabel="C">
            <key keycode="86" keylabel="V">
            <key keycode="66" keylabel="B">
            <key keycode="78" keylabel="N">
            <key keycode="77" keylabel="M">
            <key keycode="44" keylabel="," shiftlabel="<">
            <key keycode="46" keylabel="." shiftlabel=">">
            <key keycode="47" keylabel="/" shiftlabel="\?">
            <key keycode="16" keyfunc="Shift" keylabel="Shift ⇧" weight="1.5">
        </key></key></key></key></key></key></key></key></key></key></key></key></layout>
        <layout>
            <key keycode="17" keylabel="Ctrl" weight="1.5">
            <key keycode="524" keylabel="Win">
            <key keycode="18" keylabel="Alt">
            <key keycode="32" keylabel=" " weight="10.0">
            <key keycode="18" keylabel="Alt">
            <key keycode="524" keylabel="Win">
            <key keycode="93" keylabel="Menu">
            <key keycode="17" keylabel="Ctrl" weight="1.5">
        </key></key></key></key></key></key></key></key></layout>
    </keyboard></code>

    导航键部分:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <code class="hljs xml"><keyboard>
        <layout>
            <key keycode="154" keylabel="Print\nScreen">
            <key keycode="145" keylabel="Scroll\nLock">
            <key keycode="19" keylabel="Pause\nBreak">
        </key></key></key></layout>
        <layout>
            <key keycode="155" keylabel="Insert">
            <key keycode="36" keylabel="Home">
            <key keycode="33" keylabel="Page Up">
        </key></key></key></layout>
        <layout>
            <key keycode="127" keylabel="Delete">
            <key keycode="35" keylabel="End">
            <key keycode="34" keylabel="Page Down">
        </key></key></key></layout>
        <layout>
            <key visible="false">
            <key keycode="38" keylabel="↑">
            <key visible="false">
        </key></key></key></layout>
        <layout>
            <key keycode="37" keylabel="←">
            <key keycode="40" keylabel="↓">
            <key keycode="39" keylabel="→">
        </key></key></key></layout>
    </keyboard></code>

    数字键部分:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <code class="hljs xml"><keyboard>
        <layout>
            <key keycode="144" keylabel="Num\nLock">
            <key keycode="111" keylabel="/">
            <key keycode="106" keylabel="*">
            <key keycode="45" keylabel="-">
        </key></key></key></key></layout>
        <layout>
            <key keycode="0103" keylabel="7\nHome">
            <key keycode="104" keylabel="8 ↑">
            <key keycode="105" keylabel="9\nPgUp">
            <key keycode="521" keylabel="+">
        </key></key></key></key></layout>
        <layout>
            <key keycode="100" keylabel="4\n←">
            <key keycode="101" keylabel="5">
            <key keycode="102" keylabel="6\n→">
            <key visible="false">
        </key></key></key></key></layout>
        <layout>
            <key keycode="97" keylabel="1\nEnd">
            <key keycode="98" keylabel="2 ↓">
            <key keycode="99" keylabel="3\nPgDn">
            <key visible="false">
        </key></key></key></key></layout>
        <layout>
            <key keycode="96" keylabel="0\nIns" weight="2.0">
            <key keycode="110" keylabel=".\nDel">
            <key keycode="10" keylabel="Enter">
        </key></key></key></layout>
    </keyboard></code>

    这里的布局需要说明一下,这里我使用了layout标签表明,然后使用XmlResourceParser类来解析这个里面的内容,最后再添加到布局中去。下面贴出两张键盘的运行效果图:
    主键盘:
    这里写图片描述
    从键盘(导航键和数字键):
    这里写图片描述

    代码设计

    Server端

    Server整体代码就是一个app,内容不是很复杂,这里我只陈述我的代码功能和必要的代码片段,详细代码内容有限于篇幅就不贴出来了,可以查看我的github项目主页(https://github.com/CreateChance/WirelessHid)上的开源代码。
    代码的基本分布如下:
    这里写图片描述
    各个类的作用如下:

    MainActivity

    这是主界面类,基本就是MouseFragment的容器,另外就是监听用户点击回退事件,如果用户在1.5s之内连续点击两次回退就退出app,基本逻辑比较简单。

    WirelessHidService

    这是整个app的服务,这个服务是实际将数据发送出去的地方,主要就是通过looper和handler的方式将消息队列中的数据发送出去。发送部分的逻辑:
    一个looper线程

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    <code class="hljs java">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();
            }
        }</code>

    界面Fragment使用Handler和server交互,fragment需要实现server的DataHandlerListener接口,当Handler变化的时候通知Fragment,以便Fragment拿到最新的对象引用:
    需要实现的接口

    ?

    1
    2
    3
    <code class="hljs cs">public interface DataHandlerListener {
            void onHandlerChanged(Handler handler);
    }</code>

    设置listener的接口:

    ?

    1
    2
    3
    <code class="hljs cs">public void setListener(DataHandlerListener listener) {
            this.mListener = listener;
    }</code>

    fragment也可以主动获取:

    ?

    1
    2
    3
    <code class="hljs cs">public Handler getDataSendHandler() {
        return this.mDataSendHandler;
    }</code>

    这里的发送使用的就是protobuf的序列化接口,关于这个接口的描述这里就不详述,可以参考google protobuf的java部分的编程指导:
    https://developers.google.com/protocol-buffers/docs/javatutorial

    MouseFragment

    这个类是fragment类,主要是嵌套在MainActivity类中,主要逻辑功能如下:
    1. 捕获用户触摸屏移动,点击事件

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <code class="hljs cs">@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;
                }</code>

    ?

    1
    2
    <code>2. 鼠标右击,左击事件(通过button模拟)
    </code>

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    <code class="hljs cs">// 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 :)
                }
            }</code>

    ?

    1
    2
    <code>3. 鼠标滚轴滚动事件
    </code>

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <code class="hljs cs">// 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;
                }
            });</code>

    ?

    1
    2
    <code>4. 设置鼠标移动,滚轴滚动速度。
    </code>

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <code class="hljs java">设置移动速度
    // 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 :)
                }
            }</code>

    设置滚动速度

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <code class="hljs java">// 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 :)
                }
            }</code>

    更新移动速度

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <code class="hljs cs">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 :)
                }
            }
        }</code>

    更行滚动速度:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <code class="hljs cs">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 :)
                }
            }
        }</code>

    KeyboardFragment

    这是键盘的fragment,这其实是一个FragmentActivity,具体的键盘通过ViewGroup添加view接口addView添加相应的view:

    ?

    1
    2
    <code class="hljs avrasm">keyboard = (ViewGroup) this.findViewById(R.id.keyboard);
    keyboard.addView(view);</code>

    这里针对两类键盘设计了两个创建接口:
    主键盘:

    ?

    1
    2
    3
    <code class="hljs cs">private View creatQwertyKeyboard(Context context) {
        return createKeyboard(context, R.xml.qwerty_keyboard);
    }</code>

    从键盘:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <code class="hljs avrasm">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;
    }</code>

    他们都使用了createKeyboard接口创建实际的键盘:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    <code class="hljs avrasm">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;
        }</code>

    这里返回一个Keyboard类对象,Keyboard类就是键盘实际的类了,这个类是LinearLayout的子类,使用XmlResourceParser来解析刚才我们定义的xml文件去获得键值和创建布局。

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    <code class="hljs avrasm">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;
        }</code>

    WirelessHid.proto

    这是protobuf的数据定义文件,内容如下:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <code class="hljs ocaml">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;
    }</code>

    这里我只定义了一个消息类型,那就是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类移动鼠标,操作键盘输入操作:
    主方法:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <code class="hljs cs">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.");
        }</code>

    具体操作:

    ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <code class="hljs cs">//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);
        }</code>

    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设备地址


    上一篇:USBHID设备报告描述符详解
    下一篇:android ADB模拟键盘