1, an overview of the

PermissionsDispatcher – making the address

Permission requests in the project use the PermissionDispatcher library, which annotates permission requests.

Observe that the dependency it introduces contains the annotation handler, so you can see that it generates code for us at compile time.

1.1 Basic Usage

  • Introduce dependency & annotation handlers
  • The manifest file declares permissions to use
  • Modify the Fragment or Activity that requests permissions with @runtimePermissions
  • Accessorize the requested permission method with @needsperMission, stating the required permissions (in array form)
  • Build the code generation XXXXPermissionDispatcher. Kt file (kotlin)
  • Replace the request permission method with the XXXWithPermissionCheck extension method
  • Rewrite onRequestPermissionsResult method, called generated expansion method, with the same processing logic after get permission

2. Source code analysis

KotlinPoet- Official documentation

2.1 code structure analysis

  • annotation

    • annotations
  • library

    • There is only one PermissionUtils class that co-generates code to check permission usage
  • processor

    • Annotation handler, used to generate Java or Kotlin code

The organization of the code shows that the PermissionDispatcher work is generated primarily by the annotation handler

XXXXPermissionDispatcher file, XXXX corresponding class name decorated with @runtimepermissions. (such as:

MainActivity is corresponding MainActivityPermissionDispatcher).

2.2. Kapt generates code

// This file was generated by PermissionsDispatcher. Do not modify!
@file:JvmName("MainActivity2PermissionsDispatcher")

package com.example.asmrapp

import androidx.core.app.ActivityCompat
import kotlin.Array
import kotlin.Int
import kotlin.IntArray
import kotlin.String
import permissions.dispatcher.PermissionUtils

private const val REQUEST_TAKECAMERA: Int = 0

private val PERMISSION_TAKECAMERA: Array<String> = arrayOf("android.permission.CAMERA")

fun MainActivity2.takeCameraWithPermissionCheck() {
  if (PermissionUtils.hasSelfPermissions(this, *PERMISSION_TAKECAMERA)) {
    takeCamera()
  } else {
    ActivityCompat.requestPermissions(this, PERMISSION_TAKECAMERA, REQUEST_TAKECAMERA)
  }
}

fun MainActivity2.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
  when (requestCode) {
    REQUEST_TAKECAMERA ->
     {
      if (PermissionUtils.verifyPermissions(*grantResults)) {
        takeCamera()
      }
    }
  }
}
Copy the code

The PermissionDispatcher uses the annotation processor in conjunction with the JavaPoet and KotlinPoet code generation libraries to generate code for us (in Kotlin’s case, it generates a Kt file like the one above, The filename takes the name of the Activity or Fragment annotated with @runtimePermission and generates extension methods for it to check for permission requests and handle the logic that calls the code we wrote once permission is obtained.

2.3. Annotation processor

class PermissionsProcessor : AbstractProcessor() { private val javaProcessorUnits = listOf(JavaActivityProcessorUnit(), JavaFragmentProcessorUnit()) private val kotlinProcessorUnits = listOf(KotlinActivityProcessorUnit(), KotlinFragmentProcessorUnit()) /* Processing Environment helpers */ private var filer: Filer by Delegates.notNull() override fun init(processingEnv: ProcessingEnvironment) { super.init(processingEnv) filer = processingEnv.filer ELEMENT_UTILS = processingEnv.elementUtils TYPE_UTILS = processingEnv.typeUtils } override fun getSupportedSourceVersion(): SourceVersion? { return SourceVersion.latestSupported() } override fun getSupportedAnnotationTypes(): Set<String> { return hashSetOf(RuntimePermissions::class.java.canonicalName) } override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Var requestCodeProvider = requestCodeProvider (); var requestCodeProvider = requestCodeProvider () roundEnv.getElementsAnnotatedWith(RuntimePermissions::class.java) .sortedBy { it.simpleName.toString() } .forEach { val rpe = RuntimePermissionsElement(it as TypeElement) val kotlinMetadata = it.getAnnotation(Metadata::class.java) if (kotlinMetadata ! = null) {processKotlin(it, rPE, requestCodeProvider)} else {processJava(it, rPE, requestCodeProvider)} requestCodeProvider) } } return true } private fun processKotlin(element: Element, rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider) { val processorUnit = findAndValidateProcessorUnit(kotlinProcessorUnits, element) val kotlinFile = processorUnit.createFile(rpe, requestCodeProvider) kotlinFile.writeTo(filer) } private fun processJava(element: Element, rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider) { val processorUnit = findAndValidateProcessorUnit(javaProcessorUnits, element) val javaFile = processorUnit.createFile(rpe, RequestCodeProvider) javafile.writeto (filer)}} // Find the first subtype in units whose type is the targetType fun <K> findAndValidateProcessorUnit(units: List<ProcessorUnit<K>>, element: Element): ProcessorUnit<K> { val type = element.asType() try { return units.first { type.isSubtypeOf(it.getTargetType()) } } catch  (ex: NoSuchElementException) { throw WrongClassException(type) } }Copy the code
  • The annotation processor’s entry class, PermissionsProcessor
  • Gets all classes decorated with the @RuntimePermissions annotation in the Process entry method
  • Call different methods to process by determining whether the class being decorated is a Java class or a Kotlin class
  • Find the corresponding processing unit and call createFile to process it
    • Activity finds ActivityProcessorUnit and Fragment finds FragmentProcessorUnit
class KotlinActivityProcessorUnit : KotlinBaseProcessorUnit() {

    override fun getTargetType(): TypeMirror = typeMirrorOf("android.app.Activity")

    override fun getActivityName(targetParam: String): String = targetParam

    override fun addShouldShowRequestPermissionRationaleCondition(builder: FunSpec.Builder, permissionField: String, isPositiveCondition: Boolean) {
        val condition = if (isPositiveCondition) "" else "!"
        builder.beginControlFlow("if (%L%T.shouldShowRequestPermissionRationale(%L, *%N))", condition, permissionUtils, "this", permissionField)
    }

    override fun addRequestPermissionsStatement(builder: FunSpec.Builder, targetParam: String, permissionField: String, requestCodeField: String) {
        builder.addStatement("%T.requestPermissions(%L, %N, %N)", ClassName("androidx.core.app", "ActivityCompat"), targetParam, permissionField, requestCodeField)
    }
}

class KotlinFragmentProcessorUnit : KotlinBaseProcessorUnit() {

    override fun getTargetType(): TypeMirror = typeMirrorOf("androidx.fragment.app.Fragment")

    override fun getActivityName(targetParam: String): String = "$targetParam.requireActivity()"

    override fun addShouldShowRequestPermissionRationaleCondition(builder: FunSpec.Builder, permissionField: String, isPositiveCondition: Boolean) {
        val condition = if (isPositiveCondition) "" else "!"
        builder.beginControlFlow("if (%L%T.shouldShowRequestPermissionRationale(%L, *%N))", condition, permissionUtils, "this" /* Fragment */, permissionField)
    }

    override fun addRequestPermissionsStatement(builder: FunSpec.Builder, targetParam: String, permissionField: String, requestCodeField: String) {
        builder.addStatement("%L.requestPermissions(%L, %N)", targetParam, permissionField, requestCodeField)
    }
}
Copy the code
  • Kotlin Activity and Fragment correspond to the processing unit, Java also has corresponding code
  • The createFile method is not found in the Activity or Fragment processing unit
/ / annotation processor generates code logic to override fun createFile (rpe: RuntimePermissionsElement, requestCodeProvider: requestCodeProvider) : FileSpec { return FileSpec.builder(rpe.packageName, Rpe. GeneratedClassName) // Package name, class name. AddComment (FILE_COMMENT) // comment .addAnnotation(createJvmNameAnnotation(rPE. GeneratedClassName)) // @file:JvmName.addProperties(createProperties(rPE, RequestCodeProvider)) / / request code & permissions list. AddFunctions (createWithPermissionCheckFuns (rpe)) .addFunctions(createOnShowRationaleCallbackFuns(rpe)) .addFunctions(createPermissionHandlingFuns(rpe)) .addTypes(createPermissionRequestClasses(rpe)) .build() } private fun createProperties(rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider): List<PropertySpec> {val properties = arrayListOf<PropertySpec>() // All methods decorated by @needsperMission Rpe. NeedsElements. SortedBy {it. SimpleString ()}. ForEach {/ / add two attributes properties. The add (createRequestCodeProp (it, requestCodeProvider.nextRequestCode())) properties.add(createPermissionProperty(it)) if (it.parameters.isNotEmpty()) { val hasOnRationaleParams = rpe.findOnRationaleForNeeds(it)? .parameters? .isNotEmpty() ? : true if (hasOnRationaleParams) { properties.add(createPendingRequestProperty(it)) } else { Properties. AddAll (createArgProps(it))}}} return properties} Private Fun createRequestCodeProp(e: ExecutableElement, index: Int): PropertySpec { return PropertySpec.builder(requestCodeFieldName(e), Int::class.java, KModifier.CONST, KModifier.private).Initializer ("%L", index).build()} PRIVATE fun createPermissionProperty(e: ExecutableElement): PropertySpec { val permissionValue = e.getAnnotation(NeedsPermission::class.java).permissionValue() val formattedValue =  permissionValue.joinToString( separator = ", ", transform = { ""$it"" } ) val parameterType = ARRAY.plusParameter(ClassName("kotlin", "String")) return PropertySpec.builder(permissionFieldName(e), parameterType, KModifier.PRIVATE) .initializer("arrayOf(%L)", FormattedValue). The build ()} / / generated XXXXWithPermissionCheck function logic private fun createWithPermissionCheckFuns (rpe: RuntimePermissionsElement): List < FunSpec > {/ / each @ NeedsPermission modified methods return rpe. NeedsElements. Map {createWithPermissionCheckFun (rpe, it) } } private fun createWithPermissionCheckFun(rpe: RuntimePermissionsElement, method: ExecutableElement): FunSpec { val builder = FunSpec.builder(withPermissionCheckMethodName(method)) .addOriginatingElement(rpe.element) AddTypeVariables (rpe) ktTypeVariables)) receiver (rpe) ktTypeName) / / expanding method of the receiver if (method. EnclosingElement. IsInternal) { builder.addModifiers(KModifier.INTERNAL) } method.parameters.forEach { builder.addParameter(it.simpleString(), It. AsPreparedType ()} / / add the corresponding method of body addWithPermissionCheckBody (builder, method, rpe) return builder. The build ()}Copy the code
  • CreateFile method of KotlinBaseProcessorUnit
  • FileSpec is a class in KotlinPoet

The function generating the method body is a bit long, so let’s analyze it separately

fun requestCodeFieldName(e: ExecutableElement) = "$GEN_REQUEST_CODE_PREFIX${e.simpleString().trimDollarIfNeeded().toUpperCase()}" fun permissionFieldName(e: ExecutableElement) = "$GEN_PERMISSION_PREFIX${e.simpleString().trimDollarIfNeeded().toUpperCase()}" private fun addWithPermissionCheckBody(builder: FunSpec.Builder, needsMethod: ExecutableElement, rpe: RuntimePermissionsElement) {/ / Create the field names for the constants to use / / request code and the name of the permission val requestCodeField = requestCodeFieldName(needsMethod) val permissionField = permissionFieldName(needsMethod) // if maxSdkVersion is lower than os level does nothing val maxSdkVersion = needsMethod.getAnnotation(NeedsPermission::class.java).maxSdkVersion if (maxSdkVersion > 0) { BeginControlFlow ("if (% t.version.SDK_INT > %L)", build, maxSdkVersion) .addCode(CodeBlock.builder() .add("%N(", needsMethod.simpleString()) .add(varargsKtParametersCodeBlock(needsMethod)) .addStatement(")") .addStatement("return") .build()) .endControlFlow() } // Add the conditional for when permission has already been granted val needsPermissionParameter = needsMethod.getAnnotation(NeedsPermission::class.java).value[0] val activity = getActivityName() /* * if (PermissionUtils.hasSelfPermissions(this, *PERMISSION_TAKECAMERA)) {* takeCamera() *} */ / AddWithCheckBodyMap [needsPermissionParameter]? .addHasSelfPermissionsCondition(builder, activity, permissionField) ? : builder.beginControlFlow("if (%T.hasSelfPermissions(%L, *%N))", permissionUtils, activity, AddCode (codeblock. builder().add("%N(", needsMethod.simpleString()) .add(varargsKtParametersCodeBlock(needsMethod)) .addStatement(")") .build() ) Builder.nextcontrolflow ("else") // Add the conditional for "OnShowRationale", if present val onRationale = rpe.findOnRationaleForNeeds(needsMethod) val hasOnRationaleParams = onRationale? .parameters? .isNotEmpty() ? : true val hasParameters = needsMethod.parameters.isNotEmpty() if (hasParameters) { if (hasOnRationaleParams) { // If the method has parameters, precede the potential OnRationale call with // an instantiation of the temporary Request object val varargsCall = CodeBlock.builder() .add("%N = %N(this, ", pendingRequestFieldName(needsMethod), permissionRequestTypeName(rpe, needsMethod) ) .add(varargsKtParametersCodeBlock(needsMethod)) .addStatement(")") builder.addCode(varargsCall.build()) }  else { needsMethod.parameters.forEach { val code = CodeBlock.builder().addStatement("%N = %N", needsMethod.argumentFieldName(it), it.simpleString()) builder.addCode(code.build()) } } } if (onRationale ! = null) { addShouldShowRequestPermissionRationaleCondition(builder, permissionField) if (hasParameters) { if (hasOnRationaleParams) { // For methods with parameters, use the PermissionRequest instantiated above builder.addStatement("%N? .let { %N(it) }", pendingRequestFieldName(needsMethod), onRationale.simpleString()) } else { builder.addStatement("%N()", onRationale.simpleString()) } } else { if (hasOnRationaleParams) { // Otherwise, create a new PermissionRequest on-the-fly builder.addStatement("%N(%N(this))", onRationale.simpleString(), permissionRequestTypeName(rpe, needsMethod)) } else { builder.addStatement("%N()", onRationale.simpleString()) } } builder.nextControlFlow("else") } /* * else { * ActivityCompat.requestPermissions(this, PERMISSION_TAKECAMERA, REQUEST_TAKECAMERA) * } */ addWithCheckBodyMap[needsPermissionParameter]? .addRequestPermissionsStatement(builder = builder, activityVar = getActivityName(), requestCodeField = requestCodeField) ? : addRequestPermissionsStatement(builder = builder, permissionField = permissionField, requestCodeField = requestCodeField) if (onRationale ! = null) { builder.endControlFlow() } builder.endControlFlow() } override fun addRequestPermissionsStatement(builder: FunSpec.Builder, targetParam: String, permissionField: String, requestCodeField: String) { builder.addStatement("%T.requestPermissions(%L, %N, %N)", ClassName("androidx.core.app", "ActivityCompat"), targetParam, permissionField, requestCodeField) }Copy the code
  • Return if the current system SDK version is larger than maxSdkVersion specified in @needsperMission
  • Generate code that checks whether permissions have been obtained
  • Other code generated by non-essential annotations
  • Generate the code to request permission

After the above steps, PermissionDispatcher generates the code logic for permission checks and requests.