本篇为蓝牙HID系列篇章之一,本篇以红米K30(MIUI13即Android 12)手机作为蓝牙HID设备,可以与电脑、手机、平板等其他蓝牙主机进行配对从而实现鼠标触控板的功能。
蓝牙HID系列篇章:
蓝牙HID——将android设备变成蓝牙键盘(BluetoothHidDevice)
蓝牙HID——android利用手机来解锁电脑/平板/iPhone
蓝牙HID——Android手机注册HID时出现 Could not bind to Bluetooth (HID Device) Service with Intent * 的问题分析
Android 9开放了 BluetoothHidDevice
等HID相关的API,通过与系统蓝牙HID服务通信注册成蓝牙HID设备。首先通过 BluetoothProfile.HID_DEVICE
的描述类型得到 BluetoothHidDevice
抽象实例:
private BluetoothAdapter mBtAdapter;
private BluetoothHidDevice mHidDevice;
private void callBluetooth() {
Log.d(TAG, "callBluetooth");
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
mBtAdapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
Log.d(TAG, "onServiceConnected:" + i);
if (i == BluetoothProfile.HID_DEVICE) {
if (!(bluetoothProfile instanceof BluetoothHidDevice)) {
Log.e(TAG, "Proxy received but it's not BluetoothHidDevice");
return;
}
mHidDevice = (BluetoothHidDevice) bluetoothProfile;
registerBluetoothHid();
}
}
@Override
public void onServiceDisconnected(int i) {
Log.d(TAG, "onServiceDisconnected:" + i);
}
}, BluetoothProfile.HID_DEVICE);
}
再调用 BluetoothHidDevice.registerApp()
将 Android 设备注册成蓝牙HID设备:
private BluetoothDevice mHostDevice;
private final BluetoothHidDeviceAppQosSettings qosSettings
= new BluetoothHidDeviceAppQosSettings(BluetoothHidDeviceAppQosSettings.SERVICE_BEST_EFFORT,
800, 9, 0, 11250, BluetoothHidDeviceAppQosSettings.MAX
);
private final BluetoothHidDeviceAppSdpSettings mouseSdpSettings = new BluetoothHidDeviceAppSdpSettings(
HidConfig.MOUSE_NAME, HidConfig.DESCRIPTION, HidConfig.PROVIDER,
BluetoothHidDevice.SUBCLASS1_MOUSE, HidConfig.MOUSE_COMBO);
private void registerBluetoothHid() {
if (mHidDevice == null) {
Log.e(TAG, "hid device is null");
return;
}
mHidDevice.registerApp(mouseSdpSettings, null, qosSettings, Executors.newCachedThreadPool(), new BluetoothHidDevice.Callback() {
@Override
public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) {
Log.d(TAG, "onAppStatusChanged:" + (pluggedDevice != null ? pluggedDevice.getName() : "null") + " registered:" + registered);
if (registered) {
Log.d(TAG, "paired devices: " + mHidDevice.getConnectionState(pluggedDevice));
if (pluggedDevice != null && mHidDevice.getConnectionState(pluggedDevice) != BluetoothProfile.STATE_CONNECTED) {
boolean result = mHidDevice.connect(pluggedDevice);
Log.d(TAG, "hidDevice connect:" + result);
}
}
if (mBluetoothHidStateListener != null) {
mBluetoothHidStateListener.onRegisterStateChanged(registered, pluggedDevice != null);
}
}
@Override
public void onConnectionStateChanged(BluetoothDevice device, int state) {
Log.d(TAG, "onConnectionStateChanged:" + device + " state:" + state);
if (state == BluetoothProfile.STATE_CONNECTED) {
mHostDevice = device;
}
if (state == BluetoothProfile.STATE_DISCONNECTED) {
mHostDevice = null;
}
if (mBluetoothHidStateListener != null) {
mBluetoothHidStateListener.onConnectionStateChanged(state);
}
}
});
}
蓝牙鼠标Mouse的描述信息如下,主要 为 MOUSE_COMBO
的描述协议,正确的描述协议才能成功与其他设备通信。
public class HidConfig {
public final static String MOUSE_NAME = "VV Mouse";
public final static String DESCRIPTION = "VV for you";
public final static String PROVIDER = "VV";
public static final byte[] MOUSE_COMBO = {
(byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop)
(byte) 0x09, (byte) 0x02, // USAGE (Mouse)
(byte) 0xa1, (byte) 0x01, // COLLECTION (Application)
(byte) 0x85, (byte) 0x04, // REPORT_ID (4)
(byte) 0x09, (byte) 0x01, // USAGE (Pointer)
(byte) 0xa1, (byte) 0x00, // COLLECTION (Physical)
(byte) 0x05, (byte) 0x09, // USAGE_PAGE (Button)
(byte) 0x19, (byte) 0x01, // USAGE_MINIMUM (Button 1)
(byte) 0x29, (byte) 0x02, // USAGE_MAXIMUM (Button 2)
(byte) 0x15, (byte) 0x00, // LOGICAL_MINIMUM (0)
(byte) 0x25, (byte) 0x01, // LOGICAL_MAXIMUM (1)
(byte) 0x95, (byte) 0x03, // REPORT_COUNT (3)
(byte) 0x75, (byte) 0x01, // REPORT_SIZE (1)
(byte) 0x81, (byte) 0x02, // INPUT (Data,Var,Abs)
(byte) 0x95, (byte) 0x01, // REPORT_COUNT (1)
(byte) 0x75, (byte) 0x05, // REPORT_SIZE (5)
(byte) 0x81, (byte) 0x03, // INPUT (Cnst,Var,Abs)
(byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop)
(byte) 0x09, (byte) 0x30, // USAGE (X)
(byte) 0x09, (byte) 0x31, // USAGE (Y)
(byte) 0x09, (byte) 0x38, // USAGE (Wheel)
(byte) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127)
(byte) 0x25, (byte) 0x7F, // LOGICAL_MAXIMUM (127)
(byte) 0x75, (byte) 0x08, // REPORT_SIZE (8)
(byte) 0x95, (byte) 0x03, // REPORT_COUNT (3)
(byte) 0x81, (byte) 0x06, // INPUT (Data,Var,Rel)
//水平滚轮
(byte) 0x05, (byte) 0x0c, // USAGE_PAGE (Consumer Devices)
(byte) 0x0a, (byte) 0x38, (byte) 0x02, // USAGE (AC Pan)
(byte) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127)
(byte) 0x25, (byte) 0x7f, // LOGICAL_MAXIMUM (127)
(byte) 0x75, (byte) 0x08, // REPORT_SIZE (8)
(byte) 0x95, (byte) 0x01, // REPORT_COUNT (1)
(byte) 0x81, (byte) 0x06, // INPUT (Data,Var,Rel)
(byte) 0xc0, // END_COLLECTION
(byte) 0xc0, // END_COLLECTION
};
在注册完成后启动设备发现,让HID能被其他设备发现,下面ActivityResultLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
相当于调用 BluetoothAdapter.setScanMode()
的隐藏API
private ActivityResultLauncher<Intent> mActivityResultLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mouse);
mActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
Log.d(TAG, "onActivityResult:" + result.toString());
});
}
@Override
public void onRegisterStateChanged(boolean registered, boolean hasDevice) {
if (registered) {
if (!hasDevice) {
// startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), 1);
mActivityResultLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE));
}
}
}
ActivityResultLauncher
的相关方法也可用 startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), REQUEST_CODE)
来替代,但 startActivityForResult()
是废弃的方法,不建议使用。
接下来与蓝牙主机(电脑、手机等)进行蓝牙配对,已配对过需要取消配对。配对完成即可实现对蓝牙主机的鼠标触摸控制。
手势识别通过对触摸事件以及手势监听进行各种手势的判断(移动鼠标、左键单击、左键双击、右键双指单击、双指垂直/水平滚动)。
CustomMotionListener customMotionListener = new CustomMotionListener(this, mBluetoothHidManager);
findViewById(R.id.layout_touch).setOnTouchListener(customMotionListener);
手势逻辑处理代码如下:
package com.example.bluetoothproject;
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomMotionListener implements View.OnTouchListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
private final GestureDetector mGestureDetector;
private BluetoothHidManager mBluetoothHidManager;
private int mPointCount;
private long mDoubleFingerTime;
private final ScheduledExecutorService mExecutorService;
private float mPreX;
private float mPreY;
private boolean mLongPress;
public CustomMotionListener(Context context, BluetoothHidManager bluetoothHidManager) {
mBluetoothHidManager = bluetoothHidManager;
mGestureDetector = new GestureDetector(context, this);
mGestureDetector.setOnDoubleTapListener(this);
mExecutorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("mouse-schedule-pool-%d").daemon(true).build());
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
//左键单指双击(选中文本的效果)
if (e.getAction() == MotionEvent.ACTION_DOWN) {
mBluetoothHidManager.sendLeftClick(true);
} else if (e.getAction() == MotionEvent.ACTION_UP) {
mBluetoothHidManager.sendLeftClick(false);
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//左键单击
mBluetoothHidManager.sendLeftClick(true);
mBluetoothHidManager.sendLeftClick(false);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//双指滚动,x为水平滚动,y为垂直滚动,消抖处理
if (mPointCount == 2) {
if (Math.abs(distanceX) > Math.abs(distanceY)) {
distanceX = distanceX > 0 ? 1 : distanceX < 0 ? -1 : 0;
distanceY = 0;
} else {
distanceY = distanceY > 0 ? -1 : distanceY < 0 ? 1 : 0;
distanceX = 0;
}
mBluetoothHidManager.sendWheel((byte) (distanceX), (byte) (distanceY));
}
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//单键长按效果
mBluetoothHidManager.sendLeftClick(true);
mLongPress = true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (mGestureDetector.onTouchEvent(event)) {
return true;
}
mPointCount = event.getPointerCount();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:
//双指单击代表右键记录时间
if (event.getPointerCount() == 2) {
mDoubleFingerTime = System.currentTimeMillis();
}
break;
case MotionEvent.ACTION_MOVE:
//单指代表移动鼠标
if (event.getPointerCount() == 1) {
float dx = x - mPreX;
if (dx > 127) dx = 127;
if (dx < -128) dx = -128;
float dy = y - mPreY;
if (dy > 127) dy = 127;
if (dy < -128) dy = -128;
mBluetoothHidManager.senMouse((byte) dx, (byte) dy);
} else {
mBluetoothHidManager.senMouse((byte) 0, (byte) 0);
}
break;
case MotionEvent.ACTION_UP:
if (mLongPress) {
mBluetoothHidManager.sendLeftClick(false);
mLongPress = false;
}
break;
case MotionEvent.ACTION_POINTER_UP:
//双指按下代表右键
if (event.getPointerCount() == 2 && System.currentTimeMillis() - mDoubleFingerTime < ViewConfiguration.getDoubleTapTimeout()) {
mBluetoothHidManager.sendRightClick(true);
//延时释放避免无效
mExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
mBluetoothHidManager.sendRightClick(false);
}
}, 0, 50, TimeUnit.MILLISECONDS); }
break;
default:
break;
}
mPreX = x;
mPreY = y;
return true;
}
}
向蓝牙主机发送的鼠标触摸按键的报告如下:
private boolean mLeftClick;
private boolean mRightClick;
public void sendLeftClick(boolean click) {
mLeftClick = click;
senMouse((byte) 0x00, (byte) 0x00);
}
public void sendRightClick(boolean click) {
mRightClick = click;
senMouse((byte) 0x00, (byte) 0x00);
}
public void senMouse(byte dx, byte dy) {
if (mHidDevice == null) {
Log.e(TAG, "senMouse failed, hid device is null!");
return;
}
if (mHostDevice == null) {
Log.e(TAG, "senMouse failed, hid device is not connected!");
return;
}
byte[] bytes = new byte[5];
//bytes[0]字节:bit0: 1表示左键按下 0表示左键抬起 | bit1: 1表示右键按下 0表示右键抬起 | bit2: 1表示中键按下 | bit7~3:补充的常数,无意义,这里为0即可
bytes[0] = (byte) (bytes[0] | (mLeftClick ? 1 : 0));
bytes[0] = (byte) (bytes[0] | (mRightClick ? 1 : 0) << 1);
bytes[1] = dx;
bytes[2] = dy;
Log.d(TAG, "senMouse Left:" + mLeftClick+ ",Right:" + mRightClick + ",bytes: " + BluetoothUtils.bytesToHexString(bytes));
mHidDevice.sendReport(mHostDevice, 4, bytes);
}
public void sendWheel(byte hWheel, byte vWheel) {
if (mHidDevice == null) {
Log.e(TAG, "sendWheel failed, hid device is null!");
return;
}
if (mHostDevice == null) {
Log.e(TAG, "sendWheel failed, hid device is not connected!");
return;
}
byte[] bytes = new byte[5];
bytes[3] = vWheel; //垂直滚轮
bytes[4] = hWheel; //水平滚轮
Log.d(TAG, "sendWheel vWheel:" + vWheel + ",hWheel:" + hWheel);
mHidDevice.sendReport(mHostDevice, 4, bytes);
}
实现以上步骤即可将手机变成蓝牙鼠标/触控板,下面是实现的效果:
鼠标移动:
左键单击:
右键双指单击:
双指水平左右滚动:
双指垂直上下滚动:
完整视频效果展示:
蓝牙HID——将android设备变成蓝牙鼠标/触控板