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模拟键盘