1. 程式人生 > >android M 之前應用許可權和M 之後的應用許可權控制

android M 之前應用許可權和M 之後的應用許可權控制

轉載標明出處:
https://blog.csdn.net/shift_wwx/article/details/80674768

相關資源:


前言:

最近搞許可權控制的流程,從Android Runtime Permission 詳解 中瞭解到android 系統在M 之後對於許可權的控制是不一樣的流程。那對於M 之前的apk 當然沒有M 之後系統中的介面,例如checkPermission。在M 之後的應用中都會通過checkPermission 的介面來確認許可權是否是granted,M 之前的應用無法使用checkPermission,系統為此將這一部分的流程控制加到了AppOps 中。本文主要通過原始碼來分析M 系統中對於M 之前app 許可權控制預留的流程。

原始碼解析:

首先來看startActivity:
    @Override
    public final int startActivity(IApplicationThread caller, String callingPackage,
            Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
            int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) {
        return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
                resultWho, requestCode, startFlags, profilerInfo, bOptions,
                UserHandle.getCallingUserId());
    }
接著會呼叫startActivityAsUser,後面的流程原始碼不貼出來了(佔用空間)。
最後會呼叫到ActivityStarter 中的startActivity:
    /** DO NOT call this method directly. Use {@link #startActivityLocked} instead. */
    private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
            String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
            String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
            ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified,
            ActivityRecord[] outActivity, TaskRecord inTask) {

        ...
        ...

        boolean abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
                requestCode, callingPid, callingUid, callingPackage, ignoreTargetSecurity, callerApp,
                resultRecord, resultStack, options);
        abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
                callingPid, resolvedType, aInfo.applicationInfo);
				
        ...
        ...
    }

在這裡會對啟動的任何activity進行許可權的訪問,確認是否要abort,那一般對於M 之後的應用一般都是allowed,這個後面會解釋。接著來看checkStartAnyActivityPermission:

boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo,
            String resultWho, int requestCode, int callingPid, int callingUid,
            String callingPackage, boolean ignoreTargetSecurity, ProcessRecord callerApp,
            ActivityRecord resultRecord, ActivityStack resultStack, ActivityOptions options) {
        final int startAnyPerm = mService.checkPermission(START_ANY_ACTIVITY, callingPid,
                callingUid); //check step 1
        if (startAnyPerm == PERMISSION_GRANTED) {
            return true;
        }
        final int componentRestriction = getComponentRestrictionForCallingPackage(
                aInfo, callingPackage, callingPid, callingUid, ignoreTargetSecurity); //check step 2
        final int actionRestriction = getActionRestrictionForCallingPackage(
                intent.getAction(), callingPackage, callingPid, callingUid); //check step 3
        if (componentRestriction == ACTIVITY_RESTRICTION_PERMISSION
                || actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
            if (resultRecord != null) {
                resultStack.sendActivityResultLocked(-1,
                        resultRecord, resultWho, requestCode,
                        Activity.RESULT_CANCELED, null);
            }
            final String msg;
            if (actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
                msg = "Permission Denial: starting " + intent.toString()
                        + " from " + callerApp + " (pid=" + callingPid
                        + ", uid=" + callingUid + ")" + " with revoked permission "
                        + ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction());
            } else if (!aInfo.exported) {
                msg = "Permission Denial: starting " + intent.toString()
                        + " from " + callerApp + " (pid=" + callingPid
                        + ", uid=" + callingUid + ")"
                        + " not exported from uid " + aInfo.applicationInfo.uid;
            } else {
                msg = "Permission Denial: starting " + intent.toString()
                        + " from " + callerApp + " (pid=" + callingPid
                        + ", uid=" + callingUid + ")"
                        + " requires " + aInfo.permission;
            }
            Slog.w(TAG, msg);
            throw new SecurityException(msg); //throw an exception for denied state
        }

        if (actionRestriction == ACTIVITY_RESTRICTION_APPOP) {
            final String message = "Appop Denial: starting " + intent.toString()
                    + " from " + callerApp + " (pid=" + callingPid
                    + ", uid=" + callingUid + ")"
                    + " requires " + AppOpsManager.permissionToOp(
                            ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction()));
            Slog.w(TAG, message);
            return false;
        } else if (componentRestriction == ACTIVITY_RESTRICTION_APPOP) {
            final String message = "Appop Denial: starting " + intent.toString()
                    + " from " + callerApp + " (pid=" + callingPid
                    + ", uid=" + callingUid + ")"
                    + " requires appop " + AppOpsManager.permissionToOp(aInfo.permission);
            Slog.w(TAG, message);
            return false;
        }
        if (options != null) {
            if (options.getLaunchTaskId() != INVALID_STACK_ID) {
                final int startInTaskPerm = mService.checkPermission(START_TASKS_FROM_RECENTS,
                        callingPid, callingUid);
                if (startInTaskPerm == PERMISSION_DENIED) {
                    final String msg = "Permission Denial: starting " + intent.toString()
                            + " from " + callerApp + " (pid=" + callingPid
                            + ", uid=" + callingUid + ") with launchTaskId="
                            + options.getLaunchTaskId();
                    Slog.w(TAG, msg);
                    throw new SecurityException(msg);
                }
            }
            // Check if someone tries to launch an activity on a private display with a different
            // owner.
            final int launchDisplayId = options.getLaunchDisplayId();
            if (launchDisplayId != INVALID_DISPLAY && !isCallerAllowedToLaunchOnDisplay(callingPid,
                    callingUid, launchDisplayId, aInfo)) {
                final String msg = "Permission Denial: starting " + intent.toString()
                        + " from " + callerApp + " (pid=" + callingPid
                        + ", uid=" + callingUid + ") with launchDisplayId="
                        + launchDisplayId;
                Slog.w(TAG, msg);
                throw new SecurityException(msg);
            }
        }

        return true;
    }

這段code 比較簡單,通過三個check 函式,確定狀態是否正常,不正常直接會給出securityException,這樣就要求check 的返回狀態必須正常。

1、mService.checkPermission(START_ANY_ACTIVITY, callingPid, callingUid);

注意這裡的許可權是START_ANY_ACTIVITY:

    <!-- Allows an application to start any activity, regardless of permission
         protection or exported state.
         @hide -->
    <permission android:name="android.permission.START_ANY_ACTIVITY"
        android:protectionLevel="signature" />

註冊這個許可權的應用可以啟動任何的activity,如果有這個許可權,那麼其他的許可權管理就形同虛設了。

2、getComponentRestrictionForCallingPackage()
    private int getComponentRestrictionForCallingPackage(ActivityInfo activityInfo,
            String callingPackage, int callingPid, int callingUid, boolean ignoreTargetSecurity) {
        if (!ignoreTargetSecurity && mService.checkComponentPermission(activityInfo.permission,
                callingPid, callingUid, activityInfo.applicationInfo.uid, activityInfo.exported)
                == PERMISSION_DENIED) {
            return ACTIVITY_RESTRICTION_PERMISSION;
        }

        if (activityInfo.permission == null) {
            return ACTIVITY_RESTRICTION_NONE;
        }

        final int opCode = AppOpsManager.permissionToOpCode(activityInfo.permission);
        if (opCode == AppOpsManager.OP_NONE) {
            return ACTIVITY_RESTRICTION_NONE;
        }

        if (mService.mAppOpsService.noteOperation(opCode, callingUid,
                callingPackage) != AppOpsManager.MODE_ALLOWED) {
            if (!ignoreTargetSecurity) {
                return ACTIVITY_RESTRICTION_APPOP;
            }
        }

        return ACTIVITY_RESTRICTION_NONE;
    }

最開始是通過M 之後的checkPermission 介面檢測,如果這裡都過不了,那肯定無需進AppOps 了。

這裡可以看到對於給出的許可權,例如PHONE_CALLS,都會經過noteOperation 的操作,不管是否是M 之前還是之後,唯一的區別可能就是在裡面的邏輯控制了。如果這裡不過的話,會給出ACTIVITY_RESTRICTION_APPOP 的返回,以後的處理邏輯這裡不多說了,程式碼很清晰。

3、getActionRestrictionForCallingPackage()
    private int getActionRestrictionForCallingPackage(String action,
            String callingPackage, int callingPid, int callingUid) {
        if (action == null) {
            return ACTIVITY_RESTRICTION_NONE;
        }

        String permission = ACTION_TO_RUNTIME_PERMISSION.get(action);
        if (permission == null) {
            return ACTIVITY_RESTRICTION_NONE;
        }

        final PackageInfo packageInfo;
        try {
            packageInfo = mService.mContext.getPackageManager()
                    .getPackageInfo(callingPackage, PackageManager.GET_PERMISSIONS);
        } catch (PackageManager.NameNotFoundException e) {
            Slog.i(TAG, "Cannot find package info for " + callingPackage);
            return ACTIVITY_RESTRICTION_NONE;
        }

        if (!ArrayUtils.contains(packageInfo.requestedPermissions, permission)) {
            return ACTIVITY_RESTRICTION_NONE;
        }

        if (mService.checkPermission(permission, callingPid, callingUid) == PERMISSION_DENIED) {
            return ACTIVITY_RESTRICTION_PERMISSION;
        }

        final int opCode = AppOpsManager.permissionToOpCode(permission);
        if (opCode == AppOpsManager.OP_NONE) {
            return ACTIVITY_RESTRICTION_NONE;
        }

        if (mService.mAppOpsService.noteOperation(opCode, callingUid,
                callingPackage) != AppOpsManager.MODE_ALLOWED) {
            return ACTIVITY_RESTRICTION_APPOP;
        }

        return ACTIVITY_RESTRICTION_NONE;
    }

這裡是通過action 來確認permission,然後通過permission 繼續進行M 之後的介面checkPermission 和AppOps 的驗證。

總結:

這裡只是列舉了android M 之後的系統在startActivity 的時候對於許可權管理的控制流程,對於M 之前的應用,一般checkPemission 都是給的granted,至於這個預設值如果想知道的可以看PMS 或者給我留言。那如果對於M 之前的應用,想要許可權控制,而且不改變原來系統控制流程,只能在AppOps 中控制。

當然,除了這裡的startActivity有許可權的控制,還有其他的,例如ContentProvider。

        @Override
        public int update(String callingPkg, Uri uri, ContentValues values, String selection,
                String[] selectionArgs) {
            validateIncomingUri(uri);
            uri = maybeGetUriWithoutUserId(uri);
            if (enforceWritePermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
                return 0;
            }
            final String original = setCallingPackage(callingPkg);
            try {
                return ContentProvider.this.update(uri, values, selection, selectionArgs);
            } finally {
                setCallingPackage(original);
            }
        }

對話方塊提示

通過android GrantPermissionsActivity 詳解,M 之後的應用中都會有對話方塊彈出,那M 之前的應用彈框提示只能在AppOps中控制(之前在AMS 中的checkPermission 中控制,但是發現會一堆死鎖,修改的code 最後貼出)。

由於一些原因,這裡只貼一部分code:

if (uidState.opModes != null && uidState.opModes.indexOfKey(switchCode) >= 0) {
    final int uidMode = uidState.opModes.get(switchCode);
    if (uidMode != AppOpsManager.MODE_ALLOWED) {
        if (DEBUG) Log.d(TAG, "noteOperation: (uidState)reject #" + op.mode + " for code "
	    + switchCode + " (" + code + ") uid " + uid + " package "
	    + packageName);
        op.rejectTime = System.currentTimeMillis();
        final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op;
        req = enforcePermission(code, uid, packageName, switchOp);
        if (req == null)
	    return uidMode;
        isAllowed = false;
    }
} 
    private PermissionDialogReq enforcePermission(int code, int uid, String packageName, Op op) {
        if (!isStrictOpEnable()
            || !isStrictCodeValid(code)
            || Looper.myLooper() == mLooper
            || op.mode != AppOpsManager.MODE_NOTED)
            return null;

        return askOperationLocked(code, uid, packageName, op);
    }

    private boolean isStrictCodeValid(int code) {
        return code == AppOpsManager.OP_CHANGE_WIFI_STATE
            || code == AppOpsManager.OP_BLUETOOTH_ADMIN
            || code == AppOpsManager.OP_NFC
            || code == AppOpsManager.OP_SEND_MMS
            || isRuntimePermission(code);
    }

    /**
     * added by wj, just adjust the permission is runtime permission or not
     * for the application whose SDK version is less than M.
     */
    private boolean isRuntimePermission(int code) {
        String permName = AppOpsManager.opToPermission(code);

        PermissionInfo permissionInfo;
        try {
            permissionInfo = mContext.getPackageManager().getPermissionInfo(permName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }

        return (permissionInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE)
            == PermissionInfo.PROTECTION_DANGEROUS;
    }

在這基礎上搞個自定義的dialog,完成實現。

這裡的MODE 在原來ALLOWED 和 IGNORED 之外,多加了一個NOTED,表示每次都提示。

附加:

1、應用中對於MODE_NOTED 模式處理:

   public boolean noteRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) {
        final int uid = mPackageInfo.applicationInfo.uid;

        // We toggle permissions only to apps that support runtime
        // permissions, otherwise we toggle the app op corresponding
        // to the permission if the permission is granted to the app.
        for (Permission permission : mPermissions.values()) {
            if (filterPermissions != null && !ArrayUtils.contains(filterPermissions, permission.getName())) {
                continue;
            }

            if (mAppSupportsRuntimePermissions && mIsRuntimePermission && !mIsSendMMS) {
                // Do not touch permissions fixed by the system.
                if (permission.isSystemFixed()) {
                    return false;
                }

                if (permission.hasAppOp() && !permission.isAppOpNoted()) {
                    permission.setAppOpMode(AppOpsManager.MODE_NOTE);
                    // Disable the app op.
                    mAppOps.setMode(permission.getOp(), uid, mPackageInfo.packageName, AppOpsManager.MODE_NOTE);
                }

                // Grant the permission if needed.
                if (!permission.isGranted()) {
                    permission.setGranted(true);
                    mPackageManager.grantRuntimePermission(mPackageInfo.packageName, permission.getName(), mUserHandle);
                }

                // Update the permission flags.
                if (!fixedByTheUser) {
                    // Now the apps can ask for the permission as the user
                    // no longer has it fixed in a denied state.
                    if (permission.isUserFixed() || permission.isUserSet()) {
                        permission.setUserFixed(false);
                        permission.setUserSet(false);
                        mPackageManager.updatePermissionFlags(permission.getName(), mPackageInfo.packageName,
                                PackageManager.FLAG_PERMISSION_USER_FIXED | PackageManager.FLAG_PERMISSION_USER_SET, 0,
                                mUserHandle);
                    }
                }
            } else {
                // Legacy apps cannot have a not granted permission but just in case.
                if (!permission.isGranted()) {
                    continue;
                }

                int killUid = -1;
                int mask = 0;

                // If the permissions has no corresponding app op, then it is a
                // third-party one and we do not offer toggling of such permissions.
                if (permission.hasAppOp()) {
                    if (!permission.isAppOpNoted()) {
                        permission.setAppOpMode(AppOpsManager.MODE_NOTE);
                        // Enable the app op.
                        mAppOps.setMode(permission.getOp(), uid, mPackageInfo.packageName, AppOpsManager.MODE_NOTE);
                    }

                    // Mark that the permission should not be be granted on upgrade
                    // when the app begins supporting runtime permissions.
                    if (permission.shouldRevokeOnUpgrade()) {
                        permission.setRevokeOnUpgrade(false);
                        mask |= PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE;
                    }
                }

                // Granting a permission explicitly means the user already
                // reviewed it so clear the review flag on every grant.
                if (permission.isReviewRequired()) {
                    permission.resetReviewRequired();
                    mask |= PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED;
                }

                if (mask != 0) {
                    mPackageManager.updatePermissionFlags(permission.getName(), mPackageInfo.packageName, mask, 0,
                            mUserHandle);
                }

                if (killUid != -1) {
                    mActivityManager.killUid(killUid, KILL_REASON_APP_OP_CHANGE);
                }
            }
        }

        return true;
    }
2、AMS中checkPermission 加入的處理流程(會出現死鎖,使用M 系統源生即可)
    /**
     * added by wj, check permission of application which the target sdk is less than the one of M and
     * the protection level of permission is dangerous.
     * @return 0 or 1 indicates the permission has handled by AppOps.
     */
    private final Object aLock = new Object();
    private int checkPermissionForAppOps(String permission, int uid) {
        PermissionInfo info = null;
        try {
            info = mContext.getPackageManager().getPermissionInfo(permission, 0);
        } catch (PackageManager.NameNotFoundException e) {
            //ignore
        }

        //At first, the permission is dangerous.
        if (info == null
            || ((info.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE) != PermissionInfo.PROTECTION_DANGEROUS))
            return -1;

        String pkgName = null;
        try {
            pkgName = AppGlobals.getPackageManager().getNameForUid(uid);
        } catch (RemoteException e) {
            //ignore
        }
        Slog.d(TAG, "checkPermissionForAppOps, permission = " + permission + ", uid = " + uid
            + ", packageName = " + pkgName);

        if (pkgName == null)
            return -1;

        ApplicationInfo ai = null;
        try {
            ai = mContext.getPackageManager().getApplicationInfo(pkgName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            //ignore
        }

        if (ai == null || ai.targetSdkVersion >= Build.VERSION_CODES.M)
            return -1;
        Slog.d(TAG, "checkPermissionForAppOps, targetSdkVersion = " + ai.targetSdkVersion);

        synchronized(aLock) {
            AppOpsManager mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
            int tUid = Binder.getCallingUid() >= uid ? Binder.getCallingUid() : uid;//get application uid but not the one from native
            if ((tUid >= Process.FIRST_APPLICATION_UID)
                && (pkgName.indexOf("android.uid.systemui") != 0)
                && (pkgName.indexOf("android.uid.system") != 0)) {
                int result = mAppOpsManager.noteOp(AppOpsManager.permissionToOp(permission),
                    tUid, pkgName);
                return result;
            }
        }

        return -1;
    }

3、加入MODE_NOTED 之後的介面


4、許可權控制選擇介面


5、對話方塊提示介面