Monday, November 26, 2012

Opaque Daydreams and Live Wallpapers

As Jeremy mentioned, Daydream support for devices running Android 4.2+ is starting to roll out to select Live Wallpapers. We're starting out slowly, but the intent is to eventually have pretty much all of our wallpapers (including the free versions!) work as a Daydream once any initial issues are sorted out.

Speaking of implementation issues, there was a subtle one that we caught internally which I haven't seen discussed elsewhere: if your DreamService uses a full-screen SurfaceView (just about anything OpenGL-based), Android will see its window as transparent. Because of that, the application underneath the Daydream is never paused and the two will compete for resources. That's especially problematic if both are using reasonably complex OpenGL scenes (a Daydream over a view of the desktop/lock screen with a view of a Live Wallpaper, for example).

One way to work around the problem is to launch an Activity from the Daydream service. New Activities, even using a SurfaceView, will generally pause what's underneath them unless using a translucent style. While it did pause the wallpaper below, the transition between the Daydream and the Activity was noticeable and the whole thing felt a bit fragile to use. So that approach was quickly discarded.

After browsing the Android SDK source for a while and comparing application window properties in Hierarchy Viewer, the difference between Daydreams that did properly pause apps (no SurfaceView) and ones that didn't (contained a vanilla SurfaceView) came down to a single private flag that signals a window to report how much (if any) of it is transparent. The flag is set in an unavoidable call to requestTransparentRegion() that is made by a SurfaceView as soon as it's added to the view hierarchy.

After some trial and error, our shipping solution is to create a custom RelativeLayout container for the SurfaceView that overrides requestTransparentRegion(). It's an empty method in our version, which effectively blocks the request to gather the transparent region from making its way up the view hierarchy. The window is now totally opaque to Android, but that opens up a new problem: SurfaceViews are added underneath the current window by default, in case you want to draw standard UI controls above them. In other words, the SurfaceView is totally hidden by the blank DreamService window! To get around that, call SurfaceView.setZOrderOnTop( true ).

The end result is a DreamService window that displays your SurfaceView scene and pauses whatever is below it. Optional support for transparency can be easily added, if ever needed.

A minor downside is that I'm pretty sure the above-mentioned solid blank rectangle is still being drawn every frame below the SurfaceView. I haven't confirmed this yet with dumpsys, but because the contents of the rectangle never changes, it should never invalidate and so hopefully end up in a hardware overlay (fast) rather than drawing into a frame buffer (slower). Either way, it's certainly faster than drawing another complicated OpenGL scene below it, and I haven't noticed an actual real-world hit to performance.

I did try an approach that overrode the container's gatherTransparentRegion() method to always return true (opaque) and a 0x0 transparent region. Despite seeming like the Right Way to do this with public APIs, it outright failed to pause the application below the DreamService. It seems that even if the transparent region is reported to not exist, just the presence of the request flag disqualifies DreamService's window from being opaque to the view hierarchy. In the end, what we came up with gives a nearly-identical end result and has so far been extremely reliable in practice.

No comments:

Post a Comment