• 蓝牙HID——将android设备变成蓝牙鼠标/触控板(BluetoothHidDevice)


    前言

    本篇为蓝牙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 * 的问题分析

    HID开发

    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);
        }
    
    • 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

    再调用 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);
                    }
                }
            });
        }
    
    • 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

    蓝牙鼠标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
        };
    
    • 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

    在注册完成后启动设备发现,让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));
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    ActivityResultLauncher 的相关方法也可用 startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), REQUEST_CODE) 来替代,但 startActivityForResult() 是废弃的方法,不建议使用。
    接下来与蓝牙主机(电脑、手机等)进行蓝牙配对,已配对过需要取消配对。配对完成即可实现对蓝牙主机的鼠标触摸控制。

    手势识别

    手势识别通过对触摸事件以及手势监听进行各种手势的判断(移动鼠标、左键单击、左键双击、右键双指单击、双指垂直/水平滚动)。

    CustomMotionListener customMotionListener = new CustomMotionListener(this, mBluetoothHidManager);
    findViewById(R.id.layout_touch).setOnTouchListener(customMotionListener);
    
    • 1
    • 2

    手势逻辑处理代码如下:

    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;
        }
    }
    
    
    • 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
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162

    向蓝牙主机发送的鼠标触摸按键的报告如下:

        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);
        }
    
    • 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

    效果

    实现以上步骤即可将手机变成蓝牙鼠标/触控板,下面是实现的效果:
    在这里插入图片描述

    鼠标移动:
    请添加图片描述

    左键单击:
    请添加图片描述

    左键单指快速双击:
    请添加图片描述

    右键双指单击:
    请添加图片描述

    双指水平左右滚动:
    请添加图片描述

    双指垂直上下滚动:
    请添加图片描述

    完整视频效果展示:

    蓝牙HID——将android设备变成蓝牙鼠标/触控板

  • 相关阅读:
    云安全(1)--初识容器逃逸之特权容器逃逸
    uniapp开发h5或小程序调用摄像头拍照,录屏
    【全民Python】PIP模块的安装,Pyinstaller模块安装,导出exe文件
    C#实现的曲线方法
    北京公司车牌指标转让注意事项
    【MATLAB教程案例13】基于SA模拟退火优化算法的函数极值计算matlab仿真及其他应用
    Git中Branch(分支)和Tag(标签)的区别
    【分布式技术专题】「架构实践于案例分析」总结和盘点目前常用分布式技术特别及问题分析
    SpringCloud源码学习笔记3——Nacos服务注册源码分析
    【附源码】计算机毕业设计JAVA计算机系教师教研科研管理系统
  • 原文地址:https://blog.csdn.net/CJohn1994/article/details/127867838