Android 权限管理--03:后台定位权限源码分析
本文基于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.应用前后台状态更新
-
切换应用前后台时会走ActivityManagerService.noteUidProcessState()-> mAppOpsService.updateUidProcState(uid, state);
-
这里通过AppOpsService更新对应uid的应用进程前后台状态;
-
如从前台切到后台,前台状态是200,后台状态是700;
-
这里是关于状态码的定义,主要关注下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.pngprivate @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前台状态,直接返回允许或拒绝。
这里也做个引申解释下为什么下面这种方式能做到,原因是使用后台定位服务则当前应用进程状态是300,如上判断300<=300,所以返回MODE_ALLOWED,允许访问位置。
权限管理3-5.png8.总结
最后,结合前面第一个和第二个关键点来总结下结论:
-
始终允许:应用前后台可以一直访问位置
-
使用期间:
-
后台切到前台,状态即时更新,前台可以访问位置;
-
前台切到后台,状态会延时30s更新,所以在30s内应用还可以在后台访问位置,30s之后应用在后台无法访问位置。
-
这里举个具体的例子:在小米手机上,使用高德地图或者百度地图,位置权限选择使用期间,然后导航具体某个地点,将应用退到后台,左上角会有个蓝色的使用位置提醒,提示高德地图当前正在后台使用位置,等30s之后,这个位置提醒会消失,表示当前没有应用正在使用位置。
另外,这里的应用敏感行为实现是怎么做的?将在另一篇文章单独介绍