Android模拟蓝牙蓝牙键盘——适配Android和Windows
学校寒假有个程序设计比赛,我也一直想要去写一个安卓模拟的蓝牙键盘,这样无论到哪里,比如班班通和没有键盘的电脑设备,有手机就可以操作它,也比USB方便一些。忙活了一个寒假,也走了不少歪路,终于整成了,下面分享一些经验。
(学校的软件设计比赛已经交了终稿了,我的仓库开源在Gitee和GitHub,求求star:
Gitee:https://gitee.com/FengyunTHU/keyboard
GitHub:https://github.com/FengyunTHU/keyboardOFbluetooth)
自己在写代码的过程中也参考了很多CSDN博客,列举如下:
蓝牙HID——将android设备变成蓝牙键盘(BluetoothHidDevice)
仅通过蓝牙HID将安卓手机模拟成鼠标和键盘
使用旧手检做成蓝牙键盘
CSDN上大佬真的很多!
代码思路
①第一步是蓝牙HID的初始化
在安卓API28后开放了BluetoothHidDevice类,主要就是用它来完成。首先是注册HID服务:
mBtAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { Log.d(TAG, "onServiceConnected: " + profile); Toast.makeText(context, "Okk_connected_service", Toast.LENGTH_SHORT).show(); if (profile == BluetoothProfile.HID_DEVICE) { Log.d(TAG, "Proxy received but it isn't hid_OUT"); if (!(proxy instanceof BluetoothHidDevice)) { Log.e(TAG, "Proxy received but it isn't hid"); return; } Log.d(TAG,"Connecting HID…"); mHidDevice = (BluetoothHidDevice) proxy; Log.d(TAG, "proxyOK"); BluetoothHidDeviceAppSdpSettings Sdpsettings = new BluetoothHidDeviceAppSdpSettings( HidConfig.KEYBOARD_NAME, HidConfig.DESCRIPTION, HidConfig.PROVIDER, BluetoothHidDevice.SUBCLASS1_KEYBOARD, HidConfig.KEYBOARD_COMBO ); if (mHidDevice != null) { Toast.makeText(context, "OK for HID profile", Toast.LENGTH_SHORT).show(); Log.d(TAG, "HID_OK"); Log.d(TAG, "Get in register"); //getPermission(); // 创建一个BluetoothHidDeviceAppSdpSettings对象 if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling Log.d(TAG,"return before register"); String[] list = new String[] { Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT }; requestPermissions(activity,list,1); return; } BluetoothHidDeviceAppQosSettings inQos = new BluetoothHidDeviceAppQosSettings( BluetoothHidDeviceAppQosSettings.SERVICE_GUARANTEED, 200, 2, 200, 10000 /* 10 ms */, 10000 /* 10 ms */); BluetoothHidDeviceAppQosSettings outQos = new BluetoothHidDeviceAppQosSettings( BluetoothHidDeviceAppQosSettings.SERVICE_GUARANTEED, 900, 9, 900, 10000 /* 10 ms */, 10000 /* 10 ms */); mHidDevice.registerApp(Sdpsettings, null, null, Executors.newCachedThreadPool(), mCallback); // registerApp();// 注册 } else { Toast.makeText(context, "Disable for HID profile", Toast.LENGTH_SHORT).show(); } // 启用设备发现 // requestLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)); Log.d(TAG, "Discover"); } } @SuppressLint("MissingPermission") @Override public void onServiceDisconnected(int profile) {// 断开连接 if (profile == BluetoothProfile.HID_DEVICE) { Log.d(TAG, "Unexpected Disconnected: " + profile); mHidDevice = null; mHidDevice.unregisterApp(); } } }, BluetoothProfile.HID_DEVICE); } public final BluetoothHidDevice.Callback mCallback = new BluetoothHidDevice.Callback() { private final int[] mMatchingStates = new int[]{ BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED }; @Override public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) { Log.d(TAG, "ccccc_str"); if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling return; } Log.d(TAG, "onAppStatusChanged: " + (pluggedDevice != null ? pluggedDevice.getName() : "null") + "registered:" + registered); // Toast.makeText(context, "onAppStatusChanged", Toast.LENGTH_SHORT).show(); IsRegisted = registered; if (registered) { // 应用已注册 Log.d(TAG, "register OK!......."); // List matchingDevices = mHidDevice.getDevicesMatchingConnectionStates(mMatchingStates); // Log.d(TAG, "paired devices: " + matchingDevices + " " + mHidDevice.getConnectionState(pluggedDevice)); // Toast.makeText(context, "paired devices: " + matchingDevices + " " + mHidDevice.getConnectionState(pluggedDevice), Toast.LENGTH_SHORT).show(); // if (pluggedDevice != null && mHidDevice.getConnectionState(pluggedDevice) != BluetoothProfile.STATE_CONNECTED) { // boolean result = mHidDevice.connect(pluggedDevice);// pluggedDevice即为连接到模拟HID的设备 // Log.d(TAG, "hidDevice connect:" + result); // Toast.makeText(context, "hidDevice connect:" + result, Toast.LENGTH_SHORT).show(); // } else if (matchingDevices != null && matchingDevices.size() > 0) { // // 选择连接的设备 // mHostDevice = matchingDevices.get(0);// 获得第一个已经配对过的设备 // Toast.makeText(context, "device_is_ok: " + mHostDevice.getName() + mHostDevice.getAddress(), Toast.LENGTH_SHORT).show(); // } else { // // 注册成功未配对 // } } // } else { // // 应用未注册 // } } @Override public void onConnectionStateChanged(BluetoothDevice device, int state) { Log.d(TAG, "onConnectStateChanged:" + device + " state:" + state); // Toast.makeText(context, state, Toast.LENGTH_SHORT).show(); if (state == BluetoothProfile.STATE_CONNECTED) {// 已经连接了 connected = true; mHostDevice = device; if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling return; } Log.d(TAG,"hid state is connected"); Log.d(TAG,"-----------------------------------connected HID"); Log.d(TAG,device.getName().toString()); // Toast.makeText(context, "device_is_ok: " + mHostDevice.getName() + mHostDevice.getAddress(), Toast.LENGTH_SHORT).show(); } else if (state == BluetoothProfile.STATE_DISCONNECTED) { connected = false; Log.d(TAG,"hid state is disconnected"); // mHostDevice = null; // Toast.makeText(context, "device_is_null", Toast.LENGTH_SHORT).show(); } else if (state == BluetoothProfile.STATE_CONNECTING) { Log.d(TAG,"hid state is connecting"); } } };
在mBtAdapter.getProfileProxy()中注册,其中onServiceConnected()会在开始注册时调用,其中的mHidDevice.registerApp()就是注册采用的方法,提供的SdpSettings是最主要的的HID描述,其中定义一系列常量用于描述模拟的HID设备。
进行注册时会有一个回调mCallback,onAppStatusChanged()调用在注册成功,onConnectStateChanged()则是在蓝牙连接状态改变时调用,如连接上、断开、正在连接,其中的日志可以反应蓝牙连接的状态。
注意注册HID时,蓝牙必须处于打开状态。打开蓝牙的代码我暂未编写。
②发起蓝牙连接
在发起连接上,我试了从电脑端发起连接、从手机端发起连接,而且使用的都是点进系统蓝牙列表的方式,均无法建立稳定连接。
后来看到大佬文章,解决了这个问题,即使用代理连接:
@SuppressLint("MissingPermission") public void ConnectotherBluetooth() { mHostDevice = mBtAdapter.getRemoteDevice("B4:8C:9D:AD:9B:9A"); if (mHostDevice!=null) { Log.d(TAG,"Connected is OK"); Log.d(TAG,mHostDevice.getName()); } mHidDevice.connect(mHostDevice);// 代理连接 }
只要把mac地址改成所想要连接的蓝牙设备的mac即可。电脑可以采用cmd指令ipconfig /all,拉到最底即可;手机使用adb连接后,输入指令adb shell settings get secure bluetooth_address即可。当然也可以直接扫描,但我目前还未完成相关代码。
③发送报告
@JavascriptInterface @SuppressLint("MissingPermission") public void sendKey(String key) { byte b1 = 0; if (key.length() char keychar = key.charAt(0); if ((keychar=65)&&(keychar b1 = 2; } } if (keyMap.SHITBYTE.containsKey(key)) { b1 = 2; } Log.d(TAG,"pre_send: "+key); mHidDevice.sendReport(mHostDevice,8,new byte[]{ b1,0,keyMap.KEY2BYTE.get(key.toUpperCase()),0,0,0,0,0 }); mHidDevice.sendReport(mHostDevice,8,new byte[]{ 0,0,0,0,0,0,0,0 });// 这是松开按键的报告 Log.d(TAG,"after_send: "+key); } public final static String KEYBOARD_NAME = "My Keyboard"; public final static String DESCRIPTION = "KKKey"; public final static String PROVIDER = "Alphabet"; public final static byte ID_KEYBOARD = 1; // HID码表【不知道干啥的】 public static final byte[] KEYBOARD_COMBO = { (byte) 0x05, (byte) 0x01, // Usage Page (Generic Desktop) (byte) 0x09, (byte) 0x06, // Usage (Keyboard) (byte) 0xA1, (byte) 0x01, // Collection (Application) (byte) 0x85, (byte) 0x08, // REPORT_ID (Keyboard) (byte) 0x05, (byte) 0x07, // Usage Page (Key Codes) (byte) 0x19, (byte) 0xE0, // Usage Minimum (224) (byte) 0x29, (byte) 0xE7, // Usage Maximum (231) (byte) 0x15, (byte) 0x00, // Logical Minimum (0) (byte) 0x25, (byte) 0x01, // Logical Maximum (1) (byte) 0x75, (byte) 0x01, // Report Size (1) (byte) 0x95, (byte) 0x08, // Report Count (8) (byte) 0x81, (byte) 0x02, // Input (Data, Variable, Absolute) (byte) 0x95, (byte) 0x01, // Report Count (1) (byte) 0x75, (byte) 0x08, // Report Size (8) (byte) 0x81, (byte) 0x01, // Input (Constant) reserved byte(1) (byte) 0x95, (byte) 0x05, // Report Count (5) (byte) 0x75, (byte) 0x01, // Report Size (1) (byte) 0x05, (byte) 0x08, // Usage Page (Page# for LEDs) (byte) 0x19, (byte) 0x01, // Usage Minimum (1) (byte) 0x29, (byte) 0x05, // Usage Maximum (5) (byte) 0x91, (byte) 0x02, // Output (Data, Variable, Absolute), Led report (byte) 0x95, (byte) 0x01, // Report Count (1) (byte) 0x75, (byte) 0x03, // Report Size (3) (byte) 0x91, (byte) 0x01, // Output (Data, Variable, Absolute), Led report padding (byte) 0x95, (byte) 0x06, // Report Count (6) (byte) 0x75, (byte) 0x08, // Report Size (8) (byte) 0x15, (byte) 0x00, // Logical Minimum (0) (byte) 0x25, (byte) 0x65, // Logical Maximum (101) (byte) 0x05, (byte) 0x07, // Usage Page (Key codes) (byte) 0x19, (byte) 0x00, // Usage Minimum (0) (byte) 0x29, (byte) 0x65, // Usage Maximum (101) (byte) 0x81, (byte) 0x00, // Input (Data, Array) Key array(6 bytes) (byte) 0xC0 // End Collection (Application) }; } 0,0,0,0,0,0,0,0 });// 这是松开按键的报告