解决方案

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

seo靠我 2023-09-25 05:43:52

前言

本篇为蓝牙HID系列篇章之一,本篇以红米K30(MIUI13即Android 12)手机作为蓝牙HID设备,可以与电脑、手机、平板等其他蓝牙主机进行配对从而实现鼠标触控板的功能。

蓝牙HID系列篇章SEO靠我

蓝牙HID——将android设备变成蓝牙键盘(BluetoothHidDevice)

蓝牙HID——android利用手机来解锁电脑/平板/iPhone

蓝牙HID——Android手机注册HID时出SEO靠我现 Could not bind to Bluetooth (HID Device) Service with Intent * 的问题分析

HID开发

Android 9开放了 BluetoothHidSEO靠我Device 等HID相关的API,通过与系统蓝牙HID服务通信注册成蓝牙HID设备。首先通过 BluetoothProfile.HID_DEVICE 的描述类型得到 BluetoothHidDeviSEO靠我ce 抽象实例:

private BluetoothAdapter mBtAdapter;private BluetoothHidDevice mHidDevice;private void callBSEO靠我luetooth() {Log.d(TAG, "callBluetooth");mBtAdapter = BluetoothAdapter.getDefaultAdapter();mBtAdapterSEO靠我.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() {@Overridepublic void onServiceConSEO靠我nected(int i, BluetoothProfile bluetoothProfile) {Log.d(TAG, "onServiceConnected:" + i);if (i == BluSEO靠我etoothProfile.HID_DEVICE) {if (!(bluetoothProfile instanceof BluetoothHidDevice)) {Log.e(TAG, "ProxySEO靠我 received but its not BluetoothHidDevice");return;}mHidDevice = (BluetoothHidDevice) bluetoothProfilSEO靠我e;registerBluetoothHid();}}@Overridepublic void onServiceDisconnected(int i) {Log.d(TAG, "onServiceDSEO靠我isconnected:" + i);}}, BluetoothProfile.HID_DEVICE);}

再调用 BluetoothHidDevice.registerApp() 将 Android SEO靠我设备注册成蓝牙HID设备:

private BluetoothDevice mHostDevice;private final BluetoothHidDeviceAppQosSettings qosSSEO靠我ettings= new BluetoothHidDeviceAppQosSettings(BluetoothHidDeviceAppQosSettings.SERVICE_BEST_EFFORT,8SEO靠我00, 9, 0, 11250, BluetoothHidDeviceAppQosSettings.MAX);private final BluetoothHidDeviceAppSdpSettingSEO靠我s mouseSdpSettings = new BluetoothHidDeviceAppSdpSettings(HidConfig.MOUSE_NAME, HidConfig.DESCRIPTIOSEO靠我N, HidConfig.PROVIDER,BluetoothHidDevice.SUBCLASS1_MOUSE, HidConfig.MOUSE_COMBO);private void registSEO靠我erBluetoothHid() {if (mHidDevice == null) {Log.e(TAG, "hid device is null");return;}mHidDevice.regisSEO靠我terApp(mouseSdpSettings, null, qosSettings, Executors.newCachedThreadPool(), new BluetoothHidDevice.SEO靠我Callback() {@Overridepublic void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registereSEO靠我d) {Log.d(TAG, "onAppStatusChanged:" + (pluggedDevice != null ? pluggedDevice.getName() : "null") + SEO靠我" registered:" + registered);if (registered) {Log.d(TAG, "paired devices: " + mHidDevice.getConnectiSEO靠我onState(pluggedDevice));if (pluggedDevice != null && mHidDevice.getConnectionState(pluggedDevice) !=SEO靠我 BluetoothProfile.STATE_CONNECTED) {boolean result = mHidDevice.connect(pluggedDevice);Log.d(TAG, "hSEO靠我idDevice connect:" + result);}}if (mBluetoothHidStateListener != null) {mBluetoothHidStateListener.oSEO靠我nRegisterStateChanged(registered, pluggedDevice != null);}}@Overridepublic void onConnectionStateChaSEO靠我nged(BluetoothDevice device, int state) {Log.d(TAG, "onConnectionStateChanged:" + device + " state:"SEO靠我 + state);if (state == BluetoothProfile.STATE_CONNECTED) {mHostDevice = device;}if (state == BluetooSEO靠我thProfile.STATE_DISCONNECTED) {mHostDevice = null;}if (mBluetoothHidStateListener != null) {mBluetooSEO靠我thHidStateListener.onConnectionStateChanged(state);}}});}

蓝牙鼠标Mouse的描述信息如下,主要 为 MOUSE_COMBO 的描述协议,正确的SEO靠我描述协议才能成功与其他设备通信。

public class HidConfig {public final static String MOUSE_NAME = "VV Mouse";public fiSEO靠我nal static String DESCRIPTION = "VV for you";public final static String PROVIDER = "VV";public statiSEO靠我c final byte[] MOUSE_COMBO = {(byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop)(byte) 0x09, SEO靠我(byte) 0x02, // USAGE (Mouse)(byte) 0xa1, (byte) 0x01, // COLLECTION (Application)(byte) 0x85, (byteSEO靠我) 0x04, // REPORT_ID (4)(byte) 0x09, (byte) 0x01, // USAGE (Pointer)(byte) 0xa1, (byte) 0x00, // COLSEO靠我LECTION (Physical)(byte) 0x05, (byte) 0x09, // USAGE_PAGE (Button)(byte) 0x19, (byte) 0x01, // USAGESEO靠我_MINIMUM (Button 1)(byte) 0x29, (byte) 0x02, // USAGE_MAXIMUM (Button 2)(byte) 0x15, (byte) 0x00, //SEO靠我 LOGICAL_MINIMUM (0)(byte) 0x25, (byte) 0x01, // LOGICAL_MAXIMUM (1)(byte) 0x95, (byte) 0x03, // REPSEO靠我ORT_COUNT (3)(byte) 0x75, (byte) 0x01, // REPORT_SIZE (1)(byte) 0x81, (byte) 0x02, // INPUT (Data,VaSEO靠我r,Abs)(byte) 0x95, (byte) 0x01, // REPORT_COUNT (1)(byte) 0x75, (byte) 0x05, // REPORT_SIZE (5)(byteSEO靠我) 0x81, (byte) 0x03, // INPUT (Cnst,Var,Abs)(byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic DesktopSEO靠我)(byte) 0x09, (byte) 0x30, // USAGE (X)(byte) 0x09, (byte) 0x31, // USAGE (Y)(byte) 0x09, (byte) 0x3SEO靠我8, // USAGE (Wheel)(byte) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127)(byte) 0x25, (byte) 0x7F, // LSEO靠我OGICAL_MAXIMUM (127)(byte) 0x75, (byte) 0x08, // REPORT_SIZE (8)(byte) 0x95, (byte) 0x03, // REPORT_SEO靠我COUNT (3)(byte) 0x81, (byte) 0x06, // INPUT (Data,Var,Rel)//水平滚轮(byte) 0x05, (byte) 0x0c, // USAGE_PSEO靠我AGE (Consumer Devices)(byte) 0x0a, (byte) 0x38, (byte) 0x02, // USAGE (AC Pan)(byte) 0x15, (byte) 0xSEO靠我81, // LOGICAL_MINIMUM (-127)(byte) 0x25, (byte) 0x7f, // LOGICAL_MAXIMUM (127)(byte) 0x75, (byte) 0SEO靠我x08, // REPORT_SIZE (8)(byte) 0x95, (byte) 0x01, // REPORT_COUNT (1)(byte) 0x81, (byte) 0x06, // INPSEO靠我UT (Data,Var,Rel)(byte) 0xc0, // END_COLLECTION(byte) 0xc0, // END_COLLECTION};

在注册完成后启动设备发现,让HID能被其他SEO靠我设备发现,下面ActivityResultLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) 相当于调用 SEO靠我BluetoothAdapter.setScanMode() 的隐藏API

private ActivityResultLauncher<Intent> mActivityResultLauncher;SEO靠我@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setCSEO靠我ontentView(R.layout.activity_mouse);mActivityResultLauncher = registerForActivityResult(new ActivitySEO靠我ResultContracts.StartActivityForResult(), result -> {Log.d(TAG, "onActivityResult:" + result.toStrinSEO靠我g());});}@Overridepublic void onRegisterStateChanged(boolean registered, boolean hasDevice) {if (regSEO靠我istered) {if (!hasDevice) {// startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCSEO靠我OVERABLE), 1);mActivityResultLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLESEO靠我));}}}

ActivityResultLauncher 的相关方法也可用 startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUSEO靠我EST_DISCOVERABLE), REQUEST_CODE) 来替代,但 startActivityForResult() 是废弃的方法,不建议使用。

接下来与蓝牙主机(电脑、手机等)进行蓝牙配对,SEO靠我已配对过需要取消配对。配对完成即可实现对蓝牙主机的鼠标触摸控制。

手势识别

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

CustSEO靠我omMotionListener customMotionListener = new CustomMotionListener(this, mBluetoothHidManager); SEO靠我 findViewById(R.id.layout_touch).setOnTouchListener(customMotionListener);

手势逻辑处理代码如下:

package com.exSEO靠我ample.bluetoothproject;import android.content.Context; import android.view.GestureDetector; SEO靠我 import android.view.MotionEvent; import android.view.View; import android.viSEO靠我ew.ViewConfiguration;import org.apache.commons.lang3.concurrent.BasicThreadFactory;import java.util.SEO靠我concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutorSEO靠我; import java.util.concurrent.TimeUnit;public class CustomMotionListener implements View.OnTSEO靠我ouchListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {private final SEO靠我GestureDetector mGestureDetector;private BluetoothHidManager mBluetoothHidManager;private int mPointSEO靠我Count;private long mDoubleFingerTime;private final ScheduledExecutorService mExecutorService;privateSEO靠我 float mPreX;private float mPreY;private boolean mLongPress;public CustomMotionListener(Context contSEO靠我ext, BluetoothHidManager bluetoothHidManager) {mBluetoothHidManager = bluetoothHidManager;mGestureDeSEO靠我tector = new GestureDetector(context, this);mGestureDetector.setOnDoubleTapListener(this);mExecutorSSEO靠我ervice = new ScheduledThreadPoolExecutor(1,new BasicThreadFactory.Builder().namingPattern("mouse-schSEO靠我edule-pool-%d").daemon(true).build());}@Overridepublic boolean onSingleTapConfirmed(MotionEvent e) {SEO靠我return false;}@Overridepublic boolean onDoubleTap(MotionEvent e) {return false;}@Overridepublic boolSEO靠我ean onDoubleTapEvent(MotionEvent e) {//左键单指双击(选中文本的效果)if (e.getAction() == MotionEvent.ACTION_DOWN) SEO靠我{mBluetoothHidManager.sendLeftClick(true);} else if (e.getAction() == MotionEvent.ACTION_UP) {mBluetSEO靠我oothHidManager.sendLeftClick(false);}return true;}@Overridepublic boolean onDown(MotionEvent e) {retSEO靠我urn false;}@Overridepublic void onShowPress(MotionEvent e) {}@Overridepublic boolean onSingleTapUp(MSEO靠我otionEvent e) {//左键单击mBluetoothHidManager.sendLeftClick(true);mBluetoothHidManager.sendLeftClick(falSEO靠我se);return true;}@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, fSEO靠我loat distanceY) {//双指滚动,x为水平滚动,y为垂直滚动,消抖处理if (mPointCount == 2) {if (Math.abs(distanceX) > Math.abs(SEO靠我distanceY)) {distanceX = distanceX > 0 ? 1 : distanceX < 0 ? -1 : 0;distanceY = 0;} else {distanceY SEO靠我= distanceY > 0 ? -1 : distanceY < 0 ? 1 : 0;distanceX = 0;}mBluetoothHidManager.sendWheel((byte) (dSEO靠我istanceX), (byte) (distanceY));}return false;}@Overridepublic void onLongPress(MotionEvent e) {//单键长SEO靠我按效果mBluetoothHidManager.sendLeftClick(true);mLongPress = true;}@Overridepublic boolean onFling(MotioSEO靠我nEvent e1, MotionEvent e2, float velocityX, float velocityY) {return false;}@Overridepublic boolean SEO靠我onTouch(View v, MotionEvent event) {float x = event.getX();float y = event.getY();if (mGestureDetectSEO靠我or.onTouchEvent(event)) {return true;}mPointCount = event.getPointerCount();switch (event.getActionMSEO靠我asked()) {case MotionEvent.ACTION_POINTER_DOWN://双指单击代表右键记录时间if (event.getPointerCount() == 2) {mDouSEO靠我bleFingerTime = System.currentTimeMillis();}break;case MotionEvent.ACTION_MOVE://单指代表移动鼠标if (event.gSEO靠我etPointerCount() == 1) {float dx = x - mPreX;if (dx > 127) dx = 127;if (dx < -128) dx = -128;float dSEO靠我y = y - mPreY;if (dy > 127) dy = 127;if (dy < -128) dy = -128;mBluetoothHidManager.senMouse((byte) dSEO靠我x, (byte) dy);} else {mBluetoothHidManager.senMouse((byte) 0, (byte) 0);}break;case MotionEvent.ACTISEO靠我ON_UP:if (mLongPress) {mBluetoothHidManager.sendLeftClick(false);mLongPress = false;}break;case MotiSEO靠我onEvent.ACTION_POINTER_UP://双指按下代表右键if (event.getPointerCount() == 2 && System.currentTimeMillis() -SEO靠我 mDoubleFingerTime < ViewConfiguration.getDoubleTapTimeout()) {mBluetoothHidManager.sendRightClick(tSEO靠我rue);//延时释放避免无效mExecutorService.scheduleWithFixedDelay(new Runnable() {@Overridepublic void run() {mSEO靠我BluetoothHidManager.sendRightClick(false);}}, 0, 50, TimeUnit.MILLISECONDS); }break;default:break;}mSEO靠我PreX = x;mPreY = y;return true;} }

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

private boolean mLeftClick;private booSEO靠我lean mRightClick;public void sendLeftClick(boolean click) {mLeftClick = click;senMouse((byte) 0x00, SEO靠我(byte) 0x00);}public void sendRightClick(boolean click) {mRightClick = click;senMouse((byte) 0x00, (SEO靠我byte) 0x00);}public void senMouse(byte dx, byte dy) {if (mHidDevice == null) {Log.e(TAG, "senMouse fSEO靠我ailed, hid device is null!");return;}if (mHostDevice == null) {Log.e(TAG, "senMouse failed, hid deviSEO靠我ce is not connected!");return;}byte[] bytes = new byte[5];//bytes[0]字节:bit0: 1表示左键按下 0表示左键抬起 | bit1:SEO靠我 1表示右键按下 0表示右键抬起 | bit2: 1表示中键按下 | bit7~3:补充的常数,无意义,这里为0即可bytes[0] = (byte) (bytes[0] | (mLeftClick SEO靠我? 1 : 0));bytes[0] = (byte) (bytes[0] | (mRightClick ? 1 : 0) << 1);bytes[1] = dx;bytes[2] = dy;Log.SEO靠我d(TAG, "senMouse Left:" + mLeftClick+ ",Right:" + mRightClick + ",bytes: " + BluetoothUtils.bytesToHSEO靠我exString(bytes));mHidDevice.sendReport(mHostDevice, 4, bytes);}public void sendWheel(byte hWheel, bySEO靠我te vWheel) {if (mHidDevice == null) {Log.e(TAG, "sendWheel failed, hid device is null!");return;}if SEO靠我(mHostDevice == null) {Log.e(TAG, "sendWheel failed, hid device is not connected!");return;}byte[] bSEO靠我ytes = new byte[5];bytes[3] = vWheel; //垂直滚轮bytes[4] = hWheel; //水平滚轮Log.d(TAG, "sendWheel vWheel:" SEO靠我+ vWheel + ",hWheel:" + hWheel);mHidDevice.sendReport(mHostDevice, 4, bytes);}

效果

实现以上步骤即可将手机变成蓝牙鼠标/触控SEO靠我板,下面是实现的效果:

鼠标移动:

左键单击:

左键单指快速双击:

右键双指单击:

双指垂直上下滚动:

双指水平左右滚动:

完整视频效果展示:

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

“SEO靠我”的新闻页面文章、图片、音频、视频等稿件均为自媒体人、第三方机构发布或转载。如稿件涉及版权等问题,请与 我们联系删除或处理,客服邮箱:html5sh@163.com,稿件内容仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同 其观点或证实其内容的真实性。

网站备案号:浙ICP备17034767号-2