The Google Maps API is broken on Android 5. Here's a workaround for multiple map fragments

As I reported on the GMaps-API tracker here, there's a critical issue affecting apps that use more than one map fragment. Lollipop devices seem to be the only ones affected. Other devs are now commenting, verifying my analysis - my goal is for this to gain more visibility internally at Google.

I first noticed this when demoing my app, One Warm Coat, on my new Nexus 6 and saw it was repeatedly crashing on load. We use multiple map fragments in a tabbed interface; after examining the stack traces I had a hunch that running more than one map was the cause. Curiously, my Nexus 4 running 4.4.2 had no issues, and neither did emulators running pre-lollipop builds, so it seemed like a regression introduced in Android 5.0.

A summary of the crash

Without warning, upon loading your activity or fragment containing multiple map fragments (or a little later, upon trying to interact with the map fragments), you'll see the app force close and barf this all over your logcat:

E/AndroidRuntime﹕ FATAL EXCEPTION: GLThread 9994  
    java.lang.NullPointerException: Attempt to get length of null array
        at java.nio.DirectByteBuffer.put(DirectByteBuffer.java:385)
        at java.nio.ByteBufferAsIntBuffer.put(ByteBufferAsIntBuffer.java:160)
        at com.google.maps.api.android.lib6.gmm6.o.c.a.k.e(Unknown Source)
*snip*

Or rarer, this:

E/AndroidRuntime﹕ FATAL EXCEPTION: GLThread 9816  
    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.contains(java.lang.CharSequence)' on a null object reference
        at com.google.maps.api.android.lib6.gmm6.o.c.h.<init>(Unknown Source)
*snip*

Needless to say, this was a showstopper for the app, so I set out on the long road of hunting down the exact situations that caused the crash, in the hopes of implementing a workaround.

12 hours later...

After spelunking quite a few false tunnels (was it a Google Play Services mismatch? A bug with multiple Location Clients?), I emerged bleary eyed at the end of the rabbit hole with a consistently reproducible scenario, which I included in my analysis posted to Google's bug tracker:

It seems that since Android 5.0, we cannot have more than one map fragment running at once. In other words, only one map fragment can be in the resumed state at any given time. Any others must be paused, or this exception occurs.

A deeply vexing, contrived new requirement, but a requirement nonetheless if I wanted the app to work on Android 5. 

The workaround

 To maintain this lamentable invariant, I implemented three key changes:

  1. An OnPageChangeListener in my ViewPagers to help determine when each page is in focus, so that only the page in focus runs its map fragment. When the page changes, the current page is Paused (via a call to OnPause) and the new page is resumed (via, you guessed it, OnResume). Each page correctly propagates the OnPause/OnResume calls to its underlying map fragments, ensuring that we have only one map fragment in the Resumed state at a time.
  2. Implement OnHiddenChanged on the parent content fragments that host the ViewPagers. When a parent fragment is hidden (by the user clicking to a different parent fragment via the navigation drawer), it gets Paused, which Pauses its hosted ViewPager, which Pauses its current page, which Pauses its map fragment. The newly shown parent fragment does the same thing but with Resume. Ensuring the proper propagation of OnPause and OnResume calls throughout my nested fragments was crucial in maintaining this "single map invariant".
  3. Added a static MapFragmentCounter class to serve as a sort of semaphore to record how many active (resumed) map fragments were currently running. I incremented and decrement the counter every time a map fragment was resumed or paused respectively, which acted as a sanity check to help iron out crashes during system events such as activity pauses/resumes and configuration changes. (For the latter, I took a step further and forced the app to portrait mode to simplify things.)

These exorcised the crashes.

What should Google do?

  • First and foremost, obviously, they should prioritize and push out a fix for this regression in the next version of Google Play Services. You can help by starring the issue to push it up their queue - happily, it's garnered an internal issue number by now. 

  • In my opinion, they should also seriously consider open sourcing or at the very least de-obfuscating the Maps library. Thanks to ProGuard, any visibility into the possible causes of bugs inside the library itself are impossible for anyone outside of Google to debug and thus extremely frustrating and opaque to dance around. I would've saved countless hours if only their black box were just a bit more transparent and the stack traces produced by these crashes were more than unhelpful gibberish.

What can you do?

If you're suffering from these crashes and seeing these stack traces in your logs, try the workarounds I detailed above. Feel free to message me or comment for further explanation, or check out the One Warm Coat repository for the actual code. This is the commit containing the workarounds.

Finally, once again, star and comment on the issue above to keep it active and hasten the arrival of a fix.