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.
Wednesday, April 29, 2009
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'.
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
To start, you can't register your receiver to get
This isn't enough, however, since your receiver can't register itself to listen to
DemoReceiver.java:
DemoService.java:
If this all works correctly, we'll see log messages that look something like:
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:
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
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 budgetI 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!
Subscribe to:
Posts (Atom)