Android 蓝牙Hid开发

原文地址: https://blog.csdn.net/VNanyesheshou/article/details/61914974

Demo下载:http://www.demodashi.com/demo/13891.html
最近客户需求,android系统的一个设备,想连接一个扫描枪(类似蓝牙键盘,只支持hid协议),并且可以收到其数据,填写到输入框中。我这里借了一个蓝牙鼠标,用来与android设备连接。

1 Hid简介

HID设备(Hunman Interface Device Profile),即人机交互设备,常见的有鼠标,键盘,游戏手柄,等等。一般有线方式都是通过USB连线连接到机器设备,作为用户输入设备。在蓝牙技术中,HID设备的接入就是无线的了。

网上查资料说hid从android4.0开始支持(可能是usb hid),不过蓝牙hid应该从android4.2开始支持的,如下图所示:在这里插入图片描述
android4.1.2中的Bluetooth应用中没有hid的相关代码,而android4.2源码中Bluetooth应用中才有hid的代码。
在这里插入图片描述

2 主要代码

连接hid设备步骤:

  1. 开启蓝牙
  2. 获得inputDevice profile
  3. 扫描
  4. 配对
  5. 连接
2.1 开启蓝牙,通过广播接收者监听蓝牙相关状态。
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    Toast.makeText(this, "不支持蓝牙功能", 0).show();
    // 不支持蓝牙
    return;
}
// 如果没有打开蓝牙
if (!mBluetoothAdapter.isEnabled()) {
    mBluetoothAdapter.enable();
}
// 初始化广播接收者
mBroadcastReceiver = new BlueBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
intentFilter.addAction("android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED");
this.registerReceiver(mBroadcastReceiver, intentFilter);
2.2 获得inputDevice profile

// 4.2以上才支持HID模式

if (Build.VERSION.SDK_INT >= 17) {
    mHidUtil = HidUtil.getInstance(this);

}
public static HidUtil getInstance(Context context){
    if(null == instance){
        instance = new HidUtil(context);
    }
    return instance;
}
 
private HidUtil(Context context) {
    this.context = context;
    mBtAdapter = BluetoothAdapter.getDefaultAdapter();
    try {
        mBtAdapter.getProfileProxy(context,
                mListener, INPUT_DEVICE);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

通过BluetoothAdapter对象调用getProfileProxy()函数获取代理蓝牙输入设备代理对象。

其中参数mListener必须实现BluetoothProfile.ServiceListener()。获取代理对象成功或失败都会回调该Listener。

private BluetoothProfile.ServiceListener mListener = new BluetoothProfile.ServiceListener() {
	@Override
	public void onServiceConnected(int profile, BluetoothProfile proxy) {
		Log.i(TAG, "mConnectListener onServiceConnected");
		//BluetoothProfile proxy这个已经是BluetoothInputDevice类型了
		try {
			if (profile == INPUT_DEVICE) {
				mBluetoothProfile = proxy;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
 
	@Override
	public void onServiceDisconnected(int profile) {
		Log.i(TAG, "mConnectListener onServiceConnected");
	}
};

当连接代理服务成功,回调onServiceConnected()函数,失败则回调onServiceDisconnected()函数。
其中onServiceConnected()中的参数proxy类型为BluetoothProfile,这里因为获取BluetoothHeadset、BluetoothA2dp对象也要使用该回调函数,所以参数类型设置为BluetoothInputDevice、BluetoothHeadset、BluetoothA2dp的父类。这里可以将其转换成BluetoothInputDevice类型(BluetoothInputDevice是BluetoothProfile的子类)。

获取到输入设备的代理对象,之后就可以进行连接操作了。

2.3 扫描(点击连接按钮开始扫描蓝牙设备)
mBluetoothAdapter.startDiscovery();
2.4 配对

广播接收者监听扫描到蓝牙设备,过滤出所需蓝牙设备进行配对,如果之前配对过则直接连接。

if(action.equals(BluetoothDevice.ACTION_FOUND)){
    // 通过广播接收到了BluetoothDevice
    final BluetoothDevice device = (BluetoothDevice) intent
            .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    if (device == null) return;
    String btname = device.getName();
    String address = device.getAddress();
    Log.i(TAG, "bluetooth name:"+btname+",address:"+address);
    if((address != null&& address.equals(HID_ADDR))||(btname != null && btname.equals(HID_NAME))){
        mConnectDevice = device;
        mBluetoothAdapter.cancelDiscovery();
        if(!mHidUtil.isBonded(device)){
            //先配对
            mHidUtil.pair(device);
        }else {
            //已经配对则直接连接
            mHidUtil.connect(device);
        }
    }
}

HidUtil类中的配对方法:

	/**
	 * 配对
	 * @param BluetoothDevice
     */
public void pair(BluetoothDevice device) {
    Log.i(TAG, "pair device:"+device);
    Method createBondMethod;
    try {
        createBondMethod = BluetoothDevice.class.getMethod("createBond");
        createBondMethod.invoke(device);
    } catch (Exception e) {
        e.printStackTrace();
    }
 
}
2.5 连接(配对成功后)
if(action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)){
	BluetoothDevice device = intent
        	.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
	String name = device.getName();
	String address = device.getAddress();
	Log.i(TAG,"name:"+name+",address:"+address+",bondstate:"+device.getBondState());
	if((address != null&& address.equals(HID_ADDR))||(name != null && name.equals(HID_NAME))){
    		if(device.getBondState() == BluetoothDevice.BOND_BONDED)
        		mHidUtil.connect(device);
	}
}

判断是否是要连接的输入设备,如果符合条件则进行连接。

HidUtil中connect 方法

/**
 * 连接设备
 * @param bluetoothDevice
 */
public void connect(final BluetoothDevice device) {
    Log.i(TAG, "connect device:"+device);
    try {
        //得到BluetoothInputDevice然后反射connect连接设备
        Method method = mBluetoothProfile.getClass().getMethod("connect",
                new Class[] { BluetoothDevice.class });
        method.invoke(mBluetoothProfile, device);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

BluetoothInputDevice中的connect方法是隐藏的,所以需要通过反射机制获取该方法进行调用。

2.6 监听连接状态

通过广播接收者监听连接状态。

if(action.equals("android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED")){
	int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,0);
	BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
	Log.i(TAG,"state="+state+",device="+device);
	if(state == BluetoothProfile.STATE_CONNECTED){//连接成功
		Toast.makeText(MainActivity.this, R.string.connnect_success, Toast.LENGTH_SHORT).show();
	} else if(state == BluetoothProfile.STATE_DISCONNECTED){//连接失败
		Toast.makeText(MainActivity.this, R.string.disconnected, Toast.LENGTH_SHORT).show();
	}
}
2.7 断开连接
if(mConnectDevice != null)
	mHidUtil.disConnect(mConnectDevice);

HidUtil中disconnect方法

/**
 * 断开连接
 * @param BluetoothDevice
 */
public void disConnect(BluetoothDevice device) {
    Log.i(TAG, "disConnect device:"+device);
    try {
        if (device != null) {
            Method method = mBluetoothProfile.getClass().getMethod("disconnect",
                    new Class[] { BluetoothDevice.class });
            method.invoke(mBluetoothProfile, device);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

手机端断开连接后,重新连接会提示“只能有鼠标发起重新连接请求,请使用鼠标重新连接”。

3 接收数据

adb shell

getevent -l

当连接成功后,会看到如下内容:

could not get driver version for /dev/input/mouse1, Not a typewriter
add device 7: /dev/input/event6
name: “Bluetooth Mouse”

这表示蓝牙鼠标成为一个输入设备

左击鼠标:

/dev/input/event6: EV_MSC MSC_SCAN 00090001
/dev/input/event6: EV_KEY BTN_MOUSE DOWN
/dev/input/event6: EV_SYN SYN_REPORT 00000000
/dev/input/event6: EV_MSC MSC_SCAN 00090001
/dev/input/event6: EV_KEY BTN_MOUSE UP
/dev/input/event6: EV_SYN SYN_REPORT 00000000

我们应用中打印

03-13 12:08:36.526 I/MainActivity(23670): dispatchTouchEvent ev:MotionEvent { action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=269.7555, y[0]=543.9628, toolType[0]=TOOL_TYPE_MOUSE, buttonState=BUTTON_PRIMARY, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=16788085, downTime=16788085, deviceId=39, source=0x2002 }
03-13 12:08:36.653 I/MainActivity(23670): dispatchTouchEvent ev:MotionEvent { action=ACTION_UP, actionButton=0, id[0]=0, x[0]=269.7555, y[0]=543.9628, toolType[0]=TOOL_TYPE_MOUSE, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=16788216, downTime=16788085, deviceId=39, source=0x2002 }

表示在屏幕中某位置处点击了一下。

右击鼠标:

/dev/input/event6: EV_MSC MSC_SCAN 00090002
/dev/input/event6: EV_KEY BTN_RIGHT DOWN
/dev/input/event6: EV_SYN SYN_REPORT 00000000
/dev/input/event6: EV_MSC MSC_SCAN 00090002
/dev/input/event6: EV_KEY BTN_RIGHT UP
/dev/input/event6: EV_SYN SYN_REPORT 00000000

03-13 12:09:48.147 I/MainActivity(23670): dispatchKeyEvent event:KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x0, hwFlags=0x0, repeatCount=0, eventTime=16859701, downTime=16859701, deviceId=39, source=0x2002 }
03-13 12:09:48.147 I/MainActivity(23670): onKeyDown keyCode=4
03-13 12:09:48.308 I/InputDispatcher( 996): touch process [23670], send to pg
03-13 12:09:48.327 I/MainActivity(23670): dispatchKeyEvent event:KeyEvent { action=ACTION_UP, keyCode=KEYCODE_BACK, scanCode=0, metaState=0, flags=0x0, hwFlags=0x0, repeatCount=0, eventTime=16859870, downTime=16859870, deviceId=39, source=0x2002 }
03-13 12:09:48.327 I/MainActivity(23670): onKeyUp keyCode=4
03-13 12:09:48.337 I/MainActivity(23670): onBackPressed

表示点击了一下返回键,程序退出。

移动鼠标会发现屏幕上小光标在移动,滑动鼠标也会触发相应事件。

4 其他

现在大部分手机是支持hid的,并且也将该功能打开状态。

如果是做系统开发,就需要注意将Bluetooth中的hid开关打开。

将源码中的packages/apps/Bluetooth/res/values/config.xml的profile_supported_hid 修改为true。

<bool name="profile_supported_hid">true</bool>

demo下载地址:http://www.demodashi.com/demo/13891.html

5 Demo测试

测试发现在android 10上运行代码时出现如下log:
Accessing hidden method Landroid/bluetooth/BluetoothHidHost;->connect(Landroid/bluetooth/BluetoothDevice;)Z (greylist-max-o, reflection, denied)

解决方法:
adb shell settings put global hidden_api_policy 1

参考博客:
https://blog.csdn.net/wangxueming/article/details/99673555
https://zhuanlan.zhihu.com/p/62955960

欢迎关注我的微信公众号:
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值