writeCharacteristic() - permission check failed!

Permission check failed

現(xiàn)象描述

Android 10 手機(jī)連接GATT后欣舵,讀寫characteristic結(jié)果返回true,但是確沒有callback返回糙俗,logcat打印如下:

02-20 04:26:15.599  3815  3834 W BtGatt.GattService: writeCharacteristic() - permission check failed!

源碼分析

BluetoothGatt#writeCharacteristic

android.bluetooth.BluetoothGatt.java

/**
     * Writes a given characteristic and its values to the associated remote device.
     *
     * <p>Once the write operation has been completed, the
     * {@link BluetoothGattCallback#onCharacteristicWrite} callback is invoked,
     * reporting the result of the operation.
     *
     * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
     *
     * @param characteristic Characteristic to write on the remote device
     * @return true, if the write operation was initiated successfully
     */
    public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic) {
        if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
                && (characteristic.getProperties()
                & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) {
            return false;
        }

        if (VDBG) Log.d(TAG, "writeCharacteristic() - uuid: " + characteristic.getUuid());
        if (mService == null || mClientIf == 0 || characteristic.getValue() == null) return false;

        BluetoothGattService service = characteristic.getService();
        if (service == null) return false;

        BluetoothDevice device = service.getDevice();
        if (device == null) return false;

        synchronized (mDeviceBusyLock) {
            if (mDeviceBusy) return false;
            mDeviceBusy = true;
        }

        try {
            mService.writeCharacteristic(mClientIf, device.getAddress(),
                    characteristic.getInstanceId(), characteristic.getWriteType(),
                    AUTHENTICATION_NONE, characteristic.getValue());
        } catch (RemoteException e) {
            Log.e(TAG, "", e);
            mDeviceBusy = false;
            return false;
        }

        return true;
    }

GattService#writeCharacteristic

/android/platform/packages/apps/Bluetooth/src/com/android/bluetooth/gatt/GattService.java

void writeCharacteristic(int clientIf, String address, int handle, int writeType, int authReq,
            byte[] value) {
        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");

        if (VDBG) {
            Log.d(TAG, "writeCharacteristic() - address=" + address);
        }

        if (mReliableQueue.contains(address)) {
            writeType = 3; // Prepared write
        }

        Integer connId = mClientMap.connIdByAddress(clientIf, address);
        if (connId == null) {
            Log.e(TAG, "writeCharacteristic() - No connection for " + address + "...");
            return;
        }

        if (!permissionCheck(connId, handle)) {
            Log.w(TAG, "writeCharacteristic() - permission check failed!");
            return;
        }

        gattClientWriteCharacteristicNative(connId, handle, writeType, authReq, value);
    }

從上面的代碼可以看到鱼蝉,writeCharacteristic() - permission check failed! 是在permissionCheck(connId, handle)返回失敗的情況下才打印的措拇。

permissionCheck

/android/platform/packages/apps/Bluetooth/src/com/android/bluetooth/gatt/GattService.java

private boolean permissionCheck(int connId, int handle) {
        Set<Integer> restrictedHandles = mRestrictedHandles.get(connId);
        if (restrictedHandles == null || !restrictedHandles.contains(handle)) {
            return true;
        }

        return (checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)
                == PERMISSION_GRANTED);
    }

從上面的代碼可以看出檢查流程如下:

  • 首先檢查 restrictedHandles 是否包含當(dāng)前操作的 handle拷获,由于handle對(duì)應(yīng)的UUID在restrictedHandles 中淆九,所以開始檢查BLUETOOTH_PRIVILEGED權(quán)限

  • 但是只有系統(tǒng)應(yīng)用才可以申請(qǐng)獲取BLUETOOTH_PRIVILEGED權(quán)限纹安,所以最后依然返回false.

第三方應(yīng)用是無(wú)法獲取BLUETOOTH_PRIVILEGED權(quán)限的,所以問(wèn)題應(yīng)該是出在restrictedHandles上考余。

restrictedHandles

/**
* Set of restricted (which require a BLUETOOTH_PRIVILEGED permission) handles per connectionId.
*/
private final Map<Integer, Set<Integer>> mRestrictedHandles = new HashMap<>();

void onGetGattDb(int connId, ArrayList<GattDbElement> db) throws RemoteException {
        String address = mClientMap.addressByConnId(connId);

        if (DBG) {
            Log.d(TAG, "onGetGattDb() - address=" + address);
        }

        ClientMap.App app = mClientMap.getByConnId(connId);
        if (app == null || app.callback == null) {
            Log.e(TAG, "app or callback is null");
            return;
        }

        List<BluetoothGattService> dbOut = new ArrayList<BluetoothGattService>();
        Set<Integer> restrictedIds = new HashSet<>();

        BluetoothGattService currSrvc = null;
        BluetoothGattCharacteristic currChar = null;
        boolean isRestrictedSrvc = false;
        boolean isHidSrvc = false;
        boolean isRestrictedChar = false;

        for (GattDbElement el : db) {
            switch (el.type) {
                case GattDbElement.TYPE_PRIMARY_SERVICE:
                case GattDbElement.TYPE_SECONDARY_SERVICE:
                    if (DBG) {
                        Log.d(TAG, "got service with UUID=" + el.uuid + " id: " + el.id);
                    }

                    currSrvc = new BluetoothGattService(el.uuid, el.id, el.type);
                    dbOut.add(currSrvc);
                    isRestrictedSrvc =
                            isFidoSrvcUuid(el.uuid) || isAndroidTvRemoteSrvcUuid(el.uuid);
                    isHidSrvc = isHidSrvcUuid(el.uuid);
                    if (isRestrictedSrvc) {
                        restrictedIds.add(el.id);
                    }
                    break;

                case GattDbElement.TYPE_CHARACTERISTIC:
                    if (DBG) {
                        Log.d(TAG, "got characteristic with UUID=" + el.uuid + " id: " + el.id);
                    }

                    currChar = new BluetoothGattCharacteristic(el.uuid, el.id, el.properties, 0);
                    currSrvc.addCharacteristic(currChar);
                    isRestrictedChar = isRestrictedSrvc || (isHidSrvc && isHidCharUuid(el.uuid));
                    if (isRestrictedChar) {
                        restrictedIds.add(el.id);
                    }
                    break;

                case GattDbElement.TYPE_DESCRIPTOR:
                    if (DBG) {
                        Log.d(TAG, "got descriptor with UUID=" + el.uuid + " id: " + el.id);
                    }

                    currChar.addDescriptor(new BluetoothGattDescriptor(el.uuid, el.id, 0));
                    if (isRestrictedChar) {
                        restrictedIds.add(el.id);
                    }
                    break;

                case GattDbElement.TYPE_INCLUDED_SERVICE:
                    if (DBG) {
                        Log.d(TAG, "got included service with UUID=" + el.uuid + " id: " + el.id
                                + " startHandle: " + el.startHandle);
                    }

                    currSrvc.addIncludedService(
                            new BluetoothGattService(el.uuid, el.startHandle, el.type));
                    break;

                default:
                    Log.e(TAG, "got unknown element with type=" + el.type + " and UUID=" + el.uuid
                            + " id: " + el.id);
            }
        }

        if (!restrictedIds.isEmpty()) {
            mRestrictedHandles.put(connId, restrictedIds);
        }
        // Search is complete when there was error, or nothing more to process
        app.callback.onSearchComplete(address, dbOut, 0 /* status */);
    }

從上面的代碼看到restrictedHandles里面包含了需要被過(guò)濾的UUID先嬉,從前面的permissionCheck已經(jīng)知道,這些被限制的UUID只有系統(tǒng)應(yīng)用才可以訪問(wèn)楚堤。

我們測(cè)試設(shè)備的服務(wù)里面確實(shí)沒有需要被限制的UUID為什么也會(huì)被過(guò)濾疫蔓?

繼續(xù)跟蹤定位發(fā)現(xiàn),restrictedHandles只有put操作身冬,沒有remove或者clear操作衅胀。于是懷疑是restrictedHandles緩存導(dǎo)致的,模擬場(chǎng)景如下:

  • 首先先連接一個(gè)HID設(shè)備酥筝,查詢到的服務(wù)里面包含被限制的Service(這里以HID為例)滚躯,連接成功后,connId=0x09.

  • 斷開HID設(shè)備嘿歌,connId=0x09被釋放掸掏。

  • 連接一個(gè)新的LE設(shè)備,服務(wù)里面沒有需要被限制的Service.連接成功后宙帝,connId也是0x09丧凤。

  • 從前面的onGetGattDb可以看出,雖然新的設(shè)備沒有需要被限制的Service步脓,但是由于restrictedHandles沒有被清空愿待,兩次的connId也是一樣的,導(dǎo)致后面判斷的時(shí)候依然會(huì)被過(guò)濾靴患。

    if (!restrictedIds.isEmpty()) {
       mRestrictedHandles.put(connId, restrictedIds);
    }
    

為什么其他Android設(shè)備正常仍侥,只有 Android 10有問(wèn)題?

前面我們分析的代碼就是Android 10 的。mRestrictedHandles也是在Android 10才加上的鸳君。

Android 10 以前版本的 permissionCheck是直接檢查UUID访圃,不會(huì)受前一次連接的緩存影響。

boolean permissionCheck(int connId, int handle) {
        List<BluetoothGattService> db = mGattClientDatabases.get(connId);
        if (db == null) {
            return true;
        }

        for (BluetoothGattService service : db) {
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
                if (handle == characteristic.getInstanceId()) {
                    return !((isRestrictedCharUuid(characteristic.getUuid())
                            || isRestrictedSrvcUuid(service.getUuid()))
                            && (0 != checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)));
                }

                for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
                    if (handle == descriptor.getInstanceId()) {
                        return !((isRestrictedCharUuid(characteristic.getUuid())
                                || isRestrictedSrvcUuid(service.getUuid())) && (0
                                != checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)));
                    }
                }
            }
        }

        return true;
    }

結(jié)論

從上面的分析可以知道相嵌,這個(gè)問(wèn)題是Android 10 引入的 BUG腿时,Android 10 以前的系統(tǒng)沒有這個(gè)問(wèn)題况脆。第三方APP連接LE設(shè)備后,如果前一次連接了一個(gè)HID設(shè)備批糟,且當(dāng)前連接的connId和上一次連接的connId相同格了,就會(huì)觸發(fā)這個(gè)BUG。

解決方案

  • 重新開關(guān)藍(lán)牙徽鼎,這樣BluetoothManagerService就會(huì)重新bind盛末,緩存會(huì)被清除。

  • 確保App 或者第三方應(yīng)用不會(huì)去連接HID設(shè)備否淤,減小觸發(fā)BUG的機(jī)率悄但。

上面兩個(gè)方法都不能從根本上解決問(wèn)題,最終我們還是要等到Google更新patch來(lái)修復(fù)這個(gè)BUG石抡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末檐嚣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子啰扛,更是在濱河造成了極大的恐慌嚎京,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隐解,死亡現(xiàn)場(chǎng)離奇詭異鞍帝,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)煞茫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門帕涌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人续徽,你說(shuō)我怎么就攤上這事宵膨。” “怎么了炸宵?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)谷扣。 經(jīng)常有香客問(wèn)我土全,道長(zhǎng),這世上最難降的妖魔是什么会涎? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任裹匙,我火速辦了婚禮,結(jié)果婚禮上末秃,老公的妹妹穿的比我還像新娘概页。我一直安慰自己,他們只是感情好练慕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布惰匙。 她就那樣靜靜地躺著技掏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪项鬼。 梳的紋絲不亂的頭發(fā)上哑梳,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音绘盟,去河邊找鬼鸠真。 笑死,一個(gè)胖子當(dāng)著我的面吹牛龄毡,可吹牛的內(nèi)容都是我干的吠卷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼沦零,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼祭隔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蠢终,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤序攘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后寻拂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體程奠,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年祭钉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瞄沙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慌核,死狀恐怖距境,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垮卓,我是刑警寧澤垫桂,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站粟按,受9級(jí)特大地震影響诬滩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灭将,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一疼鸟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧庙曙,春花似錦空镜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)张抄。三九已至,卻和暖如春舶斧,著一層夾襖步出監(jiān)牢的瞬間欣鳖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工茴厉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泽台,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓矾缓,卻偏偏與公主長(zhǎng)得像怀酷,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嗜闻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

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