美文网首页
Android 权限管理--03:后台定位权限源码分析

Android 权限管理--03:后台定位权限源码分析

作者: DarcyZhou | 来源:发表于2023-12-19 08:44 被阅读0次

    本文转载自:Android Framework权限篇三之后台定位权限源码分析

    本文基于Android 10.0源码分析

    1.概述

      在android 10.0上新增了后台定位权限,控制应用退到后台无法访问位置,主要适配可以参考这篇文章: 《android定位权限适配看这篇就够了》今天这篇文章不是讲如何适配,而是基于android 10.0源码分析Framework侧是如何作用的;进而讲到权限机制里的一个重要角色AppOps。

    2.AppOps介绍

      首先,先介绍下AppOps机制,在Android中,已经有一套Android Runtime运行时权限机制,对应Framework中的PermissionManager和PermissionService服务;其实还有一套AppOpsManager和AppOpsService服务,关于这个服务官方的介绍是如下:App-ops提供两个用途:一个是访问控制,这个具体是和runtime运行时权限配合;这个在下篇文章介绍;一个是跟踪,这里翻译是电量耗电跟踪,我自己个人的理解是和电量耗电相关的权限,比如定位权限,本篇文章介绍的就是应用退到后台以后,如何通过Appops机制跟踪限制后台使用定位。

        /**
         * App-ops are used for two purposes: Access control and tracking.
         *
         * <p>App-ops cover a wide variety of functionality from helping with runtime permissions access
         * control and tracking to battery consumption tracking.
    

    3.后台位置权限

    权限管理3-1.png

    在android 10.0上,权限新增了后台位置权限,需要额外申请ACCESS_BACKGROUND_LOCATION权限,此时权限弹窗会展示始终允许和使用期间这两个选项;选择相应选项相应权限会授权;这里除了运行时权限会授权外,AppOps权限机制也会授权,这里定位权限在AppOps机制上对应的授权结果为:

    • 始终允许:MODE_ALLOWED

    • 使用期间:MODE_FOREGROUND

    • 拒绝:MODE_IGNORED

    这几个值在源码定义处:

    // frameworks/base/core/java/android/app/AppOpsManager.java
    public static final int MODE_ALLOWED = 0;
    public static final int MODE_IGNORED = 1;
    public static final int MODE_ERRORED = 2;
    public static final int MODE_DEFAULT = 3;
    public static final int MODE_FOREGROUND = 4;
    

    4.获取位置

      在Android设备上,获取设备位置,具体可以看下这篇博客:《Android获取位置》,接下来从获取设备位置API去推怎么限制应用退到后台访问位置。

    private LocationManager locationManager;
    locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
    //获取Location
    Location location = locationManager.getLastKnownLocation(locationProvider);
    

    可以看到这里通过LocationManager调用下来,最后会调用到LocationManagerService.getLastLocation()在这里面会去检查权限是否授权,否则返回为空。

    // frameworks/base/services/core/java/com/android/server/LocationManagerService.java
    @Override
    public Location getLastLocation(LocationRequest r, String packageName) {
          ...
          // Don't report location access if there is no last location to deliver.
                if (lastLocation != null) {
                    //这里会检查权限,如果不通过则拿到的location是null的
                    if (!reportLocationAccessNoThrow(
                            pid, uid, packageName, allowedResolutionLevel)) {
                        if (D) {
                            Log.d(TAG, "not returning last loc for no op app: " + packageName);
                        }
                        lastLocation =  null;
                    }
                }
                return lastLocation;
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
    }
    

    这里看下是怎么检查权限的:

    // frameworks/base/services/core/java/com/android/server/LocationManagerService.java
        private boolean reportLocationAccessNoThrow(
                int pid, int uid, String packageName, int allowedResolutionLevel) {
            //这里先做个转换得到FINE/CORSE对应的op值
            int op = resolutionLevelToOp(allowedResolutionLevel);
            if (op >= 0) {
                //这里判断如果值不等于MODE_ALLOWED则会返回false
                if (mAppOps.noteOpNoThrow(op, uid, packageName) != AppOpsManager.MODE_ALLOWED) {
                    return false;
                }
            }
    
            return getAllowedResolutionLevel(pid, uid) >= allowedResolutionLevel;
        }
    

    这里(AppOpsManager)mAppOps.noteOpNoThrow() -> AppOpsService.noteOperationUnchecked(),如下代码的第一点和第二点是关键,展开讨论之前,我们先看下应用在退到前后台的时候怎么更新状态。

    private int noteOperationUnchecked(int code, int uid, String packageName,
              int proxyUid, String proxyPackageName, @OpFlags int flags) {
          synchronized (this) {
              //1.getOpsRawLocked()这里会去查询并更新前后台状态
              final Ops ops = getOpsRawLocked(uid, packageName, true /* edit */,
                      false /* uidMismatchExpected */);
              ...
              final Op op = getOpLocked(ops, code, true);
              ...
              final UidState uidState = ops.uidState;
              ...
              final int switchCode = AppOpsManager.opToSwitch(code);
              // If there is a non-default per UID policy (we set UID op mode only if
              // non-default) it takes over, otherwise use the per package policy.
              if (uidState.opModes != null && uidState.opModes.indexOfKey(switchCode) >= 0) {
                  //2.UidState.evalMode()这里会得到结果MODE_ALLOWED或者MODE_IGNORED; 
                  final int uidMode = uidState.evalMode(code, uidState.opModes.get(switchCode));
                  ...
    

    5.应用前后台状态更新

    1. 切换应用前后台时会走ActivityManagerService.noteUidProcessState()-> mAppOpsService.updateUidProcState(uid, state);

    2. 这里通过AppOpsService更新对应uid的应用进程前后台状态;

    3. 如从前台切到后台,前台状态是200,后台状态是700;

    4. 这里是关于状态码的定义,主要关注下200-对应应用在前台。

    // frameworks/base/core/java/android/app/AppOpsManager.java
         /**
          * Uid state: The UID is top foreground app. The lower the UID
          * state the more important the UID is for the user.
          * @hide
          */
         @TestApi
         @SystemApi
         public static final int UID_STATE_TOP = 200;
    
         /**
          * Uid state: The UID is running a foreground service of location type.
          * The lower the UID state the more important the UID is for the user.
          * @hide
          */
         @TestApi
         @SystemApi
         public static final int UID_STATE_FOREGROUND_SERVICE_LOCATION = 300;
    
         /**
          * Uid state: The UID is running a foreground service. The lower the UID
          * state the more important the UID is for the user.
          * @hide
          */
         @TestApi
         @SystemApi
         public static final int UID_STATE_FOREGROUND_SERVICE = 400;
    
         /**
          * The max, which is min priority, UID state for which any app op
          * would be considered as performed in the foreground.
          * @hide
          */
         public static final int UID_STATE_MAX_LAST_NON_RESTRICTED = UID_STATE_FOREGROUND_SERVICE;
    
         /**
          * Uid state: The UID is a foreground app. The lower the UID
          * state the more important the UID is for the user.
          * @hide
          */
         @TestApi
         @SystemApi
         public static final int UID_STATE_FOREGROUND = 500;
    
         /**
          * Uid state: The UID is a background app. The lower the UID
          * state the more important the UID is for the user.
          * @hide
          */
         @TestApi
         @SystemApi
         public static final int UID_STATE_BACKGROUND = 600;
    
         /**
          * Uid state: The UID is a cached app. The lower the UID
          * state the more important the UID is for the user.
          * @hide
          */
         @TestApi
         @SystemApi
         public static final int UID_STATE_CACHED = 700;
    
    public void updateUidProcState(int uid, int procState) {
        synchronized (this) {
            final UidState uidState = getUidStateLocked(uid, true);
            int newState = PROCESS_STATE_TO_UID_STATE[procState];
            if (uidState != null && uidState.pendingState != newState) {
                final int oldPendingState = uidState.pendingState;
                //设置对应uid的进程uidState的pendingState,state为当前状态,pendingState为即将变成的状态
                uidState.pendingState = newState;
                //如果从后台切到前台,是200<700,则直接通过commitUidPendingStateLocked()更新进程前后台状态信息并且pendingStateCommitTime = 0;
                if (newState < uidState.state
                        || (newState <= UID_STATE_MAX_LAST_NON_RESTRICTED
                                && uidState.state > UID_STATE_MAX_LAST_NON_RESTRICTED)) {
                    // We are moving to a more important state, or the new state may be in the
                    // foreground and the old state is in the background, then always do it
                    // immediately.
                    commitUidPendingStateLocked(uidState);
                } else if (uidState.pendingStateCommitTime == 0) {
                    // We are moving to a less important state for the first time,
                    // delay the application for a bit.
                    final long settleTime;
                    //如果是从前台切到后台,则这里断点看了下设置settleTime为30000即30s,这里会延迟30s获取到的进程状态才会更新成后台,主要是通过pendingStateCommitTime来判断
                    if (uidState.state <= UID_STATE_TOP) {
                        settleTime = mConstants.TOP_STATE_SETTLE_TIME;
                    } else if (uidState.state <= UID_STATE_FOREGROUND_SERVICE) {
                        settleTime = mConstants.FG_SERVICE_STATE_SETTLE_TIME;
                    } else {
                        settleTime = mConstants.BG_STATE_SETTLE_TIME;
                    }
                    uidState.pendingStateCommitTime = SystemClock.elapsedRealtime() + settleTime;
                    BinderCallCacheAgent.removeCheckPackageBinderCache(uid);
                }
    ...
    
    //1\. 如上后台切前台,则进入if (newState < uidState.state...
    //会调用commitUidPendingStateLocked();
    private void commitUidPendingStateLocked(UidState uidState) {
    
        ...
        //即将变成的状态pendingState变成了当前的状态
        uidState.state = uidState.pendingState;
        uidState.pendingStateCommitTime = 0;
    }
    //2\. 此时如果是从前台切后台,则700<200不满足,
    //是进入else if (uidState.pendingStateCommitTime == 0) {
    //此时设置uidState.pendingStateCommitTime为当前时间加30s延时
    

    6.getOpsRawLocked()

      这里继上面的获取位置的两个关键点展开讲,第一个:getOpsRawLocked()这里会去查询并更新前后台状态;看下这个方法:

    权限管理3-2.png
    private @Nullable UidState getUidStateLocked(int uid, boolean edit) {
    
        UidState uidState = mUidStates.get(uid);
        if (uidState == null) {
            if (!edit) {
                return null;
            }
            uidState = new UidState(uid);
            mUidStates.put(uid, uidState);
        } else {
            //这里获取uidState状态前会判断是否更新state,如果当前时间是超过了之前设置的pendingStateCommitTime,则会更新,这里断点看是30s
            if (uidState.pendingStateCommitTime != 0) {
                if (uidState.pendingStateCommitTime < mLastRealtime) {
                    commitUidPendingStateLocked(uidState);
                } else {
                    mLastRealtime = SystemClock.elapsedRealtime();
                    if (uidState.pendingStateCommitTime < mLastRealtime) {
                        commitUidPendingStateLocked(uidState);
                    }
                }
            }
        }
        return uidState;
    }
    

    这里和上面的uidState.pendingStateCommitTime的值串起来了,前面从前台切到后台,设置的pendingStateCommitTime是30s,也就是说之前从前台切到后台,则需要30s之后状态才会更新为后台状态;如果是后台切到前台,则是马上调用commitUidPendingStateLocked()即时更新,看官方解释原因如下:

            if (newState < uidState.state
                    || (newState <= UID_STATE_MAX_LAST_NON_RESTRICTED
                            && uidState.state > UID_STATE_MAX_LAST_NON_RESTRICTED)) {
                //后台->前台,认为是变化到一个更重要的状态,所以需要即时更新
                // We are moving to a more important state, or the new state may be in the
                // foreground and the old state is in the background, then always do it
                // immediately.
                commitUidPendingStateLocked(uidState);
            } else if (uidState.pendingStateCommitTime == 0) {
                //前台->后台,认为是变化到不那么重要的状态,所以可以延迟更新
                // We are moving to a less important state for the first time,
                // delay the application for a bit.
                final long settleTime;
                if (uidState.state <= UID_STATE_TOP) {
                    settleTime = mConstants.TOP_STATE_SETTLE_TIME;
    

    7.UidState.evalMode()

      继上面的第二个关键点讲,evalMode()。

        //2.UidState.evalMode()这里会得到结果MODE_ALLOWED或者MODE_IGNORED; 
        final int uidMode = uidState.evalMode(code, uidState.opModes.get(switchCode));
    

    这个方法加断点可以看到,evalMode()第一个参数是对应的位置权限值,第二个参数是之前权限弹窗设置的值。

    • 如果之前设置的是使用期间对应AppOpsManager.MODE_FOREGROUND,则需要判断state状态,这个state指的是当前的前后台状态,如果是前台200<=300,则返回MODE_ALLOWED,允许访问位置;如果是后台700<=300不成立,则返回MODE_IGNORED,不允许访问位置;

    • 如果之前设置的是始终允许对应,则不需要检查state前台状态,直接返回允许或拒绝。

    权限管理3-3.png 权限管理3-4.png

    这里也做个引申解释下为什么下面这种方式能做到,原因是使用后台定位服务则当前应用进程状态是300,如上判断300<=300,所以返回MODE_ALLOWED,允许访问位置。

    权限管理3-5.png

    8.总结

      最后,结合前面第一个和第二个关键点来总结下结论:

    1. 始终允许:应用前后台可以一直访问位置

    2. 使用期间:

      1. 后台切到前台,状态即时更新,前台可以访问位置;

      2. 前台切到后台,状态会延时30s更新,所以在30s内应用还可以在后台访问位置,30s之后应用在后台无法访问位置。

      这里举个具体的例子:在小米手机上,使用高德地图或者百度地图,位置权限选择使用期间,然后导航具体某个地点,将应用退到后台,左上角会有个蓝色的使用位置提醒,提示高德地图当前正在后台使用位置,等30s之后,这个位置提醒会消失,表示当前没有应用正在使用位置。

      另外,这里的应用敏感行为实现是怎么做的?将在另一篇文章单独介绍

    相关文章

      网友评论

          本文标题:Android 权限管理--03:后台定位权限源码分析

          本文链接:https://www.haomeiwen.com/subject/ocuhidtx.html