Secrets of the API of Android devices. Yandex Report

    One of the main challenges of Android development is fragmentation. Almost every manufacturer changes Android to their needs. Developer Andrey Makeev listed the differences between vendor implementations and the original Android Open Source Project. From the report you can learn how to benefit from the individual features of the firmware on different devices.


    - I have been programming since school, I have been developing it for Android for three years. Of these, I spent a year in Yandex, participated in projects such as Launcher and Phone.



    I want to talk about how the fragmentation of the API of Android devices looks like - from the outside, from the side of application developers, and from the inside, from the point of view of developers of the platform and phones.

    My report consists of two parts. First, let's talk about how the API is fragmented externally. Then we will go through the code - we will find out how the unique feature of the abstract telephone is realized, how the development is built.

    API fragmentation is one of the parameters by which you can fragment a device. The most obvious and simplest is fragmentation according to the Android SDK, we encounter it every day, literally from the first days of development for Android. We know what and in which version of the API it appeared, that it was removed, that it was backed up, but it is still available, and which is already gone. As a rule, we use various support libraries from Google to simplify our lives. Much has already been done for us.





    In our code, it looks something like this: we turn on some functionality, turn off some - depending on which version of the SDK we are currently in. If you use any, they usually do the same, but inside.

    We will not focus on this type of fragmentation. The cons are known to everyone - we are forced to keep a whole fleet of devices with different versions in order to at least test our application. In addition, we are forced to write extra code. This is especially inconvenient when you just started developing for Android and it suddenly turns out: you need to learn what was there two or three years ago in order to support some old devices. Positive aspects: API is developing, technology is moving forward, Android is gaining new users, it is becoming more convenient for both developers and users.

    How do we work with this? We also use libraries and are very happy when we refuse to support older versions. I think in the life of everyone who has been doing this for more than a year, there was such a moment. This is just happiness. This is an obvious and simple fragmentation option. Next up is Android type fragmentation. There are several of them. Android TV speaks for itself, Android Auto is mainly intended for car radios, Android Things - for IoT and similar embedded devices. W is Wear OS, the former Android Watch, watch for Android. We will try to consider how this looks from the side of the developer. And, most interestingly, let's try to consider how it looks from the inside.



    Take two examples from developer.android.com. The first is Wear OS. What do we need to make an application? We add a compileOnly dependency to build.gradle and write two additional lines in the manifest: uses-feature android.hardware.type.watch and uses-library, which corresponds to the same package name as the library that we connected.



    How do we implement something? We create an Activity, only in this case we do not expend a standard activity that we are used to working with, and not even a composite one, but WearableActivity, and call methods specific to it, in this case setAmbientEnabled (). So, we have a compileOnly dependency, that is, it does not get into the course of our application. Uses-library, which, apparently, forces the OS to connect these classes and this code to us in runtime on the device, and the new classes that we use.



    Android Things API is practically no different. We do not prescribe uses-feature, only uses-library, compileOnly-dependency.



    We create activity, in this case it is unique to the Android Things API, the PeripheralManager class. We are polling the GPIO and trying to pledge.



    How will such an app behave on your phone? There are two options.



    If we indicated that uses-library android: required = ”true”, then we did not fulfill the mandatory requirements of the PackageManager to install the application, and it basically refuses to install it. If we specified android: required = ”false”, then the application will be installed, but when we try to access the PeripheralManager class, we get NoClassDefFoundError, because there is no such class in standard Android.

    What are the conclusions? We connect the compileOnly dependency only in order to get in touch with it during the assembly, and the classes that we use are waiting for us on the device, they are connected using certain lines in the manifest. Sometimes we prescribe a feature that is more often needed in order to distinguish on Google Play a device to which this application can or cannot be distributed.

    I could not single out the negative sides of this type of fragmentation. Only those who developed will tell many stories about how they met completely incomprehensible unfamiliar bugs that they did not encounter on the phone. The positive side is that these are additional markets, additional users, additional experience, it is always good.

    How to work with it? Write more applications. The general recommendation is to write more than one version of the application for all types of Android, but still do different ones. There should be less for a watch, for Android Things practically nothing of what is written on the phone is suitable, and so on. And use the libraries that Android developers and, sometimes, device developers provide us with.

    The next least studied type of fragmentation is producer fragmentation. Each manufacturer, having received the source code - in rare cases it is AOSP, more often it is somehow modified by the hardware developers - makes changes to it. As a rule, we learn about the negative effects of this type of fragmentation from not the best channels - from negative reviews on Google Play, because someone has broken something. Or we learn this from crash analytics, when suddenly something crashes in an incomprehensible way, only on some specific devices. In the best case, we learn this from our QA, when something broke during their testing on a specific device.



    On this topic, I have a wonderful story from our development Launcher. We got a bug report where the activity did not stretch to the full screen, and our favorite default wallpaper was not displayed at all. It was not decoded, an empty window showed. We didn’t even have devices to reproduce it. By stretching to the screen, we were still able to find a low-end device on which it did not work, and quite easily fixed everything using android: resizeableActivity = ”true” in the manifest. With the wallpaper, everything turned out much more complicated. About two days we tried to reach out and get more detailed information. In the end, they found out that on a number of devices either the codec for decoding progressive jpeg was implemented with bugs, when several compression algorithms were used on each other's results. In this case, we just wrote a lint-check, which fails the build when building the application, if we put wallpaper encoded in a progressive manner in the apk itself. Recoded all the wallpapers, repeated the situation on the backend, which distributes the rest of the wallpaper sets, and everything works great. But it cost us about two days of proceedings.



    It looks something like this in code. Unpleasant, but unfortunately, like that. Usually these lines appear after a long debugging.



    What guarantees does Google give us to ensure that the API is not broken so much that applications do not work in principle? First of all, there is a CDD, which describes what is possible and what cannot be changed, what is backward compatible and what is not. This is a document of several dozen pages with general recommendations, which, of course, will not cover all cases. To cover more cases, there is CTS, which must be completed in order for the phone to receive certification from Google and Google services can be used from it. This is a set of approximately 350,000 automated tests. There is also a CTS Verifier, a regular APK that you can put on your phone to conduct a series of checks. By the way, if you buy a phone with your hands, you can check it like that.

    With the advent of Treble, the VTS project appeared, it is more likely for developers of lower levels. It checks the driver APIs, which, starting with Project Treble, are versioned and also undergo similar tests. In addition, the phone developers themselves are healthy people who want Android applications to work fine for them, but this is so-so hope. The negative side is that we encounter unforeseen bugs that cannot be predicted until the application was launched on the device. Again, we are forced to buy, in addition to the fact that different versions of the API, there are also additional devices from different manufacturers in order to check for them.

    But there are positive aspects. At a minimum, the features most frequently implemented by manufacturers fall into Android itself. Someone may remember that the standard Fingerprint API appeared later than devices that could unlock the screen with a fingerprint. Now, according to XDA Developers, the Android API also wants to unlock using the camera in the face, but this is not yet accurate. We are likely to find out about this with you.

    In addition, device developers themselves, when they create non-standard APIs, they can, and many publish libraries for working with their API for ordinary developers. And if you have never done this before, I advise you to go over the statistics of the use of your application, see what are the most popular manufacturers, and look at the developer portals of their sites. I think you will be pleasantly surprised that many have APIs with interesting hardware features, security features, cloud services, or something else interesting. And at first glance it seems wild to write separate features for individual devices, but in addition to devices there are also manufacturers of processors, which are even smaller, which also implement their APIs. For example, Qualcomm has a wonderful hardware acceleration for recognizing images from the camera, which you can quite use,

    Thus, any of you can get some benefit even from this type of fragmentation. What are we doing with this? In no case feel free to report bugs and send bug reports to device developers and even Android developers. Because if any APIs that were worth writing the CTS test were broken, then it will be written - and there are such precedents - and after that the API becomes more reliable.

    Learn Android, learn what manufacturers offer, do not swear with them - work with them.

    What does it look like from the inside? How can I implement some feature that will be unique to our phone and use this API from a regular Android application?



    A bit of theory. How do Android developers themselves describe the internal AOSP device? The top layer is an application that was written either by you or by the developers of the phone itself, which does not have any high rights, just uses standard APIs. This is a framework, these are classes that are not part of your application, such as Activity, Parcelable, Bundle - they are part of the system, they are referred to as a framework. The classes that are available to you on the device. Next are the system services. This is what connects you to the system: ActivityManagerService, WindowManagerService, PackageManagerService, which implement the internal side of the interaction with the system.

    Next are the Hardware Abstraction Layer, this is the top layer of drivers, which contains all the logic for displaying on the screen, for interacting with Bluetooth and the like. The kernel is the bottom layer of drivers, system management. Those who know what the core is and faced, do not need to tell, but you can talk for a long time.



    How does it look on the device? Our application interacts not only with the standard framework, but can also interact with the manufacturer’s custom framework. Also, through this framework, it can communicate with custom services. If these are hardwired or low-level features, then HAL is written for them, and even drivers at the kernel level, if necessary.



    How do we write our feature? The plan is simple: you need to write a framework that is not very different from the libraries that most Android developers wrote, I think you all know this. You need to write a system service, which is an ordinary application, only with an not quite ordinary set of rights in the system. And if necessary, you can write HAL, but we omit this. You can write your own drivers at the kernel level, but we will not consider it now either. And write a client application that will use all this.



    In order for us to interact with the system, we need to write some kind of contract, and for this there is already a good mechanism for AIDL interfaces. This is just a kind of interface on the basis of which the system generates an additional class that we can extend, through which interprocess communication between your application and system services is carried out.



    Next, we write a framework, our library, which holds the implementation of this interface, and proxies all calls to it. If you're interested, then ActivityManager, PackageManager, WindowManager work in much the same way. There is a little more logic than we have implemented here, but the essence is just that.



    We implemented the framework, we need to write a system service that will receive our data from the system side, in this case we transmit and read integer. We create a class, it also extends the interface that was generated from AIDL on the previous slide. We create a field in which we write values, read, write a setter, getter. The only thing is that there are not enough locks, but they did not fit on the slide very much, they should be done.



    Further, in order for this system service to be available, we must register it in the system service manager, and in this case it is the same class that is not available to ordinary applications. It is available precisely to those that are in the platform in the system partitions. We register the service simply in Application.onCreate (), make it available under the name of the class that we created.

    What do we need for onCreate () to basically start and our service to be loaded into memory? We write in the manifest in application android: persistent = ”true”. This means that this is a persistent process, it must be in memory constantly, because it carries out system functions.



    Also in the manifest itself we can specify android: sharedUserId, in this case system, but it can be a wide range of different IDs, they allow the application to get wider rights in the system, interact with various APIs and services that are not available to ordinary applications.

    In this case, for example, we did not use anything like this.



    We wrote a framework, wrote a system service. We will omit the mechanisms inside, this is a somewhat complicated topic, it deserves a separate report.

    How to deliver the framework to application developers? Two formats. We can issue full-fledged classes and make a full-fledged library that you compile into your application, and all the logic will become part of your dexes.

    Or you can distribute the framework in the form of stub classes, relative to which you can only link at compile time and expect that these classes will wait for you similarly to previous examples from various versions of Android on the device itself. You can distribute it either through a regular Maven repository that everyone knows, or through Android Studio sdkmanager, similar to how you install new versions of the SDK. It is difficult to say which method is more convenient. It’s more convenient for me personally to connect Maven.



    We are writing a simple application. In a familiar way, we connect the compileOnly dependency, only now it is our library. We prescribe uses-library which we wrote and which put on the device. We write Activity, get access to these classes, interact with the system. So it would be possible to implement absolutely any features: data transfer to some additional devices, additional hardware features, etc.

    Thus, all developers make unique features of the device available to developers. Sometimes these are private APIs that give only to partners. Sometimes they are public and those that you can find on developer portals. There are other ways to implement such things, but I described a method that is considered the main one in Google and among Android developers.

    You should not treat device developers as people who break your applications. These are the same developers, they write the same code, at about the same level. Write bug reports, they really help, I often analyze them. Write more applications and take advantage of the opportunities that both Android and the device itself provide. That's all.

    Also popular now: