• preface
  • preview
  • permissions4m-annotation
  • permissions4m-processor

    • AnnotationProcessor
    • ProxyInfo
    • AnnotationProcessor coding
    • ProxyInfo coding
  • permissions4m-api
  • The latter

preface

Permissions4M, an Android compile-time annotation framework library, deals with Android 6.0 runtime permissions. This library is based on the secondary development of MPermissions by hongyang predecessors. Currently, several mainstream compile-time processing frameworks in Android include the well-known Dagger2 and Butterknife. I hope that after reading this blog and the author’s framework, Can help you readers a deeper step to help you understand the Android compiler annotation processing technology, so I hope readers in reading the author of this blog, please first understand some of the annotations, to Android 6.0 runtime permissions have a certain understanding, better is to try the author’s library, easy to understand the content behind the author. Recommended reading:

preview

In order to facilitate readers to understand Permissions4M, the author specially draws the following picture:

  • Compile the front:

Before you compile the program (to make it easier to understand, the two characters, the author does not use program later directly using the “Activity” to instantiate this two word) in addition to write their own code, involves the annotations library content can be divided into two parts, one part is annotations, another part is supposed to be what you call API, For example, in Butterknife, you might use the @bindView annotation when using the API butterknife.bind (this); You may not be able to subdivide modules when writing, but if you are designing an annotation library, you should separate the two parts, one is for modular development of aspects, and the other is for the module library of the annotator to be developed later.

  • In the compilation:

During compilation, our annotation handler will come into play. It will scan the Activity, extract and process information, and finally concatenate a proxy class. Similarly, our annotation handler should be a new module separate from the annotation module and API module. Therefore, a total of three modules are needed for development, namely THE API module, which is used to expose users to use; Annotation modules, although also exposed to the user, should be distinguished from API modules and perform their respective functions; The annotation handler module, which shields the user, only generates proxy classes at the compiler for activities that use annotations.

  • The compiled:

There has been no mention of proxy classes and API modules. In fact, API modules operate on generated proxy classes to achieve desired purposes, such as butterknip.bind (this); FindViewById () = @bindView (); findViewById() = @bindView (); @bindView () = @bindView (); I’m sorry I don’t know, because I haven’t seen the source code for Butterknife. But I can tell you how Permissions4M corresponds to @permissionSGranted, @permissionsdenied, and @permissionSrationale.

permissions4m-annotation

In Android Studio, click File -> New -> Module in the upper left corner of the menu and select Java Module. Why Java Module? As mentioned earlier, this library only involves the annotations used, so the Java type library can meet all our needs, there is no need to use android Library, otherwise it will involve extra resource files, which will make our library size larger. Permissions4m – Annotation Module screenshot below:

The first four annotations can be grouped into the following categories: callback for custom permission secondary request, callback for permission rejection, callback for permission pass, and callback for permission secondary request. PermissionsGranted PermissionsGranted PermissionsGranted

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PermissionsGranted {
    int[] value();
}
Copy the code

Although I mentioned earlier that I wanted you to have a little bit of annotation background, I would like to point out that we need to use @Retention(retentionPolicy.class) annotations and @target (elementType.method) to mark our annotation interface. The former preserves information about the interface only until the.class period, while the latter indicates that the object that the interface decorates is a method. What does int[] value() mean? It means that if we use @permissionsgranted annotations like this, it’s not ok

@PermissionsGranted()
public void grant() {
}
Copy the code

Instead, it should be used like this:

@PermissionsGranted({1, 2})
public void grant(int code) {
}
Copy the code

or

@PermissionsGranted(1)
public void granted() {
}
Copy the code

So the simple thing is that we need to restrict the interface to passing in an integer array, so why do we need to pass in an integer array? The @permissionsgranted annotation supports callbacks for any permission request. The @permissionsgranted annotation supports callbacks for any permission request. @permissionsgranted the @permissionSgranted annotation takes an integer value as the argument to the method. The possible value of this parameter is the value of the array.

@PermissionsGranted({1, 2}) public void grant(int code) { switch (code) { case 1: break; case 2: break; case 3: // nerver break; default: break; }}Copy the code

If you’re still confused here, don’t worry, I’ll come back to this later. Specific source view: permissions4m – an annotation jokermonn/permissions4m/permissions4m – the annotation. The source code for the Permissions4m-Annotation Module is very simple

permissions4m-processor

This is the hardest and most difficult Module to understand, but don’t panic. The first step is to create a New Java Module in Android Studio by going to File -> New -> Module. Why a Java Module? First, as mentioned in Permissions4m-Annotation, we can already use the Java library for our design, and second, Android has deleted the Javax. annotation class. So Android libraries don’t support custom processors very well, so we’ll use the Java Module. After the module is built, you need to open build.gradle under the Module to add two lines of dependencies. The first line is compile Project (‘: Permissions4m-annotation ‘) which means the PermissionS4m-Annotation library dependency, The second line is the compile ‘com. Google. Auto. Services: auto – service: 1.0 -rc3’, the library can better assist us to complete the custom annotation processor design. The permissions4m-Processor Module screenshot is as follows:

Source: see jokermonn/permissions4m/permissions4m – processor

AnnotationProcessor

AnnotationProcessor inherits AbstractProcessor, in fact, it needs to complete two things, one is to obtain the information we need, and the other is to command ProxyInfo to generate the corresponding proxy class code, the first set of routines are as follows:

1. Constructor

private Elements mUtils;
private Filer mFiler;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    mUtils = processingEnv.getElementUtils();
    mFiler = processingEnv.getFiler();
}
Copy the code

MUtils can help us get information about classes that use annotations, class packages, method information, and so on, so mUtils is a very powerful and essential tool class for getting information. MFiler is the class that generates Java files, which is the utility class that we will use later to generate proxy classes.

2. Overwrite method

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> set = new HashSet<>(5);
    set.add(PermissionsGranted.class.getCanonicalName());
    set.add(PermissionsDenied.class.getCanonicalName());
    set.add(PermissionsRationale.class.getCanonicalName());
    set.add(PermissionsCustomRationale.class.getCanonicalName());
    set.add(PermissionsRequestSync.class.getCanonicalName());

    return set;
}

@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}
Copy the code

GetSupportedAnnotationTypes () of what you need to deal with annotations getCanonicalName () Set and returns can be, And getSupportedSourceVersion (). You only need to return to the SourceVersion latestSupported (); Can.

3. Class header annotations

@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {
}
Copy the code

A set of conventions that annotate the current class with @autoService (processor.class).

4. Overwrite the process (Set

ProxyInfo

The ProxyInfo class provides the code for the proxy class, so we should first think about what we need to write a proxy class. For example, if we use our annotations in MainActivity, the generated MainActivity proxy class needs the package name of the MainActivity, the array parameters passed in the annotation, the method name of the method that the annotation modifies, and so on. With that said, we’re almost done, and we’re ready to start the formal code writing process.

AnnotationProcessor coding

As mentioned earlier, the core method is process(), and the real logical business code in the annotator only needs to be written in this method. The source of the process method in permissions4m-Processor is as follows:

First, the handler does nothing when the method returns false and skips the current first loop, similar to continue; , and should return true if you want the processor to process it. The method is divided into two parts. The first part is to extract relevant information needed by the proxy class and stuff it to ProxyInfo. The second part is to use mFiler + ProxyInfo to generate the proxy class we need. Is it so easy? The good news is that part 2 is a set piece that doesn’t need to be flexible. The other good news is that the source code for Part 1 isn’t really that hard. The first is to create a HashMap, key/value pair for String – ProxyInfo, practical significance is to use the full path to the class notes – ProxyInfo, such as “com. Joker. Test. MainActivity” – ProxyInfo,” Com. Joker. Test. – ProxyInfo SecondActivity “, mentioned earlier, this method will be circular calls for many times, so to avoid a repeat of the proxy class, avoid the class name of the generated classes is abnormal. , we need to do map.clear() at the beginning. IsAnnotatedWithMethod (RoundEnvironment roundEnv, Class
clazz);

private boolean isAnnotatedWithMethod(RoundEnvironment roundEnv, Class<? extends Annotation> clazz) {
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(clazz);
    for (Element element : elements) {
        if (isValid(element)) {
            return false;
        }
        ExecutableElement method = (ExecutableElement) element;
        TypeElement typeElement = (TypeElement) method.getEnclosingElement();
        String typeName = typeElement.getQualifiedName().toString();
        ProxyInfo info = map.get(typeName);
        if (info == null) {
            info = new ProxyInfo(mUtils, typeElement);
            map.put(typeName, info);
        }

        Annotation annotation = method.getAnnotation(clazz);
        String methodName = method.getSimpleName().toString();
        if (annotation instanceof PermissionsGranted) {
            int[] value = ((PermissionsGranted) annotation).value();
            if (value.length > 1) {
                info.grantedMap.put(methodName, value);
            } else {
                info.singleGrantMap.put(value[0], methodName);
            }
        }
    }

    return true;
}
Copy the code

ExecutableElement Method = (ExecutableElement) Element; As we started parsing, we were able to ensure that annotations could be used as we wanted (methods must be annotated, public, and non-abstract), so we forced Element to be an ExecutableElement type. To represent it as a method, then we pass getEnclosingElement(); GetQualifiedName ().toString(). This function gets the current class type. TypeElement has a handy method called getQualifiedName().toString() that prints the name of a TypeElement. That’s the class name. If we find that ProxyInfo is empty, we can put a new ProxyInfo to map. To make a new ProxyInfo, we need two parameters, one is mUtils. We will most likely need to use it in ProxyInfo to get more information), one is TypeElement (our generated proxy class needs to be in the same package as it, and we also need the type of this class, For example, our TypeElement can be an Activity or Fragment, and we need to invoke different permission application apis for each type. Here are about the same, the last thing we need is the parameter annotation, we definitely will need to use it, and get the way is simple, the above code already very clear, do extends the author here, need to suggest that the author for each annotation provides two map, This is because there is a difference between the function generated when there is only one argument and when there are multiple arguments, which will be extended later.

ProxyInfo coding

thinking

Before writing the proxy class, I want you to consider from the overall point of view, we said earlier, API Module is actually called the proxy class method to complete, so certainly said that the proxy class must have a fixed method for API Module to call, so how to have a fixed method? The answer is the interface, and this part of the knowledge is involved in permissions4m-lib. But an interface is very easy to understand, the author is here to release the source of the interface –

public interface PermissionsProxy<T> {
    void rationale(T object, int code);

    void denied(T object, int code);

    void granted(T object, int code);

    boolean customRationale(T object, int code);

    void startSyncRequestPermissionsMethod(T object);
}
Copy the code

The first method is called when the permission is rejected, the third method is called when the permission is approved, and the fourth method refers to whether the custom second request dialog box is used. If so, it will be passed to the method modified by @permissionSrationale. If not, Then transferred to @ PermissionsCustomRationale the modification method, and finally a method is invoked when applying for more permissions synchronization API. The first parameter passed in the method is a generic type, which can only be an Activity or Fragment type, since only an Activity or Fragment can apply for permissions in Android.

coding

So that’s the thinking part, and here’s the actual coding in ProxyInfo. Again, to review what we need to create a proxy class, we can open a class and look from the top of the class:

  • Package name: in ProxyInfo we pass inmUtils.getPackageOf(typlElement).getQualifiedName().toString();available
  • Referenced external classes: Our generated proxy class only needs one external class, which is the interface mentioned above, so simply “import” + permissions-lib package name
  • Proxy class name: For MainActivity we want its proxy class name to be MainActivity$$PermissionsProxy, first we usetypeElement.getQualifiedName().toString().substring(packageName.length + 1)

    .replace('.', '$');
    You can get the string “MainActivity” and add “? “PermissionsProxy” is enough, of course, but don’t forget that this class needs to implement the PerimissionsProxy interface.

The code is as follows:

StringBuilder builder = new StringBuilder(); builder.append("package ").append(packageName).append("; \n\n") .append("import com.joker.api.*; \n\n") .append("public class ").append(proxyName).append(" implements ").append (PERMISSIONS_PROXY).append ("<").append(element.getSimpleName()).append("> {\n");Copy the code

One thing to note here is that you don’t need to worry about putting too many semicolons and too few curly braces. If you put too many or too few curly braces, you will get an error if you don’t follow Java syntax, so don’t worry about it.

  • granted/denied

    The first thing to do is write one@OverrideTo help us make sure the method name is correct, then add the above method name and method parameters. ProxyInfo has several maps containing method names and resquestCode, as follows:

So we need to traverse the map, each time the key is the method name, and the value is the corresponding request code. Permissions4m is the same as permissions4m. Permissions4m is the same as permissionS4m. Permissions4m is the same as permissionS4m. If the request is granted/denied, the code for the next request should be written in the granted/denied method. The source code is as follows:

private void generateDeniedMethod(StringBuilder builder) { checkBuilderNonNull(builder); builder.append("@Override\n").append("public void denied(").append(element.getSimpleName()) .append(" object, int code) {\n") .append("switch(code) {"); for (String methodName : deniedMap.keySet()) { int[] ints = deniedMap.get(methodName); for (int requestCode : ints) { builder.append("case ").append(requestCode).append(":\n{"); builder.append("object.").append(methodName).append("(").append(requestCode).append("); \n"); // judge whether need write request permission method addSyncRequestPermissionMethod(builder, requestCode); builder.append("break; }\n"); if (singleDeniedMap.containsKey(requestCode)) { singleDeniedMap.remove(requestCode); } } } for (Integer requestCode : singleDeniedMap.keySet()) { builder.append("case ").append(requestCode).append(": {\n") .append("object.").append(singleDeniedMap.get(requestCode)).append("(); \nbreak; \n}"); } builder.append("default:\nbreak; \n").append("}\n}\n\n"); }Copy the code

Once again, this seems very obscure, but it is very quick to understand when you compare the compiled.class files

  • Rationale comment/customRationale similar to granted/denied, we use a similar routine code completion, and rationale comment method does not need to do extra work. In the customRationale, we iterate over the map associated with the customRationale, and return true after retrieving the corresponding code, indicating that the developer will customize the secondary permission request callback. The rationale function for any function that returns false is described in the rationale function, which is written in permissions4m-lib.

  • StartSyncRequestPermissionsMethod multiple access sync application function written content is very good, we only need to extract the synchronous application permissions map first rights of information, Permissions4m-lib calls the API provided by permissions4m-lib to synchronize the request, because the first authorization success function and the authorization failure function will be called back to the next permission request, reference code as follows:

permissions4m-api

This is the only Android in three module module, the Android Studio, select the File | New | module, and then select the Android library to create. Why use Android Library? Because Permissions4M-API is a developer-oriented library and involves activities and Fragment classes, you must use the Android Library. Jokermonn/permissions4m/permissions4m – API structure is as follows:

The Permissions4M interface is an interface exposed to the developer, which exposes the following methods:

  • syncRequestPermissions(Activity activity)
  • syncRequestPermissions(android.app.Fragment fragment)
  • syncRequestPermissions(android.support.v4.app.Fragment fragment)

Apply for multiple permissions synchronously, source code is as follows:

The code is basically the same. If the current version of the application is lower than Android 6.0, nothing is done. Otherwise, initProxy() is called, passing the class as an argument to the function:

Source code is very simple, first get the className of the incoming class, in the splice into the name of the proxy class, and then use reflection to instantiate the proxy class. Let’s look again at the third step in syncRequestPermissions(), the syncRequest() method —

Be clear at a glance, is actually call the proxy class startSyncRequestPermissionsMethod (). Let’s clean up the process again – make sure the current mobile version is greater than 6.0 -> instantiate the proxy class -> invoke the proxy class’s multiple permission synchronization request methods

In fact, not only for the syncRequestPermissions() method, but also for requestPermission(), a common permission request method that permissions4m exposes to developers

The key point is the request method

Although the code looks a bit much, but if the reader has done Android 6.0 dynamic permission adaptation experience, for this code should be more familiar, I take the Activity for example, First is to call ContextCompat. CheckSelfPermission ((Activity) object, permission)! = PackageManager. PERMISSION_GRANTED) method to check whether the permission has been granted, if has been granted, then call the proxy class granted () method. If has not been awarded, then through the (object) (Activity). ShouldShowRequestPermissionRationale (permission) method determine whether the application should display the corresponding message, if you don’t show, You should direct call ActivityCompat. RequestPermissions ((Activity) object, new String [] {permission}, requestCode); Request permission, or else display information. In the module that displays the information, we need to determine whether the developer has customized the callback method for the secondary permission request. If not, we need to go to the normal secondary callback method and display the @permissionSrationale annotation modification method. Then is called ActivityCompat. RequestPermissions ((Activity) object, new String [] {permission}, requestCode); Apply for permission.

In plain English, the Activity permission request process is handled by Permissions4M, which calls the corresponding proxy method.

The latter

If you have any questions, please leave a message or mention issues to Permissions4m. Recently, THE author has also studied the development of IDEA plug-in, hoping to develop the plug-in of Permissions4M as soon as possible to reduce the burden of developers. Finally make a small advertisement for oneself again, after the summer vacation is a big four, want to find a better internship at present, if each reader has recommendation or can be pushed, hope to send an email to my mailbox [email protected]. Thank you very much!