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

SDIO_WiFi驱动学习之安卓WLAN架构介绍及调用流程分析 一、引言 上一篇博客大概介绍了MMC子系统和SDIO相关知识及架构,这一篇博客则大致分析安卓的WLAN架构及上层如何调用WLAN驱动的流程。 虽然我工作的开发环境是安卓系统,但由于我不是专业的安卓应用/框架开发工程师,所以对安卓层的一些代码并不是特别熟悉,只能大致梳理流程,不专业的地方请谅解。 本文部分内容摘抄自网络,若有侵权,请联系删除。 二、安卓WLAN架构 1、应用框架(Application framework) 在应用程序框架级别是应用程序代码,它使用各种android.net.. 阅读详情

2016.06.28更新
现在我将android端作为client,pc端作为server端。目的就是为了让android端能够更加灵活地链接pc端。主要修改如下:
1. 修改整体架构,将pc端作为server端,android端作为client端。 pc端一直在监听来自android端的消息,如果android端有链接请求则建立请求; 如果android端断开链接则pc端继续监听,直到有链接消息。
2. 增加服务自动发现机制,pc端只需要运行程序即可,android端也只需要 打开app,然后app会通过UDP的224.0.0.1地址进行组播查找服务,如果找到 服务,则向服务器发起链接。
3. 修改音量键对应的键值,音量下键对应方向键下键,音量上键对应方向键上键。
详细的代码可以查看我的开源项目主页:https://github.com/CreateChance/WirelessHid
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
偶然间突发奇想,想到能不能让我们的在我们的手机设备上滑动触摸屏进而控制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,它的布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="horizontal" >

        <LinearLayout
            android:id="@+id/speed_control"
            android:layout_width="65dip"
            android:layout_height="match_parent"
            android:orientation="vertical" >

            <ToggleButton
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:textOn="@string/speed_slow"
                android:textOff="@string/speed_slow"
                android:tag="1" />

            <ToggleButton
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:textOn="@string/speed_medium"
                android:textOff="@string/speed_medium"
                android:tag="3" />

            <ToggleButton
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:textOn="@string/speed_fast"
                android:textOff="@string/speed_fast"
                android:tag="5" />

        </LinearLayout>

        <View
            android:id="@+id/touchpad"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1" />

        <View
            android:id="@+id/scrollzone"
            android:layout_width="65dip"
            android:layout_height="match_parent"
            android:background="#2b2b2b" />

        <LinearLayout
            android:id="@+id/scroll_speed_control"
            android:layout_width="65dip"
            android:layout_height="match_parent"
            android:orientation="vertical" >

            <ToggleButton
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:textOn="@string/speed_slow"
                android:textOff="@string/speed_slow"
                android:tag="1" />

            <ToggleButton
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:textOn="@string/speed_medium"
                android:textOff="@string/speed_medium"
                android:tag="3" />

            <ToggleButton
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:textOn="@string/speed_fast"
                android:textOff="@string/speed_fast"
                android:tag="5" />

        </LinearLayout>

    </LinearLayout>

    <LinearLayout
        android:id="@+id/buttons"
        android:layout_width="match_parent"
        android:layout_height="65dip"
        android:orientation="horizontal" >

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:tag="0"
            android:text="left"
            android:id="@+id/left_button" />

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:tag="1" />

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:tag="2"
            android:id="@+id/right_button"
            android:text="right" />

    </LinearLayout>

</LinearLayout>

运行时的效果如下:
这里写图片描述
键盘的布局就比较复杂了,这部分也是一个fragment,整体布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <TextView
            android:id="@+id/led_numlock"
            android:text="@string/led_numlock"
            android:background="@color/led_off"
            style="@style/keyboard_led" />

        <TextView
            android:id="@+id/led_capslock"
            android:text="@string/led_capslock"
            android:background="@color/led_off"
            style="@style/keyboard_led" />

        <TextView
            android:id="@+id/led_scrolllock"
            android:text="@string/led_scrolllock"
            android:background="@color/led_off"
            style="@style/keyboard_led" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/keyboard"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >

    </LinearLayout>
</LinearLayout>

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

<Keyboard>
    <Layout>
        <Key keyLabel="Esc" keyCode="27"/>
        <Key keyLabel="F1" keyCode="112"/>
        <Key keyLabel="F2" keyCode="113"/>
        <Key keyLabel="F3" keyCode="114"/>
        <Key keyLabel="F4" keyCode="115"/>
        <Key keyLabel="F5" keyCode="116"/>
        <Key keyLabel="F6" keyCode="117"/>
        <Key keyLabel="F7" keyCode="118"/>
        <Key keyLabel="F8" keyCode="119"/>
        <Key keyLabel="F9" keyCode="120"/>
        <Key keyLabel="F10" keyCode="121"/>
        <Key keyLabel="F11" keyCode="122"/>
        <Key keyLabel="F12" keyCode="123"/>
        <Key keyLabel="Del" keyCode="127"/>
    </Layout>
    <Layout>
        <Key keyLabel="'" shiftLabel="~" keyCode="192"/>
        <Key keyLabel="1" shiftLabel="!" keyCode="49"/>
        <Key keyLabel="2" shiftLabel="\@" keyCode="50"/>
        <Key keyLabel="3" shiftLabel="\#" keyCode="51"/>
        <Key keyLabel="4" shiftLabel="$" keyCode="52"/>
        <Key keyLabel="5" shiftLabel="%" keyCode="53"/>
        <Key keyLabel="6" shiftLabel="^" keyCode="54"/>
        <Key keyLabel="7" shiftLabel="&amp;" keyCode="55"/>
        <Key keyLabel="8" shiftLabel="*" keyCode="56"/>
        <Key keyLabel="9" shiftLabel="(" keyCode="57"/>
        <Key keyLabel="0" shiftLabel=")" keyCode="48"/>
        <Key keyLabel="-" shiftLabel="_" keyCode="45"/>
        <Key keyLabel="=" shiftLabel="+" keyCode="61"/>
        <Key keyLabel="Backspace &#x2190;" keyCode="8" weight="1.5"/>
    </Layout>
    <Layout>
        <Key keyLabel="Tab &#x21B9;" keyCode="9" weight="1.5"/>
        <Key keyLabel="Q" keyCode="81"/>
        <Key keyLabel="W" keyCode="87"/>
        <Key keyLabel="E" keyCode="69"/>
        <Key keyLabel="R" keyCode="82"/>
        <Key keyLabel="T" keyCode="84"/>
        <Key keyLabel="Y" keyCode="89"/>
        <Key keyLabel="U" keyCode="85"/>
        <Key keyLabel="I" keyCode="73"/>
        <Key keyLabel="O" keyCode="79"/>
        <Key keyLabel="P" keyCode="80"/>
        <Key keyLabel="[" keyCode="91" shiftLabel="{"/>
        <Key keyLabel="]" keyCode="93" shiftLabel="}"/>
        <Key keyLabel="\\" keyCode="92" shiftLabel="|"/>
    </Layout>
    <Layout>
        <Key keyLabel="Caps Lock" keyCode="20" weight="1.5"/>
        <Key keyLabel="A" keyCode="65"/>
        <Key keyLabel="S" keyCode="83"/>
        <Key keyLabel="D" keyCode="68"/>
        <Key keyLabel="F" keyCode="70"/>
        <Key keyLabel="G" keyCode="71"/>
        <Key keyLabel="H" keyCode="72"/>
        <Key keyLabel="J" keyCode="74"/>
        <Key keyLabel="K" keyCode="75"/>
        <Key keyLabel="L" keyCode="76"/>
        <Key keyLabel=";" keyCode="59" shiftLabel=":"/>
        <Key keyLabel="'" keyCode="44" shiftLabel="&quot;"/>
        <Key keyLabel="Enter &#x21B5;" keyCode="10" weight="3.0"/>
    </Layout>
    <Layout>
        <Key keyLabel="Shift &#x21E7;" keyCode="16" keyFunc="Shift" weight="1.5"/>
        <Key keyLabel="Z" keyCode="90"/>
        <Key keyLabel="X" keyCode="88"/>
        <Key keyLabel="C" keyCode="67"/>
        <Key keyLabel="V" keyCode="86"/>
        <Key keyLabel="B" keyCode="66"/>
        <Key keyLabel="N" keyCode="78"/>
        <Key keyLabel="M" keyCode="77"/>
        <Key keyLabel="," keyCode="44" shiftLabel="&lt;"/>
        <Key keyLabel="." keyCode="46" shiftLabel="&gt;"/>
        <Key keyLabel="/" keyCode="47" shiftLabel="\?"/>
        <Key keyLabel="Shift &#x21E7;" keyCode="16" keyFunc="Shift" weight="1.5"/>
    </Layout>
    <Layout>
        <Key keyLabel="Ctrl" keyCode="17" weight="1.5"/>
        <Key keyLabel="Win" keyCode="524"/>
        <Key keyLabel="Alt" keyCode="18"/>
        <Key keyLabel=" " keyCode="32" weight="10.0"/>
        <Key keyLabel="Alt" keyCode="18"/>
        <Key keyLabel="Win" keyCode="524"/>
        <Key keyLabel="Menu" keyCode="93"/>
        <Key keyLabel="Ctrl" keyCode="17" weight="1.5"/>
    </Layout>
</Keyboard>

导航键部分:

<Keyboard>
    <Layout>
        <Key keyLabel="Print\nScreen" keyCode="154"/>
        <Key keyLabel="Scroll\nLock" keyCode="145"/>
        <Key keyLabel="Pause\nBreak" keyCode="19"/>
    </Layout>
    <Layout>
        <Key keyLabel="Insert" keyCode="155"/>
        <Key keyLabel="Home" keyCode="36"/>
        <Key keyLabel="Page Up" keyCode="33"/>
    </Layout>
    <Layout>
        <Key keyLabel="Delete" keyCode="127"/>
        <Key keyLabel="End" keyCode="35"/>
        <Key keyLabel="Page Down" keyCode="34"/>
    </Layout>
    <Layout>
        <Key visible="false"/>
        <Key keyLabel="&#x2191;" keyCode="38"/>
        <Key visible="false"/>
    </Layout>
    <Layout>
        <Key keyLabel="&#x2190;" keyCode="37"/>
        <Key keyLabel="&#x2193;" keyCode="40"/>
        <Key keyLabel="&#x2192;" keyCode="39"/>
    </Layout>
</Keyboard>

数字键部分:

<Keyboard>
    <Layout>
        <Key keyLabel="Num\nLock" keyCode="144"/>
        <Key keyLabel="/" keyCode="111"/>
        <Key keyLabel="*" keyCode="106"/>
        <Key keyLabel="-" keyCode="45"/>
    </Layout>
    <Layout>
        <Key keyLabel="7\nHome" keyCode="0103"/>
        <Key keyLabel="8 &#x2191;" keyCode="104"/>
        <Key keyLabel="9\nPgUp" keyCode="105"/>
        <Key keyLabel="+" keyCode="521"/>
    </Layout>
    <Layout>
        <Key keyLabel="4\n&#x2190;" keyCode="100"/>
        <Key keyLabel="5" keyCode="101"/>
        <Key keyLabel="6\n&#x2192;" keyCode="102"/>
        <Key visible="false"/>
    </Layout>
    <Layout>
        <Key keyLabel="1\nEnd" keyCode="97"/>
        <Key keyLabel="2 &#x2193;" keyCode="98"/>
        <Key keyLabel="3\nPgDn" keyCode="99"/>
        <Key visible="false"/>
    </Layout>
    <Layout>
        <Key keyLabel="0\nIns" keyCode="96" weight="2.0"/>
        <Key keyLabel=".\nDel" keyCode="110"/>
        <Key keyLabel="Enter" keyCode="10"/>
    </Layout>
</Keyboard>

这里的布局需要说明一下,这里我使用了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设备地址

蓝牙HID与Android的通信--多点触摸问题 从中可以看出,如果report中有usage为HID_DG_CONTACTID,即判断为多点触摸的设备,会加载多点触摸的hid相关驱动,也就是linux内核中drivers\hid\hid-multitouch.c代码,该代码对HID的多点触摸设备进行了相关的转换,以适应android系统上报正确的多点触摸input事件。在其代码中,也可以找到static const struct hid_device_id mt_devices[]结构体数组,里面定义了支持该驱动的设备ID和厂商ID。 阅读详情

相关推荐

别再花钱买触摸屏了!用旧安卓手机+蓝牙,5分钟自制无线副屏(TouchScreen HID协议实战)

本文教你如何利用旧安卓手机和蓝牙HID协议,零成本打造无线触摸副屏。通过开源工具TouchHID和简单配置,实现超低延迟的触摸输入,适用于绘图、编程和游戏等多种场景,提升工作效率。

weixin_30565199的博客 391

【亲测免费】 探索 WirelessHid:实现无线硬件输入设备模拟的神器

探索 WirelessHid:实现无线硬件输入设备模拟的神器 项目简介 在深入技术细节之前,让我们先了解一下 这个项目。WirelessHid 是一个开源工具,允许用户通过 Wi-Fi 或蓝牙将计算机作为无线 HID(人机交互设备)使用。这意味着你可以将你的电脑变成键盘、鼠标或其他输入设备,为智能电视、树莓派等不支持传统输入设备的平台提供灵活的控制方案。 技术解析 WirelessHid 的核心...

gitblog_00091的博客 777

ESP32-C3模拟蓝牙HID触摸屏,实现Android无线自动化控制

在Android自动化测试与群控场景中,ADB方案依赖USB连接和调试授权,存在稳定性差、易被检测等问题。蓝牙HID(Human Interface Device)协议为人机交互设备提供了标准化的数据交换方式,通过将ESP32-C3配置为蓝牙触摸屏,向Android设备发送绝对坐标触摸事件,即可实现系统级的真实触摸模拟。该技术无需ROOT、无需安装App,仅需蓝牙配对即可完成隔空控制,并能同时支持滑动、点击及多点触控手势。凭借ESP32-C3的低成本、低功耗和Wi-Fi/蓝牙共存特性,这一方案可灵活扩展为无

weixin_34212762的博客 297

Android9.0适配HID标准协议的USB触摸屏

问题:在项目中遇到客户需要使用第三方USB触摸屏,协议是标准的HID协议。此时USB触摸屏类似于鼠标、键盘等外设,直接接在电脑上触摸是正常的,但接在我们平台的主板端无法识别。 使用lsusb查看,可以看到设备PID/VID;使用getevent查看,没有相关的设备。说明系统并未识别到该触摸屏,随便插入鼠标、键盘都可以查看到。 分析:USB识别不到,需要在kernel下添加相关的HID配置,VID、PID需要配置进去,免驱的设备直接配置即可。 代码修改: 1、主要三个文件需要修改hid-multito

神仙猪的专栏 7185

告别模拟器!手把手教你用旧安卓手机打造无线副屏/绘图板(基于蓝牙HID TouchScreen)

本文教你如何将闲置安卓手机通过蓝牙HID TouchScreen协议改造成无线副屏或绘图板,无需额外成本即可提升生产力。详细介绍了软硬件需求、配置步骤及优化技巧,特别适合创意工作者和极客用户。

weixin_30645617的博客 410

利用Spacedesk将旧设备变无线副屏的完整指南

无线副屏技术通过软件将闲置设备转化为电脑的扩展显示器,显著提升多任务处理效率。其核心原理包括图像编码、输入反控和动态码率调节,其中Spacedesk方案因其跨平台支持和局域网直连特性成为优选。这项技术不仅实现零成本设备复用,还能灵活适应代码开发、设计预览等场景。实测显示,1080p分辨率下旧手机完全可胜任文档处理,而旗舰机型甚至支持4K输出。通过合理的性能调优和多屏管理,开发者能构建出高效的工作站配置。

weixin_30432579的博客 334

杰理之HID 含义【篇】

HID 可以构建在 IIC、USB、传统蓝牙、BLE、WiFi 等通信协议上。通信协议只是提供一个传输通道来传送 HID 数据包。请不要把 HID 和蓝牙 HID 混为一谈,不同的概念的。HID 协议是独立于通信协议的一套描述人机接口的协议。

u013820168的博客 486

应急办公神器:旧安卓手机秒变无线鼠标/触控板,支持Win/Mac/平板(手势操作指南)

本文详细介绍了如何将旧安卓手机变身为无线鼠标或触控板,支持Win/Mac/平板等多平台操作。通过蓝牙HID技术,实现低延迟控制,并提供了手势操作指南和优化技巧,帮助用户在应急办公场景中提升效率。

weixin_42553471的博客 446

usb hid调试工具_Android 11 的无线调试简直不要太好用

前言大家应该知道,之前写过一篇旧文:你知道 adb 的无线方式开发模式吗,不过今天想跟大家介绍一下 Android 11 的无线调试有了新的变化。旧文介绍的无线调试模式,第一次使用需要先连接 usb 线来完成手机跟电脑的无线调试,而且每次都得这样的重复操作(想象一下每天早上上班都得先通过 usb 线连接一次)。而 Google 工程师也是注意到了这种无线调试模式的鸡肋之处,所以已经更新有段时间的 ...

weixin_39648297的博客 1894

自研无线游戏手柄V1:从蓝牙HID到摇杆校准的完整实战

无线游戏手柄的体验瓶颈常在于延迟与摇杆漂移。理解蓝牙、2.4G私有协议与WiFi的延迟特性差异,是选型的第一步。基于ESP32的BLE HID可实现标准的Gamepad设备描述符,而摇杆ADC死区校准与中心点采样则是消除漂移的关键。当手柄连接安卓设备时,通过无线ADB桥接将按键映射为触摸事件,能大幅扩展兼容性。本文从硬件选型、固件开发到实测数据,完整记录了一台自研无线蓝牙手柄的诞生过程。

weixin_29196315的博客 77

手机变身万能遥控器:蓝牙HID协议下的跨设备控制方案

本文详细介绍了如何利用蓝牙HID协议将手机变身为万能遥控器,实现跨设备控制键盘、鼠标和翻页笔等功能。通过技术原理解析、跨平台兼容性测试及高阶应用场景拓展,帮助用户解决遥控器丢失或设备不兼容的痛点,提升办公和家居控制的效率。

weixin_30627341的博客 577

安卓自动化脚本 HID 硬件选购指南:蓝牙HID与OTG HID区别、成本与搭建教程

安卓 HID 自动化硬件方案全解析:HID(人机接口设备)原理、蓝牙 HID 与 OTG HID 的区别与对比(无线 vs 有线、稳定性、干扰)、ESP32 开发板与 HID 小主机的成本与选购(开发板几十元、小主机约百元、固件免费)、连接配置流程、能力边界(只能用图色/OCR/点击滑动)与适用场景。附硬件清单对比表与合规提示,帮你搭一套免无障碍、免 USB 调试的安卓自动化硬件方案。

weixin_40381013的博客 724

用ESP32-C3模拟蓝牙HID触控板,实现Android自动化手势操作

蓝牙HID协议是连接外设与移动设备的通用桥梁,其底层通过报告描述符定义输入行为,使得自定义触控设备成为可能。理解这一原理后,开发者可以借助低成本芯片实现精准的绝对坐标映射与手势模拟,摆脱传统ADB注入的限制。在Android自动化测试领域,这种方式不仅解决了USB线束缚和复杂手势难以模拟的痛点,还为外设模拟、辅助工具开发提供了灵活的技术路径。本文基于ESP32-C3与NimBLE-Arduino库,详细剖析了蓝牙HID触控模拟的实现方法,涵盖报告描述符设计、坐标映射、滑动与长按算法,以及真机联调中的稳定性优

weixin_34161083的博客 285

终极指南:如何用scrcpy实现安卓设备无线投屏控制

scrcpy是一款强大的开源安卓投屏工具,能够让你在电脑上显示并控制Android设备。这款免费的投屏软件通过USB或WiFi连接,提供低延迟、高质量的屏幕镜像体验,是开发者、游戏玩家和普通用户的完美选择。 ## 🔥 scrcpy安卓投屏的核心优势 scrcpy最大的魅力在于它的**简单高效**。无需root权限,无需安装任何客户端应用,只需要开启USB调试模式,就能立即享受大屏操作的乐趣!

gitblog_01092的博客 556

android HID协议描述介绍

android HID协议描述介绍,主要描述HID描述符,report等构造

SAP ABAP VL09 BAPI VL 216

ls_batch_item-fact_unit_denom = ls_lips_split-umvkn. "销售数量转换为 SKU 的值(分母)ls_batch_item-fact_unit_nom = ls_lips_split-umvkz. "销售数量转换成SKU的分子(因子)ls_batch_item-sales_unit_iso = ls_lips_split-meins. "被拆分DN基本单位。

ChampaignWolf的专栏 2299

5分钟掌握scrcpy:安卓设备投屏与控制的终极解决方案

你是否曾想过在电脑上流畅操作手机应用?或者需要在会议中展示手机界面给团队看?又或者想用电脑键盘快速回复手机消息?今天我要介绍的开源工具scrcpy,正是为解决这些问题而生。 scrcpy是一款功能强大的安卓设备投屏工具,它能将你的手机屏幕实时显示在电脑上,并支持使用电脑的键盘和鼠标进行完全控制。最棒的是,它不需要在手机上安装任何应用,也不需要root权限,连接简单,使用方便。 ## 为什么你需

gitblog_00001的博客 1063

STM32F407VET6与CH395Q串口通信实战:从零搭建以太网客户端(附完整代码)

本文详细介绍了基于STM32F407VET6微控制器与南京沁恒微CH395Q以太网芯片,通过串口通信从零搭建TCP客户端应用的完整实战过程。内容涵盖硬件连接、驱动层设计、基于FreeRTOS的任务实现以及核心调试技巧,并提供了完整的代码示例,帮助嵌入式开发者快速实现设备联网功能。

weixin_29179311的博客 413

android wifi 框架图,android wifi框架

---恢复内容开始---frameworks/base/services/java/com/android/server/wifi 中的ReadMe文件WifiService: Implements the IWifiManager 3rd party API. The API and the device state information (screen on/off, battery sta...

weixin_42641998的博客 735

北斗三号PPP-B2b信号实战指南:从接收机配置到精度验证全流程

本文提供了北斗三号PPP-B2b信号从硬件配置到数据处理的全流程实战指南。详细解析了信号特性、接收机关键参数设置,并以飞纳接收机和Mass软件为例,演示了数据解算与精度验证方法。通过覆盖中国多地区的联调测试案例,验证了PPP-B2b服务可实现分米级实时定位精度,为无网络地区高精度应用提供了可靠解决方案。

nn3456的博客 551

Ansys口腔正畸有限元分析:从几何处理到接触设定的全流程解析

本文详细解析了Ansys在口腔正畸有限元分析中的全流程应用,从几何处理到接触设定,帮助医生精准预测牙齿移动趋势。通过有限元建模技术,可同时观察牙齿-牙周膜-牙槽骨的力学响应,显著提升矫治效果预测准确率。文章还分享了实用技巧和参数设置建议,适用于正畸医生和工程师。

weixin_30897233的博客 348

别再花钱买触摸屏了!用旧安卓手机+蓝牙,5分钟自制无线副屏(保姆级教程)

本文提供了一种零成本改造闲置安卓手机为蓝牙触控副屏的保姆级教程。通过蓝牙HID协议,5分钟内即可将旧手机变成价值数百元的无线触摸屏,适合学生党、办公族和极客玩家。教程涵盖设备兼容性检查、必备工具安装、蓝牙HID协议原理及实战连接步骤,助你轻松实现多屏协作或智能家居控制。

weixin_30800987的博客 471

Openclaw错误分析与解决方案之:Agent couldn’t generate a response.

Openclaw错误分析与解决方案之:Agent couldn't generate a response. Note: some tool actions may have already been executed — please verify before retrying.

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 1315
上一篇: 面向对象的思想
下一篇: Android Support Library介绍
createchance
博客等级 码龄15年 156粉丝 90原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值