preface

Google introduced permission application mechanism in Android 6.0, which divides all permissions into normal permissions and dangerous permissions. Every time the App uses dangerous permissions, it needs to dynamically apply for and get the user’s authorization to use them. Otherwise, an exception will be thrown.

So how is the permission application and verification implemented? This is also the focus of the analysis of this note.

Classification of permissions:

System rights are classified into normal rights and dangerous rights.

Normal permissions do not directly affect user privacy. If your application lists normal permissions in its manifest, the system will automatically grant them.

Dangerous permissions grant applications access to confidential user data. If your application lists normal permissions in its manifest, the system will automatically grant them. If you list dangerous permissions, users must explicitly approve the use of these permissions for your application.

Introduction to core Methods

  • ContextCompat.checkSelfPermissionCheck whether the application has a dangerous permission.
    • If the application has this permission, the method returns the PackageManager. PERMISSION_GRANTED, and the application can continue to operate.
    • If the application does not have the authority, the method will return the PackageManager. PERMISSION_DENIED, and applications must be made clear to the user request permissions.
  • ActivityCompat.shouldShowRequestPermissionRationaleDetermine whether a permission request has been rejected before
    • This method returns true if the application has previously requested this permission but the user has rejected the request.
    • This method returns false if the user has rejected permission requests in the past and selected the Don’t Ask Again option in the permission Request system dialog box.
    • This method also returns false if the device disallows the application from having this permission.
  • ActivityCompat.requestPermissionsThe application can apply for permissions dynamically through this method. After invoking this method, a dialog box will pop up asking the user to authorize the requested permissions.
  • Activity.onRequestPermissionsResultWhen a claim limit is applied, a dialog box is displayed to the user. When the user response, the system calls the application onRequestPermissionsResult () method, to transfer the user response, deal with the corresponding scenario.

The Android permission application process is open to the developer core in four main ways that developers can apply for permission for our application.

Permission Application Process

The following figure shows the procedure for applying for permission on the service level

Through the above steps, let’s dig deeper into the source layer to understand how PKMS controls our application permissions

Permission check a ContextCompat checkSelfPermission

ContextCompat.checkSelfPermission => ContextImpl.checkPermission => AMS.checkPermission =>AMS.checkComponentPermission => AM.checkComponentPermission

Permission check will across processes Binder call AMS. CheckPermission step by step, then call to AM. CheckComponentPermission

AM.checkComponentPermission

public static int checkComponentPermission(String permission, int uid,
        int owningUid, boolean exported) {
    // Root, system server get to do everything.
    final int appId = UserHandle.getAppId(uid);
  	// If the uid is ROOT_UID or system uid, the user has the permission directly
    if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) {
        return PackageManager.PERMISSION_GRANTED;
    }
    // Isolated processes don't get any permissions.
  	// An isolated process does not have any permissions. Common application processes are not isolated processes
    if (UserHandle.isIsolated(uid)) {
        return PackageManager.PERMISSION_DENIED;
    }
    // If there is a uid that owns whatever is being accessed, it has
    // blanket access to it regardless of the permissions it requires.
  	// If the owning id is the same as the requested ID, the user can directly have the permission
    if (owningUid >= 0 && UserHandle.isSameApp(uid, owningUid)) {//owningUid == -1 Do not execute the following code
        return PackageManager.PERMISSION_GRANTED;
    }
    // If the target is not exported, then nobody else can get to it.
    if(! exported) {//exported == true Does not execute the following code
        /* RuntimeException here = new RuntimeException("here"); here.fillInStackTrace(); Slog.w(TAG, "Permission denied: checkComponentPermission() owningUid=" + owningUid, here); * /
        return PackageManager.PERMISSION_DENIED;
    }
    if (permission == null) {
        return PackageManager.PERMISSION_GRANTED;
    }
    try {
      	// Get PAKMS Binder
        return AppGlobals.getPackageManager()
                .checkUidPermission(permission, uid);
    } catch (RemoteException e) {
        throwe.rethrowFromSystemServer(); }}Copy the code

AppGlobals.getPackageManager => ActivityThread.getPackageManager

ActivityThread.getPackageManager

public static IPackageManager getPackageManager(a) {
    if(sPackageManager ! =null) {
        return sPackageManager;
    }
    final IBinder b = ServiceManager.getService("package");
  	//sPackageManager == PackageManagerService instance
    sPackageManager = IPackageManager.Stub.asInterface(b);
    return sPackageManager;
}
Copy the code

Because PKMS and AMS are in the same process, sPackageManager here is the PackageManagerService instance that inherits ipackagemanag.stub. Then enter the PackageManagerService. CheckUidPermission

PackageManagerService.checkUidPermission => PermissionManagerService.checkUidPermission

PermissionManagerService.checkUidPermission

public int checkUidPermission(String permName, int uid) {
    // Not using Objects.requireNonNull() here for compatibility reasons.
    if (permName == null) {
        return PackageManager.PERMISSION_DENIED;
    }
    final int userId = UserHandle.getUserId(uid);
    if(! mUserManagerInt.exists(userId)) {return PackageManager.PERMISSION_DENIED;
    }
		
    final CheckPermissionDelegate checkPermissionDelegate;
    synchronized (mLock) {
        checkPermissionDelegate = mCheckPermissionDelegate;
    }
    if (checkPermissionDelegate == null)  {
      	// Do this by default
        return checkUidPermissionImpl(permName, uid);
    }
  	CheckUidPermissionImpl this is used for external interception, you can do some extra things, but you still end up doing checkUidPermissionImpl
    return checkPermissionDelegate.checkUidPermission(permName, uid,
            this::checkUidPermissionImpl);
}
Copy the code

PermissionManagerService.checkUidPermissionImpl

private int checkUidPermissionImpl(String permName, int uid) {
  	/ / 1. PackageManagerInternalImpl access application package of information
    final AndroidPackage pkg = mPackageManagerInt.getPackage(uid);
  	//2. Check whether the application has the permission
    return checkUidPermissionInternal(pkg, uid, permName);
}
Copy the code
/ / PackageManagerService. Java PackageManagerInternalImpl inner classes
public AndroidPackage getPackage(int uid) {
    synchronized (mLock) {
        final String[] packageNames = getPackagesForUidInternal(uid, Process.SYSTEM_UID);
        AndroidPackage pkg = null;
        final int numPackages = packageNames == null ? 0 : packageNames.length;
        for (int i = 0; pkg == null && i < numPackages; i++) {
            pkg = mPackages.get(packageNames[i]);
        }
        returnpkg; }}Copy the code

Get the package of the corresponding app from the mPackages collection of the PackageManagerService

PermissionManagerService.checkUidPermissionInternal => PermissionManagerService.checkPermissionInternal => PermissionManagerService.checkSinglePermissionInternal

PermissionManagerService.checkSinglePermissionInternal

private boolean checkSinglePermissionInternal(int uid,
        @NonNull PermissionsState permissionsState, @NonNull String permissionName) {
  	// The current application permissionsState holds the application permission image
    if(! permissionsState.hasPermission(permissionName, UserHandle.getUserId(uid))) {return false;
    }
  	//InstantApp
    if(mPackageManagerInt.getInstantAppPackageName(uid) ! =null) {
        return mSettings.isPermissionInstant(permissionName);
    }
    return true;
}
Copy the code

PermissionsState.hasPermission

public boolean hasPermission(String name, int userId) {
    synchronized (mLock) {
        if (mPermissions == null) {
            return false;
        }
      	//permissionData Saves application permission applications
        PermissionData permissionData = mPermissions.get(name);
        returnpermissionData ! =null&& permissionData.isGranted(userId); }}Copy the code

Permission check two ActivityCompat shouldShowRequestPermissionRationale

Check permission denial

ActivityCompat.shouldShowRequestPermissionRationale => Activity.shouldShowRequestPermissionRationale => PKMS. ShouldShowRequestPermissionRationale across processes Binder calls

PKMS.shouldShowRequestPermissionRationale

public boolean shouldShowRequestPermissionRationale(String permName,
        String packageName, int userId) {
    final int callingUid = Binder.getCallingUid();
		/ /...
  	// If permission has been requested, false will be returned
    if (checkPermission(permName, packageName, userId)
            == PackageManager.PERMISSION_GRANTED) {
        return false;
    }

    final int flags;

    final long identity = Binder.clearCallingIdentity();
    try {
      	// Return the flag of the permission request operation
        flags = getPermissionFlagsInternal(permName, packageName, callingUid, userId);
    } finally {
        Binder.restoreCallingIdentity(identity);
    }

    final int fixedFlags = PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
            | PackageManager.FLAG_PERMISSION_POLICY_FIXED
            | PackageManager.FLAG_PERMISSION_USER_FIXED;
		// Determine whether it is user force rejection (no reminder) or system rejection or policy rejection
    if((flags & fixedFlags) ! =0) {
        return false;
    }
		/ /...
		// Determine if the user clicked cancel
    return(flags & PackageManager.FLAG_PERMISSION_USER_SET) ! =0;
}
Copy the code

PKMS.getPermissionFlagsInternal

Gets the permission request flag

private int getPermissionFlagsInternal(
        String permName, String packageName, int callingUid, int userId) {
    / /...
    synchronized (mLock) {
      	// If there is no permission request, return 0
        if (mSettings.getPermissionLocked(permName) == null) {
            return 0; }}/ /...
    PermissionsState permissionsState = ps.getPermissionsState();
  	// Get permission tags
    return permissionsState.getPermissionFlags(permName, userId);
}
Copy the code

Permissions apply ActivityCompat requestPermissions

The application can apply for permissions dynamically through this method. After invoking this method, a dialog box will pop up asking the user to authorize the requested permissions.

ActivityCompat.requestPermissions => Activity.requestPermissions

Activity.requestPermissions

public final void requestPermissions(@NonNull String[] permissions, int requestCode) {
  	/ / by PKMS to encapsulate the implicit jump permission. UI. GrantPermissionsActivity Intent
    Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions);
    startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null);
    mHasCurrentPermissionsRequest = true;
}
Copy the code

Activity.getPackageManager => ContextImpl.getPackageManager => ApplicationPackageManager.getPackageManager

ApplicationPackageManager derived from PackageManager so getPackageManager (). BuildRequestPermissionsIntent = = PackageManager.buildRequestPermissionsIntent

PackageManager.buildRequestPermissionsIntent

public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS";
public Intent buildRequestPermissionsIntent(@NonNull String[] permissions) {
    if (ArrayUtils.isEmpty(permissions)) {
       throw new IllegalArgumentException("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;
}
Copy the code

Returns an implicit Intent to open the system application. So what apps to open? We can start with the ACTION_REQUEST_PERMISSIONS value

<! -- PackageInstaller AndroidManifest.xml -->
<activity android:name="com.android.permissioncontroller.permission.ui.GrantPermissionsActivity"
        android:configChanges="keyboardHidden|screenSize"
        android:excludeFromRecents="true"
        android:theme="@style/GrantPermissions.FilterTouches"
        android:visibleToInstantApps="true"
        android:inheritShowWhenLocked="true">
    <intent-filter android:priority="1">
        <action android:name="android.content.pm.action.REQUEST_PERMISSIONS" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
Copy the code

We through the android. Content. PM. Action. This action REQUEST_PERMISSIONS system application, looking for, finally found the corresponding Activity

GrantPermissionsActivity.showNextPermissionGroupGrantRequest

In GrantPermissionsActivity. Call GrantPermissionsActivity in onCreate. ShowNextPermissionGroupGrantRequest to privilege group application, This means that when you apply for a group of permissions, that is, for the entire group, you will have access to the entire group

private boolean showNextPermissionGroupGrantRequest(a) {
    int numGroupStates = mRequestGrantPermissionGroups.size();
  	// Determine the number of permission requests
    int numGrantRequests = 0;
    for (int i = 0; i < numGroupStates; i++) {
      	// Filter out unnecessary permission requests and duplicate permission requests
        if(shouldShowRequestForGroupState(mRequestGrantPermissionGroups.valueAt(i))) { numGrantRequests++; }}int currentIndex = 0;
  	/ /...
    for (GroupState groupState : groupStates) {
        if(! shouldShowRequestForGroupState(groupState)) {continue;
        }
        if (groupState.mState == GroupState.STATE_UNKNOWN) {

            / /... The permission application UI content is initialized

            // Set the permission message as the title so it can be announced.
            setTitle(message);
						NumGrantRequests Total number of permission group quests. CurrentIndex Indicates the number of entries currently applied
            mViewHandler.updateUi(groupState.mGroup.getName(), numGrantRequests, currentIndex,
                    icon, message, detailMessage, mButtonVisibilities);

            return true;
        }

        if (groupState.mState != GroupState.STATE_SKIPPED) {
            currentIndex++;
        }
    }
		// There are no permissions to apply for
    return false;
}
Copy the code

When onCreate mViewHandler create com. Android. Permissioncontroller. Permission. UI. Handheld. GrantPermissionsViewHandlerImpl Instance. Next, let’s look at the click events associated with mViewHandler

mViewHandler.onClick

public void onClick(View view) {
    int id = view.getId();
    if (id == R.id.grant_singleton) {
        if(mResultListener ! =null) {
          	// Cancel permission request
            mResultListener.onPermissionGrantResult(mGroupName, CANCELED);
        } else {
            mActivity.finish();
        }
        return;
    }
    int button = -1;
    try {
        button = BUTTON_RES_ID_TO_NUM.get(id);
    } catch (NullPointerException e) {
        // Clicked a view which is not one of the defined buttons
        return;
    }
    switch (button) {
        case ALLOW_BUTTON:
            if(mResultListener ! =null) {
                view.performAccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
              	// Always agree with permission requests
                mResultListener.onPermissionGrantResult(mGroupName, GRANTED_ALWAYS);
            }
            break;
        case ALLOW_FOREGROUND_BUTTON:
            if(mResultListener ! =null) {
                view.performAccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
              	// Only foreground permissions are allowed
                mResultListener.onPermissionGrantResult(mGroupName,
                        GRANTED_FOREGROUND_ONLY);
            }
            break;
        case ALLOW_ALWAYS_BUTTON:
            if(mResultListener ! =null) {
                view.performAccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
              	// Always agree with permission requests
                mResultListener.onPermissionGrantResult(mGroupName,
                        GRANTED_ALWAYS);
            }
            break;
        case ALLOW_ONE_TIME_BUTTON:
            if(mResultListener ! =null) {
                view.performAccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
              	// Only once
                mResultListener.onPermissionGrantResult(mGroupName, GRANTED_ONE_TIME);
            }
            break;
        case DENY_BUTTON:
        case NO_UPGRADE_BUTTON:
        case NO_UPGRADE_OT_BUTTON:
            if(mResultListener ! =null) {
                view.performAccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
              	// Reject permission request
                mResultListener.onPermissionGrantResult(mGroupName, DENIED);
            }
            break;
        case DENY_AND_DONT_ASK_AGAIN_BUTTON:
        case NO_UPGRADE_AND_DONT_ASK_AGAIN_BUTTON:
        case NO_UPGRADE_OT_AND_DONT_ASK_AGAIN_BUTTON:
            if(mResultListener ! =null) {
                view.performAccessibilityAction(
                        AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
              	// Reject permission requests without prompting
                mResultListener.onPermissionGrantResult(mGroupName,
                        DENIED_DO_NOT_ASK_AGAIN);
            }
            break; }}Copy the code

MResultListener responsible for callback, click interaction in GrantPermissionsActivity. Set in onCreate. Let’s move on to the next callback.

GrantPermissionsActivity.onPermissionGrantResult

public void onPermissionGrantResult(String name,
        @GrantPermissionsViewHandler.Result int result) {
    GroupState foregroundGroupState = getForegroundGroupState(name);
    GroupState backgroundGroupState = getBackgroundGroupState(name);

  	/ /...
    switch (result) {
        case CANCELED:/ / cancel
            if(foregroundGroupState ! =null) {
                reportRequestResult(foregroundGroupState.affectedPermissions,
                        PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED);
            }
            if(backgroundGroupState ! =null) {
                reportRequestResult(backgroundGroupState.affectedPermissions,
                        PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_IGNORED);
            }
            setResultAndFinish();
            return;
        case GRANTED_ALWAYS :
            if(foregroundGroupState ! =null) {
                onPermissionGrantResultSingleState(foregroundGroupState, true.false.false);
            }
            if(backgroundGroupState ! =null) {
                onPermissionGrantResultSingleState(backgroundGroupState, true.false.false);
            }
            break;
        case GRANTED_FOREGROUND_ONLY :
            if(foregroundGroupState ! =null) {
                onPermissionGrantResultSingleState(foregroundGroupState, true.false.false);
            }
            if(backgroundGroupState ! =null) {
                onPermissionGrantResultSingleState(backgroundGroupState, false.false.false);
            }
            break;
        case GRANTED_ONE_TIME:
            if(foregroundGroupState ! =null) {
                onPermissionGrantResultSingleState(foregroundGroupState, true.true.false);
            }
            if(backgroundGroupState ! =null) {
                onPermissionGrantResultSingleState(backgroundGroupState, false.true.false);
            }
            break;
        case DENIED :
            if(foregroundGroupState ! =null) {
                onPermissionGrantResultSingleState(foregroundGroupState, false.false.false);
            }
            if(backgroundGroupState ! =null) {
                onPermissionGrantResultSingleState(backgroundGroupState, false.false.false);
            }
            break;
        case DENIED_DO_NOT_ASK_AGAIN :
            if(foregroundGroupState ! =null) {
                onPermissionGrantResultSingleState(foregroundGroupState, false.false.true);
            }
            if(backgroundGroupState ! =null) {
                onPermissionGrantResultSingleState(backgroundGroupState, false.false.true);
            }
            break;
    }
		// Proceed to the next permission application until all permissions are applied
    if (!showNextPermissionGroupGrantRequest()) {
        setResultAndFinish();
    }
}
Copy the code

There are two main methods in the onPermissionGrantResult method

  • onPermissionGrantResultSingleStateUpdate Permission Status
  • showNextPermissionGroupGrantRequestApply for the next permission and repeat step 1

GrantPermissionsActivity.onPermissionGrantResultSingleState

private void onPermissionGrantResultSingleState(GroupState groupState, boolean granted,
        boolean isOneTime, boolean doNotAskAgain) {
    if(groupState ! =null&& groupState.mGroup ! =null
            && groupState.mState == GroupState.STATE_UNKNOWN) {
        if (granted) {
            int permissionGrantRequestResult =
                    PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED;

            if (isOneTime) {
                groupState.mGroup.setOneTime(true);
                permissionGrantRequestResult =
                        PERMISSION_GRANT_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED_ONE_TIME;
            } else {
                groupState.mGroup.setOneTime(false);
            }

            groupState.mGroup.grantRuntimePermissions(true, doNotAskAgain,
                    groupState.affectedPermissions);
            groupState.mState = GroupState.STATE_ALLOWED;
        } else {
            groupState.mGroup.revokeRuntimePermissions(doNotAskAgain,
                    groupState.affectedPermissions);
            groupState.mGroup.setOneTime(false); groupState.mState = GroupState.STATE_DENIED; }}}Copy the code

The core purpose of this method is to update the groupState permission status and then return the result to setResultAndFinish.

Access the callback

Call setResultAndFinish after GrantPermissionsActivity permission has been granted. With AMS cross-process calls, Returns the result ActivityThread. HandleResumeActivity = > ActivityThread. PerformResumeActivity = > ActivityThread. DeliverResults = > Activity.dispatchActivityResult

Activity.dispatchActivityResult

void dispatchActivityResult(String who, int requestCode, int resultCode, Intent data,
        String reason) {
    mFragments.noteStateNotSaved();
    if (who == null) {
        onActivityResult(requestCode, resultCode, data);
    } else if (who.startsWith(REQUEST_PERMISSIONS_WHO_PREFIX)) {
      	// The activity requests a permission callback
        who = who.substring(REQUEST_PERMISSIONS_WHO_PREFIX.length());
        if (TextUtils.isEmpty(who)) {
          	// Distribute permission application results
            dispatchRequestPermissionsResult(requestCode, data);
        } else {
          	// Fragment permission callback
            Fragment frag = mFragments.findFragmentByWho(who);
            if(frag ! =null) { dispatchRequestPermissionsResultToFragment(requestCode, data, frag); }}}/ /...
}
Copy the code

Activity.dispatchRequestPermissionsResult

private void dispatchRequestPermissionsResult(int requestCode, Intent data) {
    mHasCurrentPermissionsRequest = false;
    // If the package installer crashed we may have not data - best effort.String[] permissions = (data ! =null)? data.getStringArrayExtra( PackageManager.EXTRA_REQUEST_PERMISSIONS_NAMES) :new String[0];
    final int[] grantResults = (data ! =null)? data.getIntArrayExtra( PackageManager.EXTRA_REQUEST_PERMISSIONS_RESULTS) :new int[0];
  	// This is the last step of permission callback processing
    onRequestPermissionsResult(requestCode, permissions, grantResults);
}

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
        @NonNull int[] grantResults) {
    /* callback - no nothing */
}
Copy the code

Finally application of overloaded onRequestPermissionsResult permissions results callback

The end

The whole permission application logic is relatively clear, and the cross-process invocation during the process requires a certain understanding of Binder, AMS, PKMS.

Analysis of PKMS juejin. Cn/post / 688292…

Activity startup process juejin.cn/post/687819…

AMS startup process juejin.cn/post/687705…

Principle of Binder gityuan.com/2015/10/31/…