While working on an app that sets the wallpaper in Android, I’ve encountered this weird issue with the WallpaperManager API. Initially I thought this will be a very easy and straighforward task because the WallpaperManager API looks simple. You just have to get an instance and call setStream() or setBitmap() depending on the type of your source.

A simple example looks like:


WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
wallpaperManager.setStream(new FileInputStream(path));


where path refers to the location of either a PNG or JPG file.

This works fine but not always. I don’t get any errors or whatsoever in the logs but the current wallpaper doesn’t get updated. After several tries, sometimes it’ll work but sometimes you’ll end up getting OutOfMemoryError.


W/WallpaperManager(11217): Can't generate default bitmap
W/WallpaperManager(11217): java.lang.OutOfMemoryError
W/WallpaperManager(11217):  at android.graphics.Bitmap.nativeCreate(Native Method)
W/WallpaperManager(11217):  at android.graphics.Bitmap.createBitmap(Bitmap.java:697)
W/WallpaperManager(11217):  at android.graphics.Bitmap.createBitmap(Bitmap.java:674)
W/WallpaperManager(11217):  at android.graphics.Bitmap.createBitmap(Bitmap.java:641)
W/WallpaperManager(11217):  at android.app.WallpaperManager.generateBitmap(WallpaperManager.java:809)
W/WallpaperManager(11217):  at android.app.WallpaperManager$Globals.getCurrentWallpaperLocked(WallpaperManager.java:281) W/WallpaperManager(11217): at android.app.WallpaperManager$Globals.peekWallpaperBitmap(WallpaperManager.java:246)
W/WallpaperManager(11217):  at android.app.WallpaperManager.getBitmap(WallpaperManager.java:436)


Here’s what seems to be WallpaperManagerService is doing when you call setStream or any relevant method.

• If it is a stream, BitmapFactory.decodeStream() decodes it and gets written to wallpaper directory using a fixed filename wallpaper.

private static File getWallpaperDir(int userId) {
return Environment.getUserSystemDirectory(userId);
}

• If it is a bitmap, it gets compressed to an outputstream which points to the wallpaper directory using the same fixed filename.

• WallpaperManagaerService is monitoring this wallpaper directory using a FileObserver.


private class WallpaperObserver extends FileObserver {

final WallpaperData mWallpaper;
final File mWallpaperDir;
final File mWallpaperFile;
....


• If there’s a change, the onEvent method in the WallpaperObserver will be called. This checks if the new file is using the hardcoded filename wallpaper and then call bindWallpaperComponentLocked().

• bindWallpaperComponentLocked() will call the system component com.android.systemui.ImageWallpaper to display the wallpaper_dir/wallpaper image.

So the problem seems to be happening during step 1 when trying to decode the stream and write to a file. One of the possible culprit is when BitmapFactory.decodeStream failed to decode the image. This will not throw any runtime error but rather it will return a null bitmap.

I really really hate methods returning null as it forces you to check for null returns so APIs should either throw an exception or return a SPECIAL CASE object. (Read more about this on Clean Code by Robert Martin)

If you’ll look closely at the WallpaperManager code at line 809, if the BitmapFactory.decodeStream fails for some reason, WallPaperManager will try to load the default wallpaper from com.android.internal.R.drawable.default_wallpaper which creates a new bitmap. This bitmap doesn’t seem to get recycled or gc’d which will eventually cause the system to throw OOM.


Bitmap newbm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
newbm.setDensity(metrics.noncompatDensityDpi);

Canvas c = new Canvas(newbm);
...


There’s actually an issue raised regarding the BitmapFactory.decodeStream and it seems to be fixed already 2 years ago but I’m still getting the same problem.

So my last option is to use setBitmap(). I cannot use setResource because my image is not bundled in the app itself. With setBitmap(), you need to handle the decoding yourself.

What I did to overcome the issue above is to keep on trying to decode the image if it failed.


private Bitmap forceDecodeBitmap(String path) throws FileNotFoundException{
Bitmap bmp = null;

while(bmp == null){
bmp = BitmapFactory.decodeStream(new FileInputStream(path));
}

return bmp;
}


It’s not a good solution but it works for me. I know that this will not terminate if your image is really messed up so I guess there should be some limit on the number of tries.

Once you have your bitmap, then you can now call


wallpaperManager.setBitmap(bmp);