Android許可權管理原理(含6.0)
前言
Android系統在MarshMallow之前,許可權都是在安裝的時候授予的,雖然在4.3時,Google就試圖在原始碼裡面引入AppOpsManager來達到動態控制權限的目的,但由於不太成熟,在Release版本中都是把這個功能給隱藏掉的。在6.0之後,Google為了簡化安裝流程且方便使用者控制權限,正式引入了runtime-permission,允許使用者在執行的時候動態控制權限。對於開發而言就是將targetSdkVersion設定為23,並且在相應的時機動態申請許可權,在適配了Android6.0的App執行在Android 6.0+的手機上時,就會呼叫6.0相關的API,不過在低版本的手機上,仍然是按安裝時許可權處理。
AppOpsManager動態許可權管理:官方預演的許可權管理
AppOpsManager是Google在Android4.3引入的動態許可權管理方式,不過,Google覺得不成熟,所以在每個發行版的時候,總是會將這個功能給遮蔽掉。該功能跟國內的許可權動態管理表現類似,這裡用CyanogenMod12裡面的實現講述一下,(國內的ROM原始碼拿不到,不過從表現來看,實現應該類似)。AppOpsManager實現的動態管理的本質是:將鑑權放在每個服務內部,比如,如果App要申請定位許可權,定位服務LocationManagerService會向AppOpsService查詢是否授予了App定位許可權,如果需要授權,就彈出一個系統對話方塊讓使用者操作,並根據使用者的操作將結果持久化在檔案中,如果在Setting裡設定了響應的許可權,也會去更新相應的許可權操作持久化檔案/data/system/appops.xml,下次再次申請服務的時候,服務會再次鑑定許可權。
舉個栗子-定位服務LocationManagerService: CM12原始碼
App在使用定位服務的時候,一般是通過LocationManager的requestLocationUpdates獲取定位,其實是通過Binder請求LocationManagerService去定位。
/android/location/LocationManager.java
private void requestLocationUpdates(LocationRequest request, LocationListener listener, Looper looper, PendingIntent intent)
{ ... try { mService.requestLocationUpdates(request, transport, intent, packageName); .../com/android/server/LocationManagerService.java
@Override public void requestLocationUpdates(LocationRequest request, ILocationListener listener, PendingIntent intent, String packageName) { if (request == null) request = DEFAULT_LOCATION_REQUEST; checkPackageName(packageName); <!--關鍵函式 1 ,查詢Manifest檔案,是否進行了許可權宣告 --> int allowedResolutionLevel = getCallerAllowedResolutionLevel(); checkResolutionLevelIsSufficientForProviderUse(allowedResolutionLevel, request.getProvider()); 。。。 <!--獲取呼叫app的pid跟uid--> final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); // providers may use public location API's, need to clear identity long identity = Binder.clearCallingIdentity(); try { <!--關鍵函式 2 檢查是否動態授權了許可權,或者拒絕了許可權--> checkLocationAccess(uid, packageName, allowedResolutionLevel); synchronized (mLock) { Receiver receiver = checkListenerOrIntentLocked(listener, intent, pid, uid, packageName, workSource, hideFromAppOps); if (receiver != null) { requestLocationUpdatesLocked(sanitizedRequest, receiver, pid, uid, packageName); } } } finally { Binder.restoreCallingIdentity(identity); } }
getCallerAllowedResolutionLevel主要通過呼叫getAllowedResolutionLevel查詢APP是否在Manifest中進行了宣告
private int getCallerAllowedResolutionLevel() {
return getAllowedResolutionLevel(Binder.getCallingPid(), Binder.getCallingUid());
}
private int getAllowedResolutionLevel(int pid, int uid) {
if (mContext.checkPermission(android.Manifest.permission.ACCESS_FINE_LOCATION,
pid, uid) == PackageManager.PERMISSION_GRANTED) {
return RESOLUTION_LEVEL_FINE;
} else if (mContext.checkPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION,
pid, uid) == PackageManager.PERMISSION_GRANTED) {
return RESOLUTION_LEVEL_COARSE;
} else {
return RESOLUTION_LEVEL_NONE;
}
}
checkLocationAccess這裡才是動態鑑權的入口,在checkLocationAccess函式中,會呼叫mAppOps.checkOp去鑑權,mAppOps就是AppOpsManager例項,
boolean checkLocationAccess(int uid, String packageName, int allowedResolutionLevel) {
int op = resolutionLevelToOp(allowedResolutionLevel);
if (op >= 0) {
int mode = mAppOps.checkOp(op, uid, packageName);
if (mode != AppOpsManager.MODE_ALLOWED && mode != AppOpsManager.MODE_ASK ) {
return false;
}
}
return true;
}
進而通過Binder向AppOpsService服務傳送鑑權請求
public int noteOp(int op, int uid, String packageName) {
try {
int mode = mService.noteOperation(op, uid, packageName);
if (mode == MODE_ERRORED) {
throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
}
return mode;
} catch (RemoteException e) {
}
return MODE_IGNORED;
}
AppOpsService負責動態許可權的鑑定跟更新,接著看noteOperation程式碼
@Override
public int noteOperation(int code, int uid, String packageName) {
final Result userDialogResult;
verifyIncomingUid(uid);
verifyIncomingOp(code);
synchronized (this) {
Ops ops = getOpsLocked(uid, packageName, true);
...
<!--關鍵點 1-->
if (switchOp.mode == AppOpsManager.MODE_IGNORED ||
switchOp.mode == AppOpsManager.MODE_ERRORED) {
op.rejectTime = System.currentTimeMillis();
op.ignoredCount++;
return switchOp.mode;
<!--關鍵點 2-->
} else if(switchOp.mode == AppOpsManager.MODE_ALLOWED) {
op.time = System.currentTimeMillis();
op.rejectTime = 0;
op.allowedCount++;
return AppOpsManager.MODE_ALLOWED;
} else {
op.noteOpCount++;
<!--關鍵函式 3-->
userDialogResult = askOperationLocked(code, uid, packageName,
switchOp);
}
}
return userDialogResult.get();
}
在上面的程式碼裡面,1、2是對已經處理過的場景直接返回已授權,或者已經拒絕,而3就是我們常見授權入口對話方塊,這裡是統一在AppOpsServie中進行授權處理的。askOperationLocked會顯示一個系統對話方塊,使用者選擇授權或者拒絕後,AppOpsServie會將選擇記錄在案,並通知申請服務提供或者拒絕服務。askOperationLocked通過mHandler傳送鑑權Message,看一下實現其實就是新建了一個PermissionDialog授權對話方塊,並且將AppOpsService的引用傳了進去,授權後會通過mService.notifyOperation通知授權結果。
mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PERMISSION_DIALOG: {
HashMap<String, Object> data =
(HashMap<String, Object>) msg.obj;
synchronized (this) {
Op op = (Op) data.get("op");
Result res = (Result) data.get("result");
op.dialogResult.register(res);
if(op.dialogResult.mDialog == null) {
Integer code = (Integer) data.get("code");
Integer uid = (Integer) data.get("uid");
String packageName =
(String) data.get("packageName");
Dialog d = new PermissionDialog(mContext,
AppOpsService.this, code, uid,
packageName);
op.dialogResult.mDialog = (PermissionDialog)d;
d.show();
}
}
}break;
}
}
};
Android發行版原始碼對於動態許可權管理的支援(幾乎為零)
在Android4.3到5.1之間,雖然App可以獲得AppOpsManager的例項,但是真正動態操作許可權的介面setMode卻被隱藏,如下
/** @hide */
public void setMode(int code, int uid, String packageName, int mode) {
try {
mService.setMode(code, uid, packageName, mode);
} catch (RemoteException e) {
}
}
遍歷原始碼也只有NotificationManagerService這個系統應用使用了setMode,也就是說發行版,只有通知是通過系統的通知管理進行動態管理的。
public void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) {
checkCallerIsSystem();
Slog.v(TAG, (enabled?"en":"dis") + "abling notifications for " + pkg);
mAppOps.setMode(AppOpsManager.OP_POST_NOTIFICATION, uid, pkg,
enabled ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
// Now, cancel any outstanding notifications that are part of a just-disabled app
if (ENABLE_BLOCKED_NOTIFICATIONS && !enabled) {
cancelAllNotificationsInt(pkg, 0, 0, true, UserHandle.getUserId(uid));
}
}
Android 6.0許可權管理原理
Android6.0的runtime-permission機制讓使用者在任何時候都可以取消授權,因此,每次在申請系統服務的時候,都要動態查詢是否獲取了相應的許可權,如果沒有獲取,就需要動態去申請,首先先看一下許可權的查詢:
Android6.0許可權查詢
support-v4相容包裡面提供了一個工具類PermissionChecker,可以用來檢查許可權獲取情況。
PermissionChecker
public static int checkPermission(@NonNull Context context, @NonNull String permission,
int pid, int uid, String packageName) {
if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) {
return PERMISSION_DENIED;
}
String op = AppOpsManagerCompat.permissionToOp(permission);
if (op == null) {
return PERMISSION_GRANTED;
}
if (packageName == null) {
String[] packageNames = context.getPackageManager().getPackagesForUid(uid);
if (packageNames == null || packageNames.length <= 0) {
return PERMISSION_DENIED;
}
packageName = packageNames[0];
}
if (AppOpsManagerCompat.noteProxyOp(context, op, packageName)
!= AppOpsManagerCompat.MODE_ALLOWED) {
return PERMISSION_DENIED_APP_OP;
}
return PERMISSION_GRANTED;
}
這裡我們只關心context.checkPermission,從上面對於4.3-5.1的APPOpsManager的分析,我們知道AppOpsManagerCompat本身的一些操作對於許可權管理並沒有實際意義,只是用來做一些標記,最多就是對於通知許可權有些用,接下來看checkPermission:
ContextImple.java
/** @hide */ @Override public int checkPermission(String permission, int pid, int uid, IBinder callerToken) { if (permission == null) { throw new IllegalArgumentException("permission is null"); } try { return ActivityManagerNative.getDefault().checkPermissionWithToken( permission, pid, uid, callerToken); } catch (RemoteException e) { return PackageManager.PERMISSION_DENIED; } }
接著往下看
ActivityManagerNative.java
public int checkPermission(String permission, int pid, int uid)
throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
data.writeString(permission);
data.writeInt(pid);
data.writeInt(uid);
mRemote.transact(CHECK_PERMISSION_TRANSACTION, data, reply, 0);
reply.readException();
int res = reply.readInt();
data.recycle();
reply.recycle();
return res;
}
ActivityManagerService
public int checkPermission(String permission, int pid, int uid) { if (permission == null) { return PackageManager.PERMISSION_DENIED; } return checkComponentPermission(permission, pid, UserHandle.getAppId(uid), -1, true); }
進而呼叫ActivityManager.checkComponentPermission,呼叫AppGlobals.getPackageManager().checkUidPermission(permission, uid);
ActivityManager.java
/** @hide */ public static int checkComponentPermission(String permission, int uid, int owningUid, boolean exported) { // Root, system server get to do everything. <!--root及System能獲取所有許可權--> if (uid == 0 || uid == Process.SYSTEM_UID) { return PackageManager.PERMISSION_GRANTED; } 。。。 <!--普通的許可權查詢--> try { return AppGlobals.getPackageManager() .checkUidPermission(permission, uid); } catch (RemoteException e) { // Should never happen, but if it does... deny! Slog.e(TAG, "PackageManager is dead?!?", e); } return PackageManager.PERMISSION_DENIED; }
最終呼叫PackageManagerService.java去檢視是否有許可權,到這裡,我們只需要知道許可權的查詢其實是通過PKMS來進行的。心裡先有個底,許可權的更新,持久化,恢復都是通過PKMS來進行的。
PKMS不同版本的許可權查詢
Android5.0的checkUidPermission
public int checkUidPermission(String permName, int uid) {
final boolean enforcedDefault = isPermissionEnforcedDefault(permName);
synchronized (mPackages) {
<!--PackageManagerService.Setting.mUserIds陣列中,根據uid查詢uid(也就是package)的許可權列表-->
Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
if (obj != null) {
GrantedPermissions gp = (GrantedPermissions)obj;
if (gp.grantedPermissions.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
} else {
<!--mSystemPermissions記錄一些系統級的應用的 uid 對應的 permission->
HashSet<String> perms = mSystemPermissions.get(uid);
if (perms != null && perms.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
}
if (!isPermissionEnforcedLocked(permName, enforcedDefault)) {
return PackageManager.PERMISSION_GRANTED;
}
}
return PackageManager.PERMISSION_DENIED;
}
Android6.0+的checkUidPermission
@Override
public int checkUidPermission(String permName, int uid) {
final int userId = UserHandle.getUserId(uid);
if (!sUserManager.exists(userId)) {
return PackageManager.PERMISSION_DENIED;
}
synchronized (mPackages) {
Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
if (obj != null) {
final SettingBase ps = (SettingBase) obj;
final PermissionsState permissionsState = ps.getPermissionsState();
if (permissionsState.hasPermission(permName, userId)) {
return PackageManager.PERMISSION_GRANTED;
}
// Special case: ACCESS_FINE_LOCATION permission includes ACCESS_COARSE_LOCATION
if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(permName) && permissionsState
.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION, userId)) {
return PackageManager.PERMISSION_GRANTED;
}
} else {
ArraySet<String> perms = mSystemPermissions.get(uid);
if (perms != null) {
if (perms.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(permName) && perms
.contains(Manifest.permission.ACCESS_FINE_LOCATION)) {
return PackageManager.PERMISSION_GRANTED;
}
}
}
}
return PackageManager.PERMISSION_DENIED;
}
可以看到Android6.0之後,對許可權的操作是PermissionsState
PermissionsState.java (android-6.0frameworksbaseservicescorejavacomandroidserverpm)
public boolean hasPermission(String name, int userId) { enforceValidUserId(userId); if (mPermissions == null) { return false; } PermissionData permissionData = mPermissions.get(name); return permissionData != null && permissionData.isGranted(userId); }
從上面的程式碼可以很清晰看出,6.0之後,除了聲明瞭許可權之外,還必須是授權了的。執行時許可權跟install許可權有所不同,對於install許可權isGranted一直返回是True,這裡先不必深究PermissionsState是怎麼存進記憶體,先記住,後面會將講。
Android6.0動態申請許可權
申請許可權可以通過V4包裡面的ActivityCompat,它已經對不同版本做了相容
ActivityCompat.java
public static void requestPermissions(final @NonNull Activity activity,
final @NonNull String[] permissions, final int requestCode) {
if (Build.VERSION.SDK_INT >= 23) {
ActivityCompatApi23.requestPermissions(activity, permissions, requestCode);
} else if (activity instanceof OnRequestPermissionsResultCallback) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
final int[] grantResults = new int[permissions.length];
PackageManager packageManager = activity.getPackageManager();
String packageName = activity.getPackageName();
final int permissionCount = permissions.length;
for (int i = 0; i < permissionCount; i++) {
grantResults[i] = packageManager.checkPermission(
permissions[i], packageName);
}
((OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult(
requestCode, permissions, grantResults);
}
});
}
}
可以看到,如果是6.0以下,直接通過PKMS查詢是否在Manifest裡面申請了許可權,並把查詢結果通過onRequestPermissionsResult回撥傳給Activity或者Fragment。其實這裡只要在Manifest中聲明瞭,就會預設是Granted。接著往下看:ActivityCompatApi23最終會呼叫activity.requestPermissions去請求許可權。
Activity
public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
}
Intent其實是通過PackageManager(ApplicationPackageManager實現類)獲取的Intent
public Intent buildRequestPermissionsIntent(@NonNull String[] permissions) {
if (ArrayUtils.isEmpty(permissions)) {
throw new NullPointerException("permission cannot be null or empty");
}
Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
intent.putExtra(EXTRA_REQUEST_PERMISSIONS_NAMES, permissions);
intent.setPackage(getPermissionControllerPackageName());
return intent;
}
這裡首先是隱式的獲取授權Activity元件相關資訊(GrantPermissionsActivity),其實就是對話方塊樣式的授權Activity,它是PackageInstaller系統應用裡面的一個Activity。這裡的getPermissionControllerPackageName其實就是獲取相應的包名,
ApplicationPackageManager.java (android-6.0frameworksbasecorejavaandroidapp)
@Override
public String getPermissionControllerPackageName() {
synchronized (mLock) {
if (mPermissionsControllerPackageName == null) {
try {
mPermissionsControllerPackageName = mPM.getPermissionControllerPackageName();
} catch (RemoteException e) {
throw