Monday, July 6, 2009

WallSwitch v1.5.0 Released!

I'm incredibly happy to finally announce that WallSwitch 1.5 has been published to the Android Market! It's been a significant amount of work to add some of the new features and fix the outstanding bugs. Here's what you can expect to find in the new version:
  • You can now select multiple wallpaper folders
  • New picker for auto-switch interval
  • Low Performance Mode, in case your phone is sluggish
  • Android 1.5 officially, and exclusively, supported
  • Won't auto-switch right at boot. It waits a minute so the boot can finish.
There were also a few bugs fixed in the image processing. Notably, cropping should be faster and more accurate. As always, if you find any bugs, please don't hesitate to email me.

Sunday, May 3, 2009

WallSwitch v1.0.4 Released!

Released tonight with a couple more bug fixes.

I'm going to start work on v1.1 soon, which will add some of my more frequently requested features.

Wednesday, April 29, 2009

WallSwitch v1.0.3 Released!

Just a quick note on a release that happened a couple days ago. Some bugs were found very quickly and I had to do a quick release so it would work on more phones.

Other than that, working on a couple more features. Right now, I'm thinking checkboxes instead of a radio button for choosing the folder for your wallpapers will be the first thing I do. It seems to be the more requested feature at the moment and is a really good idea.

Sunday, April 26, 2009

WallSwitch 1.0 Released!

I'm happy to announce that WallSwitch 1.0 has been released!

WallSwitch is a program designed to randomly switch the wallpaper on your phone at a given interval. It handles all the image processing, like cropping, automatically so that you never have to manually set your wallpaper again!

It is also designed to intelligently determine if an image should be cropped or letterboxed so that you always have the optimal wallpaper size for you.

You can download it to your G1 (or any future Android devices) through the Android Marketplace. Search for 'WallSwitch'.

Saturday, April 25, 2009

Registering for TIME_TICK After a Reboot in Android

Creating a service that will receive TIME_TICK, but also start on boot isn't immediately obvious. It took me a bit to figure out the best way to do it, so I figure it's worth sharing.

To start, you can't register your receiver to get TIME_TICK in the android manifest. The registration for TIME_TICK has to happen in code using Intent.registerReceiver. You can, however, define that your receiver has android.permission.RECEIVE_BOOT_COMPLETED and set your receiver to handle it, which will give you some control on boot. Here's the relevant portion of AndroidManifest.xml:

<application android:icon="@drawable/icon" android:label="@string/app_name"
 android:enabled="true" android:debuggable="false">
 <activity android:name=".AndroidMain" android:label="@string/app_name">
  <intent-filter>
   <action android:name="android.intent.action.MAIN" />
   <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
 </activity>
 <service android:name="DemoService"></service>
 <receiver android:name="DemoReceiver"
  android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
  <intent-filter>
   <action android:name="android.intent.action.TIME_TICK"></action>
   <action android:name="android.intent.action.BOOT_COMPLETED"></action>
  </intent-filter>
 </receiver>
</application>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"></uses-permission>

This isn't enough, however, since your receiver can't register itself to listen to TIME_TICK. Intent.registerReceiver registers for the lifetime of the Intent it's called from. If we register it using the intent created for BOOT_COMPLETED, it's dead as soon as it exits the handler. Therefore, we need to start a background service that will keep running. This service then creates a new instance of the receiver and registers that for TIME_TICK.

DemoReceiver.java:
public class DemoReceiver extends BroadcastReceiver {
 static final String LOGGING_TAG = "MyDemo";

 @Override
 public void onReceive(Context context, Intent intent) {
  if (intent.getAction().compareTo(Intent.ACTION_BOOT_COMPLETED) == 0){   
   Log.v(LOGGING_TAG, "DemoReceiver.onReceive(ACTION_BOOT_COMPLETED)");   
   context.startService(new Intent(context, DemoService.class));   
  }else if(intent.getAction().compareTo(Intent.ACTION_TIME_TICK) == 0)
   Log.v(LOGGING_TAG, "DemoReceiver.onReceive(ACTION_TIME_TICK)");
  else
   Log.v(LOGGING_TAG, "DemoReceiver.onReceive(" + intent.getAction() + ")");
 }
}


DemoService.java:
public class DemoService extends Service {
 static final String LOGGING_TAG = "MyDemo";
 
 @Override
 public IBinder onBind(Intent intent) {
  return null;
 }
 
 @Override
 public void onStart(Intent intent, int startId){
  super.onStart(intent, startId);
  Log.v(LOGGING_TAG, "DemoService.onStart()");
 }
 
 @Override
 public void onCreate(){
  super.onCreate();
  Log.v(LOGGING_TAG, "DemoService.onCreate()");
  
  registerReceiver(
    new DemoReceiver(), 
    new IntentFilter(Intent.ACTION_TIME_TICK));  
 }
}


If this all works correctly, we'll see log messages that look something like:
04-25 21:04:22.580: VERBOSE/MyDemo(182): DemoReceiver.onReceive(ACTION_BOOT_COMPLETED)
04-25 21:04:22.630: VERBOSE/MyDemo(182): DemoService.onCreate()
04-25 21:04:22.650: VERBOSE/MyDemo(182): DemoService.onStart()
04-25 21:05:00.140: VERBOSE/MyDemo(182): DemoReceiver.onReceive(ACTION_TIME_TICK)

Tuesday, April 14, 2009

Image Processing in Android

While working on WallSwitch, I ran into a number of hairy problems with image processing. The worst, by far, was this:
04-15 02:20:34.693: ERROR/dalvikvm-heap(165): 50331648-byte external allocation too large for this process.
04-15 02:20:34.693: ERROR/(165): VM won't let us allocate 50331648 bytes
04-15 02:20:38.233: ERROR/AndroidRuntime(165): java.lang.OutOfMemoryError: bitmap size exceeds VM budget 
I got it when trying to do a simple BitmapFactory.decodeFile() on a jpeg I had on the sd card. The jpeg was huge (~4MB) but that should exceed the 16MB heap I get, right? I found some decent help here and here, but nothing that really explained what was going on. I knew I was hitting the limit, but had no idea why.

Then, I remembered something a friend had told me a few years ago about how jpegs are compressed (duh) but that it had to first decompress to a bitmap before painting to your screen. It makes sense, after all. If I've got 1024x768 pixels on my monitor and I want to paint a picture over all of it, I'm going to need 768,432 bytes (depending on bit-depth).

I whipped out my calculator and had a facepalm moment. Needless to say 50,331,648 is 4096x6144 * 2, which corresponds to the size of my jpeg, when fully decompressed.

Fixing this was, happily, very easy. You just need to set inSampleSize on your BitmapFactory.Options to something useful. It'll sample the image you get back, returning something 1/2, 1/4, ... the size. The key here is to try to keep the sample size a power of two. It isn't necessary, but it makes the processing faster and it'll make sure the image you get back keeps the same proportions.

// First, get the dimensions of the image
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);

// Only scale if we need to 
// (16384 buffer for img processing)
Boolean scaleByHeight = Math.abs(options.outHeight - targetHeight) >= Math.abs(options.outWidth - targetWidth);
if(options.outHeight * options.outWidth * 2 >= 16384){
    // Load, scaling to smallest power of 2 that'll get it <= desired dimensions
    double sampleSize = scaleByHeight
        ? options.outHeight / targetHeight
        : options.outWidth / targetWidth;
    options.inSampleSize = 
        (int)Math.pow(2d, Math.floor(
        Math.log(sampleSize)/Math.log(2d)));
}

// Do the actual decoding
options.inJustDecodeBounds = false;
options.inTempStorage = new byte[IMG_BUFFER_LEN];  
Bitmap output = BitmapFactory.decodeFile(filePath, options);
It's still a pretty slow operation to perform, but this at least makes it possible!