Beware of ProGuard and android:onClick
4 min read • 695 words
The Background
As I developed ToDo, I was using a lot of onClickListeners, especially on the Task Details screen. In onCreate, I was setting up onClickListeners for all 4 priority radio buttons, as well as for the Pick Date, and Pick List buttons. While this was an acceptable implementation, I have always thought onClickListeners were an ugly API requirement.
That's why when I learned about being able to specify android:onClick for Buttons in the layout.xml files, I immediately jumped on board. I retroactively removed all of my onClickListeners, and changed them to functions specified by the android:onClick xml attribute.
GREAT! Clean code, happy coder. But, of course, it was almost too good to be true.
I do extensive testing of all of my apps before releasing them, however most of this testing is done on the emulator, with the APK as signed by eclipse for debugging. This is a mistake–one I will not make again.
The Problem
As I got closer to the release of my application, and learned that the Android Market's copy protection was going to soon be deprecated, I began researching the Android Application Licensing scheme, and implementing this in ToDo. All went well. I thought I had licensing implemented and working flawlessly.
Part of implementing licensing (and a good practice in general) is using ProGuard, as recommended here, when exporting your unsigned application or obfuscate your application's code and make it more difficult to hack.
Having tested my application extensively on the emulator, and under the impression I had the licensing working perfectly, I uploaded my app to the Android Market, and published. My friends were the first to download the app, and then immediately reported that anytime they clicked on a button in the TaskDetails screen, the app would crash. I installed it on my Android handset, and sure enough was able to re-create the issue right away. Plugging it into my computer, and looking at the LogCat's output in the DDMS prospective, I was able to deduce that the app was unable to find any of the functions that were being called by my buttons' onClick xml attribute.
WHAT THE F? I double checked the function names, and they were defined correctly, as specified in my xml files. After much experimentation, I narrowed down the possibilities as to what could be causing this issue, and determined that since it wasn't happening in the emulator (before I was exporting and signing my application), and it wasn't happening prior to implementing my application's licensing, that it had to be something occurring in between exporting my application, and signing, and zipalign-ing it. What is in between these steps that wasn't before? ProGuard. Of course, ProGuard was, unbeknownst to me, making optimizations in my code. Because my onClick functions were never being called from my code (as they were only referenced from my xml), ProGuard decided they were "dead code" and removed from my exported APK.
The Solution
Once I identified the problem, conceiving the solution was simple. Implementing it was a whole 'nother story. I knew right away I would have to add some kind of additional -keep option in the proguard.cfg file, but was very against adding a line for every function I specified to be called by a button's onClick attribute. My solution is this:
Step 1: For every function called by a button's onClick, pick a naming convention: i.e. onclick_functionName
Step 2: In the proguard.cfg file, add the following
-keepclassmembers class * {
public void onclick_*(android.view.View);
}
This tells ProGuard to keep any function that is in any (*) class, whose signature is public, returns void, name starts with "onclick_" and who takes a single parameter of type android.view.View.
Don't forget to update the android:onClick xml attribute in your layouts, or the app will crash for a different reason.
I asked this on StackOverflow but got a response I was not entirely happy with. The primary advantages to my approach are that the functions can exist in any class, not just a single class specified as in the StackOverflow response.
I hope this was helpful, and that I saved you the time I spent.