文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-09-07 15:52

关注

前言

本篇为蓝牙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);    }

再调用 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设备变成蓝牙鼠标/触控板

来源地址:https://blog.csdn.net/CJohn1994/article/details/127867838

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯