从 蓝牙连接 权限检查入手 分析 权限检查机制

Android 12(API 31) 的蓝牙权限做了个更改.

2021-7-20 · Android 12 引入了 BLUETOOTH_SCAN 、 BLUETOOTH_ADVERTISE 和 BLUETOOTH_CONNECT 权限,
可让应用 扫描附近的设备(NearBy),而无需请求位置权限(ACCESS_FINE_LOCATION).

参考: https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
(1)如果您的应用程序寻找蓝牙设备,例如 BLE 外围设备,请声明 BLUETOOTH_SCAN 权限。
(2)如果您的应用程序使当前设备可被其他蓝牙设备发现,请声明该 BLUETOOTH_ADVERTISE 权限。
(3)如果您的应用程序与已配对的蓝牙设备通信,请声明 BLUETOOTH_CONNECT 权限。
(4)对于遗留的蓝牙相关权限声明,设置 android:maxSdkVersion为30. 此应用兼容性步骤有助于系统仅向您的应用授予安装在运行 Android 12 或更高版本的设备上时所需的蓝牙权限。
具体使用方法参考指南。

由于这三种蓝牙权限都是 运行时权限,必须 先在应用中 明确请求 用户同意,然后才能查找蓝牙设备.
因此,就产生了 蓝牙连接 权限检查问题,我们从这个角度(BLUETOOTH_CONNECT) 入手, 分析 权限检查机制.

注:(1)在 targetSKD <=30 的应用,是无法声明 上面三个权限的,仅有以下几个( Nearby 附近的设备 权限默认开启???)
<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 附近的设备 权限是为了兼容 旧版本Android 系统(旧SDK),所以在SDK<=30默认开启 ???
因此,targetSKD <=30 的应用, 在API31(Android12) 的设备上必须要有这个权限才能使用蓝牙 ??
否则会抛出 安全异常 错误?? 这是因为 AppOpsManager 在
系统层面**还会限制??
(3) targetSDK <=30, 无法使用 checkSelfPermission() 检查 BLUETOOTH_CONNECT 等API31才有的权限.

1. Framework 蓝牙模块

客户端(例如三方应用),想要使用到蓝牙,需要调用 系统蓝牙接口 获取 蓝牙服务.

这里会使用 AIDL 获取 已绑定的 蓝牙服务,
即从系统 Framework 蓝牙 转到了 蓝牙app 进程,
可以看出,也存在 蓝牙应用 作为 服务提供者.

// /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 最终的是实现,是在 蓝牙应用里.
可以看出,是在 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()会进行调用者权限的检查, 信息封装在 attributionSource

2.1 内部权限检查

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

会调用它的内部方法:checkPermissionForDataDelivery, 并传入权限为 BLUETOOTH_CONNECT 即 蓝牙连接 的权限


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

这里又调用了 PermissionChecker.checkPermissionForDataDeliveryFromDataSource(), 根据返回结果处理.
(1) 返回 PERMISSION_GRANTED, 则结果返回true,代表 调用者 拥有了蓝牙权限
(2) 返回 PERMISSION_HARD_DENIED, 则会抛出 SecurityException, 并提示 需要 XXX 的权限.
(3) 返回 其它值(只有PERMISSION_SOFT_DENIED这种), 则会返回false ,表示没有 相应的蓝牙权限.

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) 会调用内部方法 checkPermissionForDataDeliveryCommon, 并fromDatasource 参数设置为true传入
(2) 进一步委托给 PermissionCheckerManager, 它也是一个系统服务,在 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, 因此它还是调用 Context.checkPermission 进行检查.
(2) Context.checkPermission 是一个抽象方法, 最终由 ContextImpl 实现.(查看最初传递的 service, 即 AdapterService 对象 )
它需要三个参数,第一个是权限名称,第二个是pid, 第三个是 uid, pid/uid 均由attributionSource提供
(3) AppOpsManager 是谷歌原生的 应用操作(权限) 管理, PermissionManager 也是基于这个实现的(具体参考另外一篇)

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

可见,它会根据 权限名称/pid / uid, 再进行委托给 PermissionManager 去检查.

而 PermissonManager 将会再后续单独介绍.

3. 小结

到此, 我们可以初步得出一个结论:
(1) 应用使用某个 需要权限的功能时, 系统(Framework)会检查 应用有没有这个 权限,没有则抛出异常.
(2) 应用的 pid/uid 信息, 封装在 AttributionSource 对象里。
(3) 委托 PermissionCheckerManager 进行检查, 这里将 pid/uid 取出来, 再调用 Context 进行检查.
(4) 这个 Context 其实是 AdapterService 对象,类推可以是我们应用 中的 Activity、Service 、Application
(5) Context 里又会进行 委托 PermissionManager 进行 真正的检查.

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容