從 藍牙連接 權(quán)限檢查入手 分析 權(quán)限檢查機制

Android 12(API 31) 的藍牙權(quán)限做了個更改.

2021-7-20?·?Android 12 引入了 BLUETOOTH_SCAN 、 BLUETOOTH_ADVERTISE 和 BLUETOOTH_CONNECT 權(quán)限元暴,
可讓應用 掃描附近的設(shè)備(NearBy)舞骆,而無需請求位置權(quán)限(ACCESS_FINE_LOCATION).

參考: https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
(1)如果您的應用程序?qū)ふ宜{牙設(shè)備固灵,例如 BLE 外圍設(shè)備鲫趁,請聲明 BLUETOOTH_SCAN 權(quán)限踱葛。
(2)如果您的應用程序使當前設(shè)備可被其他藍牙設(shè)備發(fā)現(xiàn)土陪,請聲明該 BLUETOOTH_ADVERTISE 權(quán)限煤蹭。
(3)如果您的應用程序與已配對的藍牙設(shè)備通信笔喉,請聲明 BLUETOOTH_CONNECT 權(quán)限取视。
(4)對于遺留的藍牙相關(guān)權(quán)限聲明,設(shè)置 android:maxSdkVersion為30. 此應用兼容性步驟有助于系統(tǒng)僅向您的應用授予安裝在運行 Android 12 或更高版本的設(shè)備上時所需的藍牙權(quán)限常挚。
具體使用方法參考指南作谭。

由于這三種藍牙權(quán)限都是 運行時權(quán)限,必須 先在應用中 明確請求 用戶同意奄毡,然后才能查找藍牙設(shè)備.
因此折欠,就產(chǎn)生了 藍牙連接 權(quán)限檢查問題,我們從這個角度(BLUETOOTH_CONNECT) 入手吼过, 分析 權(quán)限檢查機制.

注:(1)在 targetSKD <=30 的應用锐秦,是無法聲明 上面三個權(quán)限的,僅有以下幾個( Nearby 附近的設(shè)備 權(quán)限默認開啟???)
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="android.permission.BLUETOOTH_DEBUG"/>
(2)Nearby 附近的設(shè)備 權(quán)限是為了兼容 舊版本Android 系統(tǒng)(舊SDK)盗忱,所以在SDK<=30默認開啟 ???
因此酱床,targetSKD <=30 的應用, 在API31(Android12) 的設(shè)備上必須要有這個權(quán)限才能使用藍牙 趟佃?扇谣?
否則會拋出 安全異常 錯誤?? 這是因為 AppOpsManager 在
系統(tǒng)層面**還會限制??
(3) targetSDK <=30, 無法使用 checkSelfPermission() 檢查 BLUETOOTH_CONNECT 等API31才有的權(quán)限.

1. Framework 藍牙模塊

客戶端(例如三方應用),想要使用到藍牙闲昭,需要調(diào)用 系統(tǒng)藍牙接口 獲取 藍牙服務(wù).

這里會使用 AIDL 獲取 已綁定的 藍牙服務(wù),
即從系統(tǒng) Framework 藍牙 轉(zhuǎn)到了 藍牙app 進程,
可以看出罐寨,也存在 藍牙應用 作為 服務(wù)提供者.

// /frameworks/base/core/java/android/bluetooth/
package android.bluetooth;

public final class BluetoothAdapter {
    public Set<BluetoothDevice> getBondedDevices() {
        if (getState() != STATE_ON) {
            return toDeviceSet(Arrays.asList());
        }
        try {
            mServiceLock.readLock().lock();
            if (mService != null) {
                return toDeviceSet(Attributable.setAttributionSource(
                        Arrays.asList(mService.getBondedDevices(mAttributionSource)),
                        mAttributionSource));
            }
            return toDeviceSet(Arrays.asList());
        } catch (RemoteException e) {
            Log.e(TAG, "", e);
        } finally {
            mServiceLock.readLock().unlock();
        }
        return null;
    }

這里注意 mService.getBondedDevices(mAttributionSource), 下面會分析.

2. App 藍牙模塊

上面的getBondedDevices 最終的是實現(xiàn),是在 藍牙應用里.
可以看出序矩,是在 IBluetooth.Stub 里的方法.

// /packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterService.java

public class AdapterService extends Service {
package com.android.bluetooth.btservice;
    public static class AdapterServiceBinder extends IBluetooth.Stub {
        @Override
        public BluetoothDevice[] getBondedDevices(AttributionSource attributionSource) {
            // don't check caller, may be called from system UI
            AdapterService service = getService();
            if (service == null || !Utils.checkConnectPermissionForDataDelivery(
                    service, attributionSource, "AdapterService getBondedDevices")) {
                return new BluetoothDevice[0];
            }

            return service.getBondedDevicesWoCustomDevice(); /* SS_BLE_FEATURE_P50 */
        }

這里注意 Utils.checkConnectPermissionForDataDelivery()會進行調(diào)用者權(quán)限的檢查, 信息封裝在 attributionSource

2.1 內(nèi)部權(quán)限檢查

// /packages/apps/Bluetooth/src/com/android/bluetooth/Utils.java
package com.android.bluetooth;
public final class Utils {
    @SuppressLint("AndroidFrameworkRequiresPermission")
    @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
    public static boolean checkConnectPermissionForDataDelivery(
            Context context, AttributionSource attributionSource, String message) {
        return checkPermissionForDataDelivery(context, BLUETOOTH_CONNECT,
                attributionSource, message);
    }

會調(diào)用它的內(nèi)部方法:checkPermissionForDataDelivery, 并傳入權(quán)限為 BLUETOOTH_CONNECT 即 藍牙連接 的權(quán)限


    @SuppressLint("AndroidFrameworkRequiresPermission")
    private static boolean checkPermissionForDataDelivery(Context context, String permission,
            AttributionSource attributionSource, String message) {
        // STOPSHIP(b/188391719): enable this security enforcement
        // attributionSource.enforceCallingUid();
        final int result = PermissionChecker.checkPermissionForDataDeliveryFromDataSource(
                context, permission, PID_UNKNOWN,
                new AttributionSource(context.getAttributionSource(), attributionSource), message);
        if (result == PERMISSION_GRANTED) {
            return true;
        }

        final String msg = "Need " + permission + " permission for " + attributionSource + ": "
                + message;
        if (result == PERMISSION_HARD_DENIED) {
            throw new SecurityException(msg);
        } else {
            Log.w(TAG, msg);
            return false;
        }
    }

這里又調(diào)用了 PermissionChecker.checkPermissionForDataDeliveryFromDataSource(), 根據(jù)返回結(jié)果處理.
(1) 返回 PERMISSION_GRANTED, 則結(jié)果返回true衩茸,代表 調(diào)用者 擁有了藍牙權(quán)限
(2) 返回 PERMISSION_HARD_DENIED, 則會拋出 SecurityException, 并提示 需要 XXX 的權(quán)限.
(3) 返回 其它值(只有PERMISSION_SOFT_DENIED這種), 則會返回false ,表示沒有 相應的藍牙權(quán)限.

2.2 委托PermissionChecker

package android.content;

public final class PermissionChecker {
    public static final int PERMISSION_GRANTED = PermissionCheckerManager.PERMISSION_GRANTED;
    public static final int PERMISSION_SOFT_DENIED =
            PermissionCheckerManager.PERMISSION_SOFT_DENIED;
    public static final int PERMISSION_HARD_DENIED =
            PermissionCheckerManager.PERMISSION_HARD_DENIED;        

    public static int checkPermissionForDataDeliveryFromDataSource(@NonNull Context context,
            @NonNull String permission, int pid, @NonNull AttributionSource attributionSource,
            @Nullable String message) {
        return checkPermissionForDataDeliveryCommon(context, permission, attributionSource,
                message, false /*startDataDelivery*/, /*fromDatasource*/ true);
    }
    
    @SuppressWarnings("ConstantConditions")
    private static int checkPermissionForDataDeliveryCommon(@NonNull Context context,
            @NonNull String permission, @NonNull AttributionSource attributionSource,
            @Nullable String message, boolean startDataDelivery, boolean fromDatasource) {
        return context.getSystemService(PermissionCheckerManager.class).checkPermission(permission,
                attributionSource.asState(), message, true /*forDataDelivery*/, startDataDelivery,
                fromDatasource, AppOpsManager.OP_NONE);
    }

分析:
(1) 會調(diào)用內(nèi)部方法 checkPermissionForDataDeliveryCommon, 并fromDatasource 參數(shù)設(shè)置為true傳入
(2) 進一步委托給 PermissionCheckerManager, 它也是一個系統(tǒng)服務(wù)贮泞,在 SystemServiceRegistry 代碼塊中注冊.

package android.app;

@SystemApi
public final class SystemServiceRegistry {
    static {
        registerService(Context.PERMISSION_CHECKER_SERVICE, PermissionCheckerManager.class,
                new CachedServiceFetcher<PermissionCheckerManager>() {
                    @Override
                    public PermissionCheckerManager createService(ContextImpl ctx)
                            throws ServiceNotFoundException {
                        return new PermissionCheckerManager(ctx.getOuterContext());
                    }});

2.3 委托 PermissionCheckerManager

package android.permission; 

public class PermissionCheckerManager {
    public static final int PERMISSION_GRANTED = IPermissionChecker.PERMISSION_GRANTED; 
    public static final int PERMISSION_SOFT_DENIED = IPermissionChecker.PERMISSION_SOFT_DENIED; 
    public static final int PERMISSION_HARD_DENIED = IPermissionChecker.PERMISSION_HARD_DENIED;

    @PermissionResult
    public int checkPermission(@NonNull String permission,
            @NonNull AttributionSourceState attributionSource, @Nullable String message,
            boolean forDataDelivery, boolean startDataDelivery, boolean fromDatasource,
            int attributedOp) {
        Objects.requireNonNull(permission);
        Objects.requireNonNull(attributionSource);
        // Fast path for non-runtime, non-op permissions where the attribution chain has
        // length one. This is the majority of the cases and we want these to be fast by
        // hitting the local in process permission cache.
        if (AppOpsManager.permissionToOpCode(permission) == AppOpsManager.OP_NONE) {
            if (fromDatasource) {
                if (attributionSource.next != null && attributionSource.next.length > 0) {
                    return mContext.checkPermission(permission, attributionSource.next[0].pid,
                            attributionSource.next[0].uid) == PackageManager.PERMISSION_GRANTED
                            ? PERMISSION_GRANTED : PERMISSION_HARD_DENIED;
                }
            } else {
                return (mContext.checkPermission(permission, attributionSource.pid,
                            attributionSource.uid) == PackageManager.PERMISSION_GRANTED)
                        ? PERMISSION_GRANTED : PERMISSION_HARD_DENIED;
            }
        }
        try {
            return mService.checkPermission(permission, attributionSource, message, forDataDelivery,
                    startDataDelivery, fromDatasource, attributedOp);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
        return PERMISSION_HARD_DENIED;
    }

分析:
(1) 由上面可以知, fromDatasource == true, 因此它還是調(diào)用 Context.checkPermission 進行檢查.
(2) Context.checkPermission 是一個抽象方法, 最終由 ContextImpl 實現(xiàn).(查看最初傳遞的 service, 即 AdapterService 對象 )
它需要三個參數(shù),第一個是權(quán)限名稱幔烛,第二個是pid, 第三個是 uid, pid/uid 均由attributionSource提供
(3) AppOpsManager 是谷歌原生的 應用操作(權(quán)限) 管理啃擦, PermissionManager 也是基于這個實現(xiàn)的(具體參考另外一篇)

package android.content;
class ContextImpl extends Context {
    @Override
    public int checkPermission(String permission, int pid, int uid) {
        if (permission == null) {
            throw new IllegalArgumentException("permission is null");
        }
        if (mParams.isRenouncedPermission(permission)
                && pid == android.os.Process.myPid() && uid == android.os.Process.myUid()) {
            Log.v(TAG, "Treating renounced permission " + permission + " as denied");
            return PERMISSION_DENIED;
        }
        return PermissionManager.checkPermission(permission, pid, uid);
    }

可見,它會根據(jù) 權(quán)限名稱/pid / uid, 再進行委托給 PermissionManager 去檢查.

而 PermissonManager 將會再后續(xù)單獨介紹.

3. 小結(jié)

到此, 我們可以初步得出一個結(jié)論:
(1) 應用使用某個 需要權(quán)限的功能時, 系統(tǒng)(Framework)會檢查 應用有沒有這個 權(quán)限饿悬,沒有則拋出異常.
(2) 應用的 pid/uid 信息, 封裝在 AttributionSource 對象里令蛉。
(3) 委托 PermissionCheckerManager 進行檢查, 這里將 pid/uid 取出來, 再調(diào)用 Context 進行檢查.
(4) 這個 Context 其實是 AdapterService 對象,類推可以是我們應用 中的 Activity狡恬、Service 珠叔、Application
(5) Context 里又會進行 委托 PermissionManager 進行 真正的檢查.

參考:
Android 12 源碼:http://aosp.opersys.com/xref/android-12.0.0_r2/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市弟劲,隨后出現(xiàn)的幾起案子祷安,更是在濱河造成了極大的恐慌,老刑警劉巖兔乞,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汇鞭,死亡現(xiàn)場離奇詭異凉唐,居然都是意外死亡,警方通過查閱死者的電腦和手機霍骄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門台囱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人读整,你說我怎么就攤上這事簿训。” “怎么了米间?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵强品,是天一觀的道長。 經(jīng)常有香客問我车伞,道長择懂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任另玖,我火速辦了婚禮困曙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谦去。我一直安慰自己慷丽,他們只是感情好,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布鳄哭。 她就那樣靜靜地躺著要糊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妆丘。 梳的紋絲不亂的頭發(fā)上锄俄,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機與錄音勺拣,去河邊找鬼奶赠。 笑死,一個胖子當著我的面吹牛药有,可吹牛的內(nèi)容都是我干的毅戈。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼愤惰,長吁一口氣:“原來是場噩夢啊……” “哼苇经!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宦言,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤扇单,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后奠旺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體令花,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡阻桅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了兼都。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫂沉。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扮碧,靈堂內(nèi)的尸體忽然破棺而出趟章,到底是詐尸還是另有隱情,我是刑警寧澤慎王,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布蚓土,位于F島的核電站,受9級特大地震影響赖淤,放射性物質(zhì)發(fā)生泄漏蜀漆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一咱旱、第九天 我趴在偏房一處隱蔽的房頂上張望确丢。 院中可真熱鬧,春花似錦吐限、人聲如沸鲜侥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽描函。三九已至,卻和暖如春狐粱,著一層夾襖步出監(jiān)牢的瞬間舀寓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工肌蜻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留互墓,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓宋欺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親胰伍。 傳聞我的和親對象是個殘疾皇子齿诞,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

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