preface

In previous articles, we covered the basics of annotations and how to use reflection to implement runtime annotations.

# Learn to customize annotations, see this is enough (1)- custom runtime annotations

This chapter will focus on another form of annotations, the use of compile-time annotations, again using open source library source code.

The body of the

Since we are starting with the PermissionsDispatcher open source library, let’s first look at a simple use of the library.

PermissionsDispatcher

PermissionsDispatcher is an open source library for handling runtime permissions. The project address is PermissionsDispatcher.

Github.com/permissions…

To take a look at simple usage, the code here is an activity click the button to apply for camera permissions:

@RuntimePermissions class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) setContentView(r.layout.activity_main) // Click the button to request permission val buttonCamera: Button = the findViewById (R.i db utton_camera) buttonCamera. SetOnClickListener {/ / call methods showCameraWithPermissionCheck ()}} / / system application permissions callback override fun onRequestPermissionsResult (requestCode: Int, permissions: Array < String >, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) onRequestPermissionsResult(requestCode, GrantResults)} / / must apply the permissions for success will only be called after the @ NeedsPermission (Manifest) permission) CAMERA) fun showCamera () {Log i. (TAG, "showCamera: After the success of the access to a camera ") supportFragmentManager. BeginTransaction (). The replace (R.i d.s ample_content_fragment, CameraPreviewFragment. NewInstance ()). AddToBackStack (" camera "). CommitAllowingStateLoss ()} / / application access is denied @OnPermissionDenied(Manifest.permission.CAMERA) fun onCameraDenied() { Log.i(TAG, "onCameraDenied: MakeText (this, "Camera permission denied ", Toast. LENGTH_SHORT). The show ()} / / after refusing to explain to the authority of reason @ OnShowRationale (Manifest) permission) CAMERA) fun showRationaleForCamera(request: PermissionRequest) { Log.i(TAG, "showRationaleForCamera: ") showRationaleDialog(r.sing.permission_camera_rationale, Request)} / / never tip @ OnNeverAskAgain (Manifest) permission) CAMERA) fun onCameraNeverAskAgain () {Log i. (TAG, "onCameraNeverAskAgain: Never ask again ") toast.maketext (this, r.string.permission_camerA_never_ask_again, Toast.length_short).show()} private fun showRationaleDialog(@stringres messageResId: Int, request: PermissionRequest) { AlertDialog.Builder(this) .setPositiveButton(R.string.button_allow) { _, _ -> request.proceed() } .setNegativeButton(R.string.button_deny) { _, _ -> request.cancel() } .setCancelable(false) .setMessage(messageResId) .show() } }Copy the code

In fact, it is nothing more than several cases of permission application, successful application, rejected, never asked these three cases, so the above corresponding 3 notes,

Note that after writing the annotations, you need to build the project, which generates a file:

It cannot be called directly when the button is clicked

showCamera()
Copy the code

It needs to be called

showCameraWithPermissionCheck()
Copy the code

This method can be generated, let’s take a look at the renderings, and then discuss the details of the implementation:

Permission denied until never reminded,

If permission is granted, perform the corresponding method.

Ok, so the next big thing is to see how to call the build file method after compiling the project build file.

Annotation processor

Unlike runtime annotations, which need to be parsed and processed as the code allows, compile-time annotations, which are used when the project builds, need an annotation handler. So the focus here is on the annotation handler, which is the same for registered annotations as it was for run-time annotations, but handles annotations differently.

Introduction to the

Now that you know what annotations do, it’s how to define annotations and handle them at compile time.

Custom annotation handlers

The run-time annotations in the previous article are easy to understand. There are code actions registered in the code, and then they are scanned by reflection. How do we load the compile-time annotations when we build them?

Register annotation handlers

Registration is also very simple, consisting of 2 steps:

  • Add in android scope in Gradle
packagingOptions {
    exclude 'META-INF/services/javax.annotation.processing.Processor'
}
Copy the code
  • Just write custom annotation Processor in this Processor. This file is located in

It says:

permissions.dispatcher.processor.PermissionsProcessor
Copy the code

This PermissionsProcessor is our custom annotation processor that is called during build compilation.

AbstractProcessor

Take a look at the PermissionsProcessor class:

class PermissionsProcessor : AbstractProcessor() {}
Copy the code

You’ll notice that this section of code is derived from an AbstractProcessor class, and you’ll need to understand this abstract class otherwise the code will be really hard to read. Just like reflection in the last article, this section of code has a lot of uncommon apis.

From the above summary, it is easy for us to understand, annotation scanning and other working systems help us do well, here we only need to write logic according to the requirements.

PermissionsProcessor

Now that we know about AbstractProcessor’s abstract methods, let’s take a look at this PermissionsProcessor implementation:

// if: undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined... Types by Delegates.notNull() class PermissionsProcessor : AbstractProcessor() {AbstractProcessor() { Regardless of the first private val javaProcessorUnits = listOf (JavaActivityProcessorUnit (), JavaFragmentProcessorUnit()) private val kotlinProcessorUnits = listOf(KotlinActivityProcessorUnit(), KotlinFragmentProcessorUnit ()) / / generated file class private var filer: Filer by Delegates.notNull() override fun init(processingEnv: ProcessingEnvironment) {super.init(processingEnv) // Important init method, Tools can be obtained from parameters of filer = processingEnv. Filer ELEMENT_UTILS = processingEnv. ElementUtils TYPE_UTILS = processingEnv. TypeUtils}  override fun getSupportedSourceVersion(): SourceVersion? {/ / generally do not modify the return SourceVersion. LatestSupported ()} override fun getSupportedAnnotationTypes () : Set < String > {/ / return to scanning and processing of comments return hashSetOf (RuntimePermissions: : class. Java. CanonicalName)} override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): 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)}} 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)} Regardless of the first private fun processJava (element: the element, rpe: RuntimePermissionsElement requestCodeProvider: RequestCodeProvider) { val processorUnit = findAndValidateProcessorUnit(javaProcessorUnits, element) val javaFile = processorUnit.createFile(rpe, requestCodeProvider) javaFile.writeTo(filer) } }Copy the code

The RuntimePermissions annotation is scanned and processed using the PermissionsDispatcher library. This annotation is essential in an activity or fragment, indicating that the page requires dynamic permissions. So it makes sense.

Let’s focus on the processing code

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) } }Copy the code

Again, some of the less familiar apis, here is not the square, as in the previous reflection, first to introduce some basic knowledge.

Scanning Java files

Instead of runtime reflection, the annotator works by scanning Java source files as if they were parsing XML, as in the following code:

package com.example;    // PackageElement
 
public class Foo {        // TypeElement
 
    private int a;      // VariableElement
    private Foo other;  // VariableElement
 
    public Foo () {}    // ExecuteableElement
 
    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}
Copy the code

There is no Java code running here, just a compilation scan, where the Java code is parsed into each Element and then processed. Here are the types of all elements:

So for a piece of Java code, when it’s scanned, there are these elements, and you just do the processing on the specific elements. In fact, this is easy to understand, is part of the Java code.

Get a specific Element

In the previous PermissionsProcessor, we set the @RuntimePermissions annotation on an Activity or Fragment, and the process calls back any Element that is scanned with that annotation. The type in this case is TypeElement.

/ / get all the Element with the annotation roundEnv. GetElementsAnnotatedWith (RuntimePermissions: : class. Java). SortedBy { It.simplename.tostring ()}.foreach {// By definition, This element is TypeElement type val rpe = RuntimePermissionsElement (it as TypeElement) / / to parse and process the Elemenet val kotlinMetadata = it.getAnnotation(Metadata::class.java) if (kotlinMetadata ! = null) { processKotlin(it, rpe, requestCodeProvider) } else { processJava(it, rpe, requestCodeProvider) } }Copy the code

Depending on the use of the permissions library, we need to return the class Element, so we need to find the annotation of the various methods in the class Element, let’s see how to do that.

Handle specific elements

Now that we’re dealing with Element, let’s take a look at the class’s methods. The main methods are as follows:

In process, we call the following method: TypeElement (); TypeElement ();

val rpe = RuntimePermissionsElement(it as TypeElement)
Copy the code

So take a look at the RuntimePermissionsElement classes:

/ / parsing TypeElement element node class RuntimePermissionsElement (val element: TypeElement) {/ / get all the elements of information val typeName: TypeName = TypeName.get(element.asType()) val ktTypeName = element.asType().asTypeName() val typeVariables = element.typeParameters.map { TypeVariableName.get(it) } val ktTypeVariables = element.typeParameters.map { it.asTypeVariableName() } val packageName = element.packageName() val inputClassName = element.simpleString() val generatedClassName = inputClassName + GEN_CLASS_SUFFIX val needsElements = element.childElementsAnnotatedWith(NeedsPermission::class.java) private val onRationaleElements = element.childElementsAnnotatedWith(OnShowRationale::class.java) private val onDeniedElements = element.childElementsAnnotatedWith(OnPermissionDenied::class.java) private val onNeverAskElements = Element. ChildElementsAnnotatedWith (OnNeverAskAgain: : class. Java) init {/ / for printing first Look at what is println (" zyh $typeName)" println("zyh $ktTypeName") println("zyh $typeVariables") println("zyh $ktTypeVariables") println("zyh $packageName") println("zyh $inputClassName") println("zyh $generatedClassName") println("zyh $needsElements") println("zyh $onRationaleElements") println("zyh $onDeniedElements") println("zyh $onNeverAskElements") validateNeedsMethods() validateRationaleMethods() validateDeniedMethods() validateNeverAskMethods() } }Copy the code

Println must be used to print, not Log, because Log is an Android library, which is compiled at compile time and does not reference the Android library. At the same time, print is also in Build Output, not in logcat.

The two key methods are to get all elements below the element node:

println("zyh enclosedElements = ${element.enclosedElements}")
println("zyh enclosingElement = ${element.enclosingElement}")
Copy the code

The print is:

So the general idea is pretty clear, and it goes something like this:

To see how the code works, we use the @Needpermissions annotation to scan the TypeElement node element. The other annotations are the same as the following:

Private fun validateNeedsMethods() { Otherwise an error checkNotEmpty (needsElements, this NeedsPermission: : class. Java) / / can't be private modifiers checkPrivateMethods (needsElements, NeedsPermission: : class. / / Java) method returns a value must be Void checkMethodSignature (needsElements) / / get the values in the annotations checkMixPermissionType(needsElements, NeedsPermission: : / / class. Java) to determine whether this method is in this class checkDuplicatedMethodName (needsElements)}Copy the code

After this series of operations, we can easily get the contents of the annotations, and we are done parsing the annotations.

Generate the file

This is the essence of compile-time annotations again, after we have scanned the file for annotations and their elements, we need to generate the file as needed.

Kotlinpoet or javapoet

We recommend using the Poet open source library to help us complete the creation file. Here I use kotlinPoet because it is Android.

square.github.io/kotlinpoet/

You will find that it is actually very simple, according to the file you want to generate, the Java file into a node for splicing, the code is as follows:

private fun processKotlin(element: Element
                         , rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider) {
   val processorUnit = findAndValidateProcessorUnit(kotlinProcessorUnits, element)
   val kotlinFile = processorUnit.createFile(rpe, requestCodeProvider)
   kotlinFile.writeTo(filer)
}
Copy the code

The key here is the createFile function, which concatenates the desired file:

override fun createFile(rpe: RuntimePermissionsElement, requestCodeProvider: RequestCodeProvider): FileSpec {
    return FileSpec.builder(rpe.packageName, rpe.generatedClassName)
            .addComment(FILE_COMMENT)
            .addAnnotation(createJvmNameAnnotation(rpe.generatedClassName))
            .addProperties(createProperties(rpe, requestCodeProvider))
            .addFunctions(createWithPermissionCheckFuns(rpe))
            .addFunctions(createOnShowRationaleCallbackFuns(rpe))
            .addFunctions(createPermissionHandlingFuns(rpe))
            .addTypes(createPermissionRequestClasses(rpe))
            .build()
}
Copy the code

You can find the contents of the file, such as comments, annotations, attributes, methods, types, etc., by looking at the generated file:

// This file was generated by PermissionsDispatcher. Do not modify! @file:JvmName("MainActivityPermissionsDispatcher") package permissions.dispatcher.sample import androidx.core.app.ActivityCompat import java.lang.ref.WeakReference import kotlin.Array import kotlin.Int import kotlin.IntArray import kotlin.String import permissions.dispatcher.PermissionRequest import permissions.dispatcher.PermissionUtils private const val REQUEST_SHOWCAMERA: Int = 0 private val PERMISSION_SHOWCAMERA: Array<String> = arrayOf("android.permission.CAMERA") fun MainActivity.showCameraWithPermissionCheck() { if (PermissionUtils.hasSelfPermissions(this, *PERMISSION_SHOWCAMERA)) { showCamera() } else { if (PermissionUtils.shouldShowRequestPermissionRationale(this, *PERMISSION_SHOWCAMERA)) { showRationaleForCamera(MainActivityShowCameraPermissionRequest(this)) } else { ActivityCompat.requestPermissions(this, PERMISSION_SHOWCAMERA, REQUEST_SHOWCAMERA) } } } fun MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) { when (requestCode) { REQUEST_SHOWCAMERA -> { if (PermissionUtils.verifyPermissions(*grantResults)) { showCamera() } else { if (! PermissionUtils.shouldShowRequestPermissionRationale(this, *PERMISSION_SHOWCAMERA)) { onCameraNeverAskAgain() } else { onCameraDenied() } } } } } private class MainActivityShowCameraPermissionRequest( target: MainActivity ) : PermissionRequest { private val weakTarget: WeakReference<MainActivity> = WeakReference(target) override fun proceed() { val target = weakTarget.get() ? : return ActivityCompat.requestPermissions(target, PERMISSION_SHOWCAMERA, REQUEST_SHOWCAMERA) } override fun cancel() { val target = weakTarget.get() ? : return target.onCameraDenied() } }Copy the code

You will find a one-to-one correspondence with the nodes added above.

After generating the file, the annotation parsing at compile time is all said and done. The difficulty is that the API for scanning annotations and generating the file is too unfamiliar.

conclusion

Compile-time annotations are much more cumbersome than run-time annotations, which can fetch annotation information and processing logic directly through reflection. You need to define an annotation parser to scan Java files for annotation information, and then poet to generate Java files.