What are you keeping from me?

Sebastiano Gottardo
ProAndroidDev

--

On why default Proguard rules are important

A few days ago, while I was working on introducing AndroidX to the Blinkist app, I stumbled upon an unexpected crash. After a solid amount of hours spent on trying to understand whether it was a regression or a jolly good bug, I realized that it had to do with Proguard, and that the solution was apparently not as well known as one might think.

TL;DR: using default Proguard rules can save you time and pain, especially (but not limited to) when migrating to AndroidX.

At I/O ’18, Google announced AndroidX. Put it simply, it’s a way to unite the plethora of different Support Libraries under the same umbrella, while providing independent semantic versioning and sane package naming at the same time.

At a superficial glance, this sounds very intuitive and easy, but in practice there are a couple of issues that need to be taken care of.

First off, your project likely imports a ton of components from the Support Libraries. Deciding to use AndroidX requires you to change each and every import statement to point to the new androidx package name. For small apps, a manual approach to this task would be annoying to say the least; for big apps, let alone enterprise ones, this is simply not practical.

Secondly, while you have control over your source code, the same cannot be said for third-party libraries, in particular those which in turn make use of the Support Libraries as a provided dependency. By blindly migrating to AndroidX, those libraries would lose access to the classes they need (because their imports still point to the old android.support package names) and stop working altogether. Your only option would be to wait until every single one of your dependencies is updated to support AndroidX, which is yet again not practical.

Thankfully, we have two powerful tools to deal with these two problems.

The first one is a an action offered by the Android Plugin (available in Android Studio) that allows you to automatically migrate all your code to the new package names of AndroidX. It will scan your code, look for those package names and migrated them according to this mapping. A word of caution is due here: although the plugin does most of the heavy lifting, you might still end up with some things not properly migrated or with unnecessary usages of fully qualified names instead of imports. Given the “simple” nature of the operation, Daniel Lew wrote a simple script to run this migration via command line.

The second tool is called Jetifier, and its purpose is to rename import statements for third-party libraries (it does this by altering the bytecode directly). This way, you can migrate right away without having to wait for all the dependencies you have to do the migration first.

With these allies at my side, I proceeded to the migration.

The problem

I started the migration using the provided action in Android Studio, and together with the team, we proceeded to review what was done and what was left to be done. A couple of simple project-wide searches (here’s a handy shortcut: cmd + shift + F) for android.support highlighted roughly a couple dozen references that the automatic tool forgot to amend, together with some overly zealous fully-qualified declarations. Having amended those, and given Jetifier was automatically enabled by the migration tool, we then proceeded to test the app.

Running a debug build didn’t highlight anything off, so we started to become hopeful, even bold: migrating to AndroidX has been almost seamless and painless!

But, alas, we then created a release build and tried to start the app: instant crash. There wasn’t even time for the launch Activity animation to kick off, so it had to be something serious.

The investigation

A quick look at the logs pointed at a cascade of ClassNotFoundException instances. Apparently, some UI widgets, now living in the new AndroidX package, appeared to be missing. A simple equation pointed to the culprit:

Apparently, the AndroidX migration tool didn't update the Proguard rules, for a reason that became clearer only later on. And indeed, updating those rules with AndroidX’s package name solved the issue, and the app was running again, with no obvious errors to be spotted.

Proguard rules to keep some widgets and UI-related classes from being stripped away.

Except…

Looking at the logs again upon app startup time, we noticed a couple more exceptions that were not fatal. Let’s take a closer look at those missing classes; first we have

com.google.firebase.analytics.connector.internal.AnalyticsConnectorRegistrar

and then

com.google.firebase.iid.Registrar

Wait a minute. Firebase? What does Firebase have to do with AndroidX? And why on Earth do we have a regression for something we didn’t even touch?

We use Firebase almost exclusively for FCM (push notifications), and performing a regression test confirmed our suspicions that push notifications were indeed broken.

Once more, adding rules that prevented Proguard from stripping those classes solved the issue, and push notifications started to work again:

Proguard rules for keeping Firebase-related classes from being stripped away.

Happy ending? Not quite.

You see, we kept wondering on why migrating to AndroidX introduced those problems (at least, I was very keen to understanding the full problem). And we were even more puzzled by the fact that nobody seemed to be having the same issues as we had, in having to manually keep AndroidX classes as well as Firebase’s.

So we resorted to the Android community, that once more proved to be a thriving place full of folks willing to help each other.

Thanks to Zac Sweers and Jake Wharton, we discovered that a couple of bugs were reported and fixed on this regard (aosp/891685 and aosp/903818), and were released under version 1.0.2 of AndroidX’s annotation library. But we were not using this library, so what gives?

And then, reading the release notes for that version, we got hit by the following statement:

Note: This would have only had an impact on your builds if you were not using getDefaultProguardFile as those default rules also included correct rules for both packages.

🤦‍♂

Default Proguard files

The default Proguard files contain, you guessed it, a set of rules that automatically instruct Proguard on how best to process a defined set of classes, for example AndroidX-related classes. These files are packaged together with the Android Plugin and, upon build time, are copied in project-dir/build/intermediates/proguard-files directory.

If you start a new Android project from scratch, you already have a statement in your build.gradle file that includes something like this:

proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'

However, for older codebases, this might not be the case! As far as we know, there is no warning nor linting that points to not including the default files. And, as we have just demonstrated, the lack of it can have terrible results.

To summarize, what we believe happened is:

  1. The AndroidX migration tool assumed our project included the default rules, so it didn’t bother updating any Proguard file.
  2. We didn’t have the default rules, so a bunch of AndroidX classes were being stripped.
  3. The default rules also prevent classes and methods that are annotated with the @Keep annotation from being stripped, which is the case for those Firebase classes.

… that took long enough!

The solution

Not exactly a one line fix, but close enough. 🤦‍♂

If you want to use the default rules and have your custom set of rules at the same time, the above solution is the correct one. If, on the other hand, you don’t have the need for custom rules, omitting the proguardFiles statement altogether is another perfectly valid solution, because the default rules will be included anyway (h/t to Sebastiano Poggi for discovering that!).

Conclusion

I believe that, were I to tell you “use the default Proguard files!”, you would’ve rightfully replied “DUH”. But as you can see, especially for existing codebases that have been around for a while, the answer is not that obvious.

I think that some sort of linting around this would be more than beneficial, especially in light of the migration to AndroidX that every existing Android app will have to go through at some point. I opened a ticket, so if you agree on the importance of this issue, please star it!

https://issuetracker.google.com/issues/126772206

(Remember, it’s enough to star an issue, no need to leave a comment unless you’re providing more information/context to the issue)

Or, as a starting point, mentioning this check as a point in the “Migrating to AndroidX” documentation page! Here’s another ticket on the Issue Tracker:

https://issuetracker.google.com/issues/126882651

--

--