You can fix the 0.1% === At Qonto we care for every single customer and don't hesitate to fix uncanny bugs for your experience and the community. ## TL;DR: - Google added the TextInputLayout component in its Support Library - Meizu modified the AOSP and in particular the Editor class - Further evolutions of the TextInputLayout component introduced a crash on Meizu devices only - We understood the root cause of those crashes and decided to update the Android Support Library to fix it # Part 1 - Meizu and Material crash's resolution In november 2018, the Android team released their library [Material Component](https://material.io/develop/android/). Since then, all applications containing the [TextInputEditText](https://developer.android.com/reference/com/google/android/material/textfield/TextInputEditText.html) running on Meizu devices experienced crashes. ## Workaround This crash led to a lot of noise on the issue tracker of [Android applications and libraries](https://github.com/search?q=Meizu+Attempt+to+invoke+virtual+method+%27int+android.text.Layout.getLineForOffset%28int%29%27&type=Issues), [on StackOverflow](https://stackoverflow.com/questions/51891415/nullpointerexception-on-meizu-devices-in-editor-updatecursorpositionmz), and eventually on the [Android issue tracker](https://issuetracker.google.com/issues/112105087). A workaround was quickly found: instead of using **TextInputEditText** inside a **TextInputLayout**, prefer an **[AppCompatEditText](https://developer.android.com/reference/kotlin/androidx/appcompat/widget/AppCompatEditText)** (*which supports compatible features on older versions of the platform*) or an **EditText**, inside a **TextInputLayout**. It does work by getting rid of the **TextInputEditText** modification (see part 2. for more details), which fixed accessibility issues. But by using this workaround, developers create unusable interface for visually impaired users. ## Investigation The main issue comes from Meizu modifying the AOSP, but as Meizu doesn't update its devices and because its modifications are not Open Source, this path looks like a dead-end. On the other hand, the [Material Component library](https://github.com/material-components) (containing the TextInputLayout) is **Open Source**, available on Github and **is updated every month**. Moreover, the Android team had a look at this Meizu issue and found [the commit](https://github.com/material-components/material-components-android/commit/3e20d0720fe261b551aeab277cc7b886c6381fba) that led to the crash. But since they do not own any Meizu device, they [encouraged the community](https://issuetracker.google.com/issues/112105087#comment12) to help out and propose a fix through a merge request. As our company, [Qonto](https://qonto.eu), bought us a Meizu device to test the above workaround, we decided to understand and fix the issue. First of all we had to understand 3 things: - What is the purpose of the Android team [commit](https://github.com/material-components/material-components-android/commit/3e20d0720fe261b551aeab277cc7b886c6381fba)? - What is the purpose of the [Meizu framework modification](https://github.com/FlymeOS/devices-base/blob/02fbf97d856ce94795ca0cb1ce96f57bbd3291ad/framework.jar.out/smali_classes2/android/widget/Editor.smali#L9291)? - Where is the conflict leading to the crash? #### The purpose of the Android team commit Reading the [commit](https://github.com/material-components/material-components-android/commit/3e20d0720fe261b551aeab277cc7b886c6381fba) on Github might be enough to understand its purpose, thanks to the commit description, the comments in the code and the code itself. #### The purpose of the Meizu framework modification, and the conflict leading to the crash To investigate, we cloned the [Material Component repository](https://github.com/material-components) on our workstation and followed the repo documentation to know [**How to build**](https://github.com/material-components/material-components-android/blob/master/docs/building-from-source.md) and [**How to contribute**](https://github.com/material-components/material-components-android/blob/master/docs/contributing.md). Eventually, it's even simpler to only open the whole project in [Android Studio](https://developer.android.com/studio) and test the included [**Catalog** showcase app](https://github.com/material-components/material-components-android/tree/master/catalog). This way we can modify the library then test the code directly in the **Catalog** app. First, we ran the **Catalog** app on the Meizu device and confirmed the crash. We then did the same steps with an emulator or another device and confirmed there is no crash. ![Same EditText crash on Meizu and not on Emulator](https://i.imgur.com/lemErSX.gif) Now that we did confirm the bug, we put some debug break points in the library code following what we learned from the crash stacktrace and from the commit leading to the crash. We added a **breakpoint** in the **`TextInputEditText.getHint()`** method to **see the following executed code**: ![Same code run on Meizu device and on AOSP Emulator](https://i.imgur.com/lVuZ3Qy.gif) From that debug trace we were able to reverse engineer that part of the code:: ```java= void updateCursorsPositionsMz(...) { [...] if (!TextUtils.isEmpty(mTextView.getHint()) && TextUtils.isEmpty(mTextView.getText()) { // getHint() overriden by TextInputEditText // return the TextInputLayout so the test is true Layout hintLayout = mTextView.getHintLayout(); // getHintLayout() is a final TextView class. // As TextView.mHint is null, it returns null, but the // developer assumed it will always return a non-null value Int value = hintLayout.getLineForOffset(offset); // As hintLayout is null, this leads to a NullPointerException [...] } } ``` We discovered the [Meizu sources](https://github.com/FlymeOS/devices-base/blob/02fbf97d856ce94795ca0cb1ce96f57bbd3291ad/framework.jar.out/smali_classes2/android/widget/Editor.smali#L9291) while writing this post, which confirmed our guesses. The purpose of the Meizu modification might be that they want to **draw the cursor slighly differently when there is a hint and no text**, but we didn't observe this kind of behaviour in the Meizu apps. ## Fixing the problem After this investigation, we shared our results on the related [Google issue thread](https://issuetracker.google.com/issues/112105087#comment13). The first idea to fix the issue was to [ignore the TextInputEditText.getHint()](https://github.com/material-components/material-components-android/pull/358/commits/2d122c1e8aed4a19577321f1a272b107326a3f23) modification if the device OEM running the code is Meizu. As we had to add an OEM's exception in a generic code, we were not confident with this solution. However it was the best we found, and it worked pretty well if we look at the piece of code above: - in the Meizu `updateCursorsPositionsMz()` method, the `mTextView.getHint()` is now done on the **TextInputEditText**'s parent, **the TextView**. - The returned value is now `null` and not the `TextInputLayout.mHint` - The crashing `if` condition is not run anymore. We submitted this modification as a commit in a **Pull Request** in the [Android Material Component repository](https://github.com/material-components). After a quick signature on the [Google Contributor License Agreement](https://cla.developers.google.com/), the review started with [Cameron Ketcham](https://github.com/cketcham) as reviewer and maintainer of the library. Cameron asked me to do more tests and to try other fixes. After some more investigations and reviews from his team, we finally agreed on another kind of solution: if the code runs on a Meizu device and if the **TextView** doesn't provides a hint, we will set it an empty one at the View creation: ```java= @Override protected void onAttachedToWindow() { // Meizu devices expect a hintLayout if the hint is not null. In order to avoid crashing, // we force the creation of the hintLayout by setting an empty non null hint. TextInputLayout layout = getTextInputLayout(); if (layout != null && layout.isProvidingHint() && super.getHint() == null && Build.MANUFACTURER.equals("Meizu")) { setHint(""); } super.onAttachedToWindow(); } ``` With this solution, on a Meizu device, the `TextInputEditText.getHint()` still returns the **TextInputLayout's hint** so the accessibility update of the library still works as expected. Moreover, as `TextView.setHint("")` is called, a `TextView.mHintLayout` is created. Then, in the Meizu `updateCursorsPositionsMz()` method, the crashing`if` condition can now call `mTextView.getHintLayout().getLineForOffset(offset)` without `NullPointerException`. ![Fix result step by step on Meizu device](https://i.imgur.com/BWMxyL1.gif) This last commit was [accepted by the Android team](https://github.com/material-components/material-components-android/pull/358#event-2347745965), the Google issue [was closed](https://issuetracker.google.com/issues/112105087#comment14), the code was merged [in the master branch](https://github.com/material-components/material-components-android/commit/ccb7fbe8b37281142698f37a7554d4a2f0afe0e1) and released in the [1.1.0-alpha07 version of the library](https://github.com/material-components/material-components-android/tree/1.1.0-alpha07). ## Results and Conclusion Even if it a fix for only 0.01% to 1% of the market, it has been really appreciated by the Android developers community. What made this fix interesting is that it only took one particular phone and a **few hours** of cumulative work **to understand and find the best fix** possible. Moreover, **the collaboration with the Android Team was excellent** and really reactive even if it was our first contribution to the Google source code. As a last word, fixing the 0.1% is essential as it is not always difficult, it shows you care about all your users, and it can help not only your users but also all the other applications' users. ![Fixing is Caring](https://i.imgur.com/cZ7KUxH.jpg) Sorry for the picture ;) ... # Part 2 - How AOSP creates incompatibilities In that second part of our "You can fix the 0.1%" series we analyse the origin of the Meizu's crash we fixed in part one. ## AOSP, fragmentation and Support Library The [AOSP *(Android Open Source Project)*](https://source.android.com/) is the whole code of the Android Operating System. It is Open Source and is the base of code for all Android device OEM (*Original Equipment Manufacturer*, or vendors). OEM can use it out of the box (or with almost no chang) like in the [Nexus](https://en.wikipedia.org/wiki/Google_Nexus) and [Pixel](https://en.wikipedia.org/wiki/Google_Pixel) Google's devices, or completely change it like in the [Amazon's Fire tablet](https://en.wikipedia.org/wiki/Amazon_Fire_tablet). Android currently has a [huge fragmentation between its different versions](https://developer.android.com/about/dashboards): - [15 versions of Android](https://en.wikipedia.org/wiki/Android_version_history) were released in the last 10 years - OEMs can decide whether to update or not their devices - Updates can be difficult if they target complicated or important changes. ![Android fragmentation](https://chart.googleapis.com/chart?chf=bg,s,00000000&chd=t:0.3,0.3,3.2,6.9,14.5,16.9,19.2,28.3,10.4&chco=c4df9b,6fad0c&chl=Gingerbread%7CIce%20Cream%20Sandwich%7CJelly%20Bean%7CKitKat%7CLollipop%7CMarshmallow%7CNougat%7COreo%7CPie&cht=p&chs=500x250) To tackle that issue, Google created in march 2011 the [Support Library](https://developer.android.com/topic/libraries/support-library) so **developers could use recent framework features on old Android versions**. For example, when Google released the [TextInputLayout](https://developer.android.com/reference/android/support/design/widget/TextInputLayout), it was first added in the [Design package of the Support Library](https://developer.android.com/reference/android/support/packages) (and moved in march 2018 in the [Material Component package](https://developer.android.com/reference/com/google/android/material/packages)) instead of the AOSP. Android developers can embed this library in their application and use this component. Then, the TextInputLayout can be used on very old Android devices with old versions of Android without updating anything but their usual applications. ## OEMs and the AOSP modifications Since the first public version of Android ([Android 1.5 - Cupcake](https://developer.android.com/about/versions/android-1.5-highlights.html)), the whole [source code](https://android-review.googlesource.com/q/status:open) is available (mainly) under the [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) license. > "the license allows the user of the software the freedom to use the software for any purpose, to distribute it, to modify it, and to distribute modified versions of the software, under the terms of the license, without concern for royalties" - [Wikipedia](https://en.wikipedia.org/wiki/Apache_License) Manufacturers must adapt core parts of their system so [device sensors](https://source.android.com/devices/sensors) and physical elements are recognized by the system. That could be the end of the story (like for the Google [Nexus](https://en.wikipedia.org/wiki/Google_Nexus)' devices) but manufacturers like to customize their devices to reflect brand identity. Those **overlays** are a big mix of UI modifications, base applications (launcher, sms, phone, calculator...) modifications or replacements, features additions, branding applications and even bloatwares. Samsung overlay [TouchWiz](https://www.androidpit.com/why-i-will-never-go-back-to-touchwiz) was probably the most infamous one for developers and end-users. And that's where the bad things happens: - when facing framework limitations, Android developers must understand how it works and extend it to create what they need, using only the API (*public* part) of the framework. The advantage of this solution is that the [API will be supported forever](https://youtu.be/b5Yj7re7u1w?t=1226), and so their solutions should work with any (new) version of Android. - On the opposite, system UI overlays developers, when facing a limitation with Android can modify it themself. This way they can do whatever they want, making the code simpler (they generally do the opposite though) and easily reuse their solutions in all the overlay. There are two main issues with the OEMs overlays: - The first one is a problem on the end-users side: complex framework changes will be even harder to maintain in following Android versions. And as OEMs release new smartphone with the new Android versions, they usually don’t bother developing updates for old devices, which leads to an augmentation of the fragmentation. - The second problem is on the application developers side: By changing the framework, the overlay developers changes its normal behaviour and may lead to unexpected situations. Here are some situations of bad overlay modifications that leads to strange behaviour: - Battery optimizations abuses [break apps](https://dontkillmyapp.com/), leading [VLC to ban Huawei](https://www.theverge.com/2018/7/25/17614014/vlc-blacklisting-recent-huawei-devices-negative-app-reviews) from the Play store - LG *Menu button* modifications leads to [crash when using the Support Library](https://issuetracker.google.com/issues/37008260) - [Huawei devices crashing](https://stackoverflow.com/questions/56159347/mediastyle-remoteserviceexception-bad-notification-posted-from-package) when trying to apply a style to push notifications - Samsung's white color (@android:color/white) [is not white](https://twitter.com/mandybess/status/910556038461407232) (Oneplus and Huawei seems to made this error too) - [Realm database cannot be opened](https://github.com/realm/realm-java/issues/5715) on Huawei devices - [TextInputEditText crash on Meizu](https://issuetracker.google.com/issues/112105087) devices We will focus on this last modification and its implications. ## The Meizu modification [Meizu](https://en.wikipedia.org/wiki/Meizu) is a Chinese OEM with a very small part of the smartphone marketshare. If its first goal is the Chinese market (1.06%) with smartphones only available in China, it also releases some smartphones worldwide (0.09%), especially in Europe (0.16%) but almost nothing in US (0.01%) ([sources](https://docs.google.com/spreadsheets/d/1rbQGVvHnXQs7uuLiUncxfO5fvWIS5I27LYhrz8uqvdY/edit?usp=sharing)) Since 2012, Meizu has been developing its own system UI overlay called [Flyme](https://en.wikipedia.org/wiki/Meizu#Flyme), with international (understand "outside China") versions since the end of 2015. International versions are not Meizu priority so device owners should not expect system updates or bug fixes. Flyme Overlay is quite successful with the dev community who created *kind-of* [open source versions](https://github.com/FlymeOS) of it which helped us getting the root cause of the crash. It is barely readable but here is one of the framework modification they did: 1. When getting focus, the [TextView](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java#4710) will [define and update the cursors positions](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java#4710), using the [hidden utils class Editor](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/Editor.java#107) ([`Editor.updateCursorsPositions()`](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/Editor.java#1412)) in order to draw it. 2. This last method should call [`Editor.updateCursorPosition()`](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/Editor.java#1610) but instead **Meizu changed the code to call a new implementation**, [`Editor.updateCursorPosition**Mz**()`](https://github.com/FlymeOS/devices-base/blob/02fbf97d856ce94795ca0cb1ce96f57bbd3291ad/framework.jar.out/smali_classes2/android/widget/Editor.smali#L9291). 3. This last method should calculate how to draw the cursor depending on the parameters. The Meizu new implementation will do a more complicated calculation depending on whether or not [the TextView has a hint](https://github.com/FlymeOS/devices-base/blob/02fbf97d856ce94795ca0cb1ce96f57bbd3291ad/framework.jar.out/smali_classes2/android/widget/Editor.smali#L9314) and [a text](https://github.com/FlymeOS/devices-base/blob/02fbf97d856ce94795ca0cb1ce96f57bbd3291ad/framework.jar.out/smali_classes2/android/widget/Editor.smali#L9326). 4. If the TextView has a hint (**`!TextView.getHint().isEmpty()`**) and if the TextView has no text (**`TextView.getText().isEmpty()`**), it will [get the **`TextView.mHintLayout`**](https://github.com/FlymeOS/devices-base/blob/02fbf97d856ce94795ca0cb1ce96f57bbd3291ad/framework.jar.out/smali_classes2/android/widget/Editor.smali#L9339) in order to [get its **`Layout.getLineForOffset()`**](https://github.com/FlymeOS/devices-base/blob/02fbf97d856ce94795ca0cb1ce96f57bbd3291ad/framework.jar.out/smali_classes2/android/widget/Editor.smali#L9346) and do its calculations If you didn't notice, there is a problem in this code: it assumes that if [**getHint()**](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java#3670) is not empty, then [**getHintLayout()**](https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java#1318) must return a not null value. We will see later on that this assumption is incorrect. ## The TextInputLayout In may 2015, Google released the [version 22.2.0](https://developer.android.com/topic/libraries/support-library/rev-archive#rev21-2-0) of its [Android Support Library](https://developer.android.com/topic/libraries/support-library). This version introduced the [TextInputLayout](https://developer.android.com/reference/com/google/android/material/textfield/TextInputLayout.html) component *for showing EditText hint and error text as floating labels*. [In april 2018](https://github.com/material-components/material-components-android/commits/1.0.0-alpha1), this component was moved to the [Material Component library](https://material.io/develop/android/). ![TextInputLayout example](https://i.imgur.com/gKLA3Kb.gif) > The idea of **float label pattern** was introduced on [septembre 2013](http://mds.is/float-label-pattern/) by the Design Director of [Studio Mds](http://mds.is/), [Matt D. Smith](http://mds.is/matt/). Several developers created their Android implementations during the year 2014, until the Google Android developer [Chris Banes](https://gist.github.com/chrisbanes) introduced the [FloatLabelLayout](https://gist.github.com/chrisbanes/11247418). One year later, this implementation would be added to the Android Support Library as **TextInputLayout** and became part of the [Material Design](https://material.io/design/components/text-fields.html) guidelines. At the end of may 2018, the Android team from Google NYC released a new version of their library Material Component in their [alpha version 1.0.0-alpha3](https://github.com/material-components/material-components-android/commits/1.0.0-alpha3), and a [stable version 1.0.0](https://github.com/material-components/material-components-android/commits/1.0.0) in november 2018. This version provides [accessibility support for TextInputLayout](https://github.com/material-components/material-components-android/commit/3e20d0720fe261b551aeab277cc7b886c6381fba): developers must [provide a hint](https://developer.android.com/reference/kotlin/android/widget/TextView.html#setHint(kotlin.CharSequence)) to the user to describe the EditText content, that will be shown as a placeholder of the EditText when it is empty. For blind users (and for some automatic testing tools), the hint is read when the EditText get the focus, with the method **`TextView.getHint()`** (EditText is an extension of TextView). But with the TextInputLayout, the TextInputEditText's hint is *absorbed* by its parent, the TextInputLayout. If it's still shown to usual users, blind users (and some testing tools) don't have the feedback from the **`TextInputEditText.getHint()`** anymore. The above modification fixes this behaviour by returning **`TextInputLayout.getHint()`** when calling the **`TextInputEditText.getHint()`** method. ## The Meizu crash If we combine the conclusion of last two chapters, we can understand the crash: - Meizu modification assumed that: 1. if **`TextView.getHint().isNotEmpty()`** 2. then **`TextView.getHintLayout()`** is **not null** 3. so they can call **`TextView.getHintLayout().getLineForOffset()`**. - **`TextInputEditText`** inherits **`TextView`** and **`TextInputEditText.getHint()`** returns **`TextInputLayout.getHint()`** value even if no hint has been defined in **`TextInputEditText`** - Finally, if a **`TextInputEditText`** gets focus on a Muzei device, there will be a **`NullPointerException`** and the application will crash. ## Conclusion AOSP license allowing anybody to use it, modifying it and redistribute it is the strength and the weakness of Android. It led it to have [around 75%](http://gs.statcounter.com/os-market-share/mobile/worldwide) of the worlwide mobile market share, but it is also [widely fragmented](http://gs.statcounter.com/os-version-market-share/android/mobile-tablet/worldwide) and OEMs use to modify the framework not often for the best. As the fragmentation problem as been handled by the Android Support Library, the framework modifications are way more complex to prevent and until the OEMs don't accept to not modify it, the Android team and the community will have to handle each issue at a time. So please, Device Manufacturer and OS developers, please, **don't touch the Android framework**! ![Photo credit: Arrested Development](https://i.imgur.com/R3Oqu1t.gif) *Photo credit: Arrested Development* ## notes: ### Don't workaround, fix the Library Here is some development where modifying a library with the community was prefered as finding a workaround for ourselves: oct 2012: CyanogenMod Trebuchet: Adapt Launcher Dock and Apps button to the newly phablets video: https://www.youtube.com/watch?v=S3bu3Las8ws&feature=plcp Commit : https://github.com/LineageOS/android_packages_apps_Trebuchet/commit/e74a54343ce7896b5005154e8bcbc7d5f06c7808 dec 2012: SlidingMenu: Prevent a ViewPager to be ignored when using a SlidingMenu (Jeremy Feinstein Library mainly used prior to the Google Android Navigation Drawer component) PR: https://github.com/jfeinstein10/SlidingMenu/pull/176 apr 2019: Fix Lottie animation when cancelling system animation PR: https://github.com/airbnb/lottie-android/pull/1187 may 2019: CountryCodePicker Library : Small fix upper/lower case error creating errors in tests PR: https://github.com/hbb20/CountryCodePickerProject/pull/313 may 2019: Material Components : Fix TextInputLayout crash when used on Meizu devices PR: https://github.com/material-components/material-components-android/pull/358