Bluetooth HID - Turn an android device into a Bluetooth mouse/touchpad (BluetoothHidDevice)

foreword

This article is one of the chapters of the Bluetooth HID series. This article uses the Redmi K30 (MIUI13 or Android 12) mobile phone as a Bluetooth HID device, which can be paired with other Bluetooth hosts such as computers, mobile phones, and tablets to realize the function of the mouse touchpad.
Bluetooth HID series chapters:
Bluetooth HID——Turning an android device into a Bluetooth keyboard (BluetoothHidDevice)
Bluetooth HID——Android uses a mobile phone to unlock a computer/tablet/iPhone
Bluetooth HID——Could not bind to Bluetooth (HID Device) Service with Intent * problem analysis

HID development

Android 9 opens up HID-related APIs BluetoothHidDevicesuch as , and registers as a Bluetooth HID device by communicating with the system Bluetooth HID service. First get the abstract instance BluetoothProfile.HID_DEVICEthrough the description type of :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);
    }

Then call BluetoothHidDevice.registerApp()to register the Android device as a Bluetooth HID device:

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

The description information of the Bluetooth mouse Mouse is as follows, mainly MOUSE_COMBOfor the description protocol, and the correct description protocol can successfully communicate with other devices.

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

Start device discovery after the registration is complete, so that HID can be discovered by other devices, the following ActivityResultLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)is equivalent to BluetoothAdapter.setScanMode()calling the hidden 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));
            }
        }
    }

ActivityResultLauncherThe related methods of can also be startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), REQUEST_CODE)used instead, but startActivityForResult()the obsolete method is not recommended.
Next, perform Bluetooth pairing with the Bluetooth host (computer, mobile phone, etc.), and need to cancel the pairing if it has been paired. After the pairing is completed, the mouse touch control of the Bluetooth host can be realized.

Gesture Recognition

Gesture recognition judges various gestures through touch events and gesture monitoring (moving the mouse, left-clicking, left-clicking double-clicking, right-clicking with two fingers, and two-finger vertical/horizontal scrolling).

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

The gesture logic processing code is as follows:

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

The report of the mouse touch button sent to the Bluetooth host is as follows:

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

Effect

After completing the above steps, the mobile phone can be turned into a Bluetooth mouse/touchpad. The following is the effect:
insert image description here

mouse movement:
Please add a picture description

Left click:
Please add a picture description

Double-click quickly with one finger of the left button:
Please add a picture description

Right-click with two fingers:
Please add a picture description

Scroll horizontally with two fingers:
Please add a picture description

Two fingers scroll vertically up and down:
Please add a picture description

Full video effect display:

Bluetooth HID - Turn an android device into a Bluetooth mouse/trackpad

Guess you like

Origin blog.csdn.net/CJohn1994/article/details/127867838