Android 配對(duì)連接HID藍(lán)牙設(shè)備

前段時(shí)間公司項(xiàng)目的需要連接藍(lán)牙設(shè)備挠锥,我們這個(gè)藍(lán)牙設(shè)備是一個(gè)藍(lán)牙手柄,相當(dāng)于是一個(gè)藍(lán)牙外設(shè)鍵盤侨赡,所以是屬于HID設(shè)備蓖租。剛開始也不知道還有HID藍(lán)牙設(shè)備,所以就按照普通藍(lán)牙設(shè)備連接羊壹。翻找了好久的資料蓖宦,才一點(diǎn)一點(diǎn)做好。

HID 是Human Interface Device的縮寫舶掖,由其名稱可以了解HID設(shè)備是直接與人交互的設(shè)備球昨,例如鍵盤、鼠標(biāo)與游戲桿等眨攘。不過(guò)HID設(shè)備并不一定要有人機(jī)接口,只要符合HID類別規(guī)范的設(shè)備都是HID設(shè)備嚣州。
android4.1.2中的Bluetooth應(yīng)用中沒(méi)有hid的相關(guān)代碼鲫售,而android4.2源碼中Bluetooth應(yīng)用中才有hid的代碼。

重點(diǎn)提一句该肴,在實(shí)際使用中情竹,其實(shí)設(shè)備的配對(duì)連接都不難,但是一定要開啟子線程進(jìn)行藍(lán)牙設(shè)備的開關(guān)匀哄、配對(duì)和連接秦效。不然很容易出現(xiàn)一堆異常。涎嚼。阱州。。

使用藍(lán)牙之前法梯,我們需要在AndroidManifest.xml里邊申請(qǐng)三個(gè)權(quán)限苔货,因?yàn)槲易龅乃闶窍到y(tǒng)級(jí)的軟件,就沒(méi)有動(dòng)態(tài)申請(qǐng)權(quán)限立哑。

 <uses-permission android:name="android.permission.BLUETOOTH" />
 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

這里說(shuō)一下夜惭,剛開始遇到的第一個(gè)坑,就是很多博客里邊只講了前兩個(gè)權(quán)限铛绰,而沒(méi)有說(shuō)第三個(gè)權(quán)限诈茧。

1.首先是藍(lán)牙設(shè)備的打開和關(guān)閉

在了解藍(lán)牙開關(guān)之前,我們首先要知道藍(lán)牙適配器的類BluetoothAdapter捂掰,這個(gè)類的主要功能就是:

  • 開關(guān)藍(lán)牙
  • 掃描藍(lán)牙設(shè)備
  • 設(shè)置/獲取藍(lán)牙狀態(tài)信息敢会,例如:藍(lán)牙狀態(tài)值镊叁、藍(lán)牙Name、藍(lán)牙Mac地址等走触;

直接上代碼:

//獲取BluetoothAdapter晦譬,如果獲取的BluetoothAdapter為空,則表示當(dāng)前設(shè)備不支持藍(lán)牙
BluetoothManager bluetoothManager = (BluetoothManager) SPApplication.getInstance().getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager != null) {
       BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
 } else {
      ToastUtils.showShort("藍(lán)牙暫時(shí)不可用");
      finishDelegate();
}

//判斷藍(lán)牙是否打開
if (!mBluetoothAdapter.isEnabled()) {
  //藍(lán)牙設(shè)備沒(méi)有打開
//打開藍(lán)牙設(shè)備互广,這一步應(yīng)該放到線程里邊操作
mBluetoothAdapter.enable();          
} else {
//藍(lán)牙設(shè)備已經(jīng)打開敛腌,需要關(guān)閉藍(lán)牙
//這一步也需要放到線程中操作
mBluetoothAdapter.disable();         
 }

2.開始進(jìn)行藍(lán)牙的掃描

藍(lán)牙的掃描是使用BluetoothAdapter對(duì)象中的方法startDiscovery(),不過(guò)首先我們需要注冊(cè)一個(gè)廣播接收者,接收藍(lán)牙掃描的數(shù)據(jù)惫皱。

1.廣播接收者:
public class BluetoothReciever extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            BluetoothDevice device;
            // 搜索發(fā)現(xiàn)設(shè)備時(shí)像樊,取得設(shè)備的信息;注意旅敷,這里有可能重復(fù)搜索同一設(shè)備
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                //這里就是我們掃描到的藍(lán)牙設(shè)備
                device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 
            }
            //狀態(tài)改變時(shí)
            else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
                device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                switch (device.getBondState()) {
                    case BluetoothDevice.BOND_BONDING://正在配對(duì)
                        Log.d("BlueToothTestActivity", "正在配對(duì)......");
                        break;
                    case BluetoothDevice.BOND_BONDED://配對(duì)結(jié)束
                        Log.d("BlueToothTestActivity", "完成配對(duì)");

                        break;
                    case BluetoothDevice.BOND_NONE://取消配對(duì)/未配對(duì)
                        Log.d("BlueToothTestActivity", "取消配對(duì)");

                    default:
                        break;
                }
            }
        }
    }

創(chuàng)建并注冊(cè)藍(lán)牙掃描的廣播接收者:

 // 初始化廣播接收者
mBroadcastReceiver = new BluetoothReciever();
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");
//這里我是用全局上下文注冊(cè)廣播接收者生棍,防止在解注冊(cè)的時(shí)候出錯(cuò)
SPApplication.getInstance().registerReceiver(mBroadcastReceiver, intentFilter);

開始進(jìn)行藍(lán)牙掃描

 mBluetoothAdapter.startDiscovery();

開始掃描之后,廣播接收者中就會(huì)出現(xiàn)我們掃描到的設(shè)備,設(shè)備信息存放在BluetoothDevice對(duì)象中媳谁,我們可以將這個(gè)對(duì)象存放到列表中涂滴,然后取出里邊的信息展示到界面上。

3.藍(lán)牙設(shè)備的配對(duì)和連接過(guò)程

HID藍(lán)牙設(shè)備在配對(duì)成功之后還要進(jìn)行連接這一步操作晴音,而普通的藍(lán)牙設(shè)備只需要配對(duì)這一步柔纵。順便提一句,藍(lán)牙設(shè)備的配對(duì)和連接需要放到子線程中操作锤躁。
對(duì)其中的連接操作以及BluetoothProfile的我也不是很理解搁料,這里可以參考Android HID設(shè)備的連接,這篇博客中對(duì)連接的步驟講的還是很清楚的系羞。
這里我把藍(lán)牙設(shè)備的配對(duì)和連接郭计,我寫到了Runnable的復(fù)寫類里邊,方便放到子線程中使用椒振。

public class BlueConnectThread implements Runnable {
    private Handler handler;
    private BluetoothDevice device;
    private BluetoothAdapter mAdapter;
    
    public BlueConnectThread(Handler handler, BluetoothDevice device, BluetoothAdapter adapter) {
        this.handler = handler;
        this.device = device;
        this.mAdapter = adapter;
    }

    @Override
    public void run() {
        //進(jìn)行HID藍(lán)牙設(shè)備的配對(duì)昭伸,配對(duì)成功之后才進(jìn)行下邊的操作
        if (pair(device)) {
            try {
               //進(jìn)行HID藍(lán)牙設(shè)備的連接操作,并返回連接結(jié)果杠人,這里邊的第3個(gè)參數(shù)就是代表連接HID設(shè)備勋乾,
                boolean profileProxy = mAdapter.getProfileProxy(SPApplication.getInstance(), connect, 4);
                Thread.sleep(3000);
                Message message = handler.obtainMessage(BluetoothDialog.CONNECT_BLUE);
                //通過(guò)handler將連接結(jié)果和連接的代理返回主線程中。這個(gè)BluetoothProfile對(duì)象在后邊還有用嗡善,所以要返回主線程中
                if (profileProxy) {
                    message.arg1=1;
                }else {
                    message.arg1=2;
                }
                message.obj = proxys;
                handler.sendMessage(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 配對(duì)
     *
     * @param bluetoothDevice
     */
    public boolean pair(BluetoothDevice bluetoothDevice) {
        device = bluetoothDevice;
        Method createBondMethod;
        try {
            createBondMethod = BluetoothDevice.class.getMethod("createBond");
            createBondMethod.invoke(device);
            return true;
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return false;
    }
    /**
      *個(gè)人理解是獲取當(dāng)前設(shè)備設(shè)備是否支持INPUT_DEVICE設(shè)備(HID設(shè)備)
      */
    public static int getInputDeviceHiddenConstant() {
        Class<BluetoothProfile> clazz = BluetoothProfile.class;
        for (Field f : clazz.getFields()) {
            int mod = f.getModifiers();
            if (Modifier.isStatic(mod) && Modifier.isPublic(mod)
                    && Modifier.isFinal(mod)) {
                try {
                    if (f.getName().equals("INPUT_DEVICE")) {
                        return f.getInt(null);
                    }
                } catch (Exception e) {
                }
            }
        }
        return -1;
    }

    private BluetoothProfile proxys;
    /**
     * 查看BluetoothInputDevice源碼辑莫,connect(BluetoothDevice device)該方法可以連接HID設(shè)備,但是查看BluetoothInputDevice這個(gè)類
     * 是隱藏類罩引,無(wú)法直接使用各吨,必須先通過(guò)BluetoothProfile.ServiceListener回調(diào)得到BluetoothInputDevice,然后再反射connect方法連接
     */
    private BluetoothProfile.ServiceListener connect = new BluetoothProfile.ServiceListener() {
        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
        @Override
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
            //BluetoothProfile proxy這個(gè)已經(jīng)是BluetoothInputDevice類型了
            try {
                proxys = proxy;
                if (profile == getInputDeviceHiddenConstant()) {
                    if (device != null) {
                        //得到BluetoothInputDevice然后反射connect連接設(shè)備
                        @SuppressLint("PrivateApi") Method method = proxy.getClass().getDeclaredMethod("connect",
                                new Class[]{BluetoothDevice.class});
                        method.invoke(proxy, device);
                    }
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(int profile) {

        }
    };
}

4.關(guān)閉當(dāng)前界面

需要注銷廣播接收者的注冊(cè)
@Override
public void onPause() {
    super.onPause();
    SPApplication.getInstance().unregisterReceiver(mBroadcastReceiver);
}
解除連接的代理(暫時(shí)這樣理解)

做這一步的原因是因?yàn)槊看瓮顺鏊{(lán)牙界面都會(huì)出現(xiàn)錯(cuò)誤,

android.app.ServiceConnectionLeaked: android.app.ServiceConnectionLeaked: Activity.com.xxxxxx.bluetoothconnect.MainActivity has leaked ServiceConnection android.bluetooth.BluetoothInputDevice$2@fd389e1 that was originally bound here

這揭蜒,横浑,,這好像是連接那一步綁定了服務(wù)屉更,在關(guān)閉界面的時(shí)候沒(méi)有解綁服務(wù)徙融。那我們只需要找到在哪開啟了什么服務(wù),再找到解綁這個(gè)服務(wù)的方法就行了瑰谜。
然后我就找到HID設(shè)備連接的方法getProfileProxy(SPApplication.getInstance(), connect, 4)欺冀,進(jìn)到源碼中查看,被我發(fā)現(xiàn)了BluetoothAdapter里邊的closeProfileProxy(int profile, BluetoothProfile proxy)這個(gè)方法萨脑,這個(gè)方法在源碼中的注釋是

 /**
     * Close the connection of the profile proxy to the Service.
     *
     * <p> Clients should call this when they are no longer using
     * the proxy obtained from {@link #getProfileProxy}.
     * Profile can be one of  {@link BluetoothProfile#HEALTH}, {@link BluetoothProfile#HEADSET} or
     * {@link BluetoothProfile#A2DP}
     *
     * @param profile
     * @param proxy Profile proxy object
     */
 public void closeProfileProxy(int profile, BluetoothProfile proxy) 

憑借我英語(yǔ)亞四級(jí)的水平隐轩,我們可以知道這個(gè)方法就是關(guān)閉服務(wù)的,這個(gè)服務(wù)跟連接有某種不可告人的關(guān)系渤早。职车。。
那我們就直接在關(guān)閉界面的時(shí)候使用就行了鹊杖。注意悴灵,經(jīng)驗(yàn)告訴我這個(gè)方法也需要在子線程中使用。
剛才從子線程中傳回來(lái)的BluetoothProfile對(duì)象在這里就能用到了仅淑。

 @Override
    public void onDestroyView() {
        super.onDestroyView();
        if (proxy != null) {
            ThreadPoolProxyFactory.getNormalThreadPoolProxy().execute(new Runnable() {
                @Override
                public void run() {
                    mBluetoothAdapter.closeProfileProxy(4, proxy);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

-----------結(jié)束称勋!
這里我只做了藍(lán)牙開關(guān),掃描涯竟,連接配對(duì)的操作,關(guān)于藍(lán)牙信號(hào)的接受和發(fā)送沒(méi)有做空厌。因?yàn)闆](méi)有需求啊庐船,哈哈哈。
有時(shí)間我再把源碼整理出來(lái)嘲更。
里邊的線程池參考Android線程池得要這么用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筐钟,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子赋朦,更是在濱河造成了極大的恐慌篓冲,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宠哄,死亡現(xiàn)場(chǎng)離奇詭異壹将,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)毛嫉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門诽俯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人承粤,你說(shuō)我怎么就攤上這事暴区〈惩牛” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵仙粱,是天一觀的道長(zhǎng)房交。 經(jīng)常有香客問(wèn)我,道長(zhǎng)伐割,這世上最難降的妖魔是什么候味? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮口猜,結(jié)果婚禮上负溪,老公的妹妹穿的比我還像新娘。我一直安慰自己济炎,他們只是感情好川抡,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著须尚,像睡著了一般崖堤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上耐床,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天密幔,我揣著相機(jī)與錄音,去河邊找鬼撩轰。 笑死胯甩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的堪嫂。 我是一名探鬼主播偎箫,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼皆串!你這毒婦竟也來(lái)了淹办?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤恶复,失蹤者是張志新(化名)和其女友劉穎怜森,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谤牡,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡副硅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拓哟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片想许。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出流纹,到底是詐尸還是另有隱情糜烹,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布漱凝,位于F島的核電站疮蹦,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏茸炒。R本人自食惡果不足惜愕乎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望壁公。 院中可真熱鬧感论,春花似錦、人聲如沸紊册。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)囊陡。三九已至芳绩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撞反,已是汗流浹背妥色。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遏片,地道東北人嘹害。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吮便,于是被迫代替她去往敵國(guó)和親吼拥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 最近項(xiàng)目使用藍(lán)牙线衫,之前并沒(méi)有接觸,還是發(fā)現(xiàn)了很多坑惑折,查閱了很多資料授账,說(shuō)的迷迷糊糊,今天特查看官方文檔惨驶。 說(shuō)下遇到的...
    King9527閱讀 1,797評(píng)論 0 1
  • 藍(lán)牙 注:本文翻譯自https://developer.android.com/guide/topics/conn...
    RxCode閱讀 8,691評(píng)論 11 99
  • 前言 最近在做Android藍(lán)牙這部分內(nèi)容白热,所以查閱了很多相關(guān)資料,在此總結(jié)一下粗卜。 基本概念 Bluetooth是...
    貓疏閱讀 14,629評(píng)論 7 113
  • Guide to BluetoothSecurity原文 本出版物可免費(fèi)從以下網(wǎng)址獲得:https://doi.o...
    公子小水閱讀 8,018評(píng)論 0 6
  • Android 平臺(tái)包含藍(lán)牙網(wǎng)絡(luò)堆棧支持屋确,憑借此項(xiàng)支持,設(shè)備能以無(wú)線方式與其他藍(lán)牙設(shè)備交換數(shù)據(jù)。應(yīng)用框架提供了通過(guò)...
    虎三呀閱讀 772評(píng)論 0 1