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

2023-12-19  本文已影响0人  DarcyZhou

本文转载自: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机制上对应的授权结果为:

这几个值在源码定义处:

// 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()第一个参数是对应的位置权限值,第二个参数是之前权限弹窗设置的值。

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

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

权限管理3-5.png

8.总结

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

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

  2. 使用期间:

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

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

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

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

上一篇下一篇

猜你喜欢

热点阅读