Saturday, May 8, 2010

Making Random Suck Less

One of the biggest complaints I get about WallSwitch is that the random switching sucks. It repeats images too frequently and just doesn't 'feel right'. I couldn't agree more, so when I ran across this article, I figured I could do something about it.

The problem is that random numbers don't work how the human brain expects. The article above explains it in detail, but suffice it to say that pure/pseudo random numbers suck for most things a user interacts with. What I wanted was a way to provide selections that are still nondeterministic, but closer to what users expect.

The method itself is nothing more than random selection using weighted items. I wanted to keep a feeling of randomness, but make sure more recent selections were less likely to appear. This being Java, the first thing I did was make an interface to hold the object and it's priority:

public interface BetterRandomItem {
 int getPriority();
 void setPriority(int newPriority);
}


Then, I made a choice() function (name taken from Python's random.choice) that uses this priority.

public static BetterRandomItem choice(List items) {
  if(items == null || items.size() == 0)
   throw new IllegalArgumentException("items is null or empty");
  
  // Sum all the priority values
  int itemSpace = 0;
  for(int i = 0; i < items.size(); i++)
   itemSpace += items.get(i).getPriority();  
  
  // Pull an int from that space
  int target = new Random().nextInt(itemSpace + 1);
  
  // Match the int to the corresponding item
  int tmpVal = 0;
  for(int i = 0; i < items.size(); i++){
   tmpVal += items.get(i).getPriority();
   if(tmpVal >= target)
    return items.get(i);
  }
  
  // If that failed, return the last in the list, I guess
  return items.get(items.size() - 1);
 }


Not the most complex piece of code ever written, but it'll go a long way to making users a bit happier with 'random' selections. Since putting this in, the emails and comments about my random function sucking have completely stopped.

Note: For my implementation, I kept track of the last 50 images displayed and gave them weights 1-50. Any unseen images were weighted as (num seen images * 10), giving them significantly more weight than those that had been seen.

Another Note: Watch out for int overflows in itemSpace. It would be nice if Random.nextLong took an argument.

Friday, February 26, 2010

WallSwitch 2.0 (and 2.0.1) Released!

WallSwitch 2.0 was released a couple days ago. It was quickly followed by 2.0.1 with some bug fixes for Droid users. There are more blog posts and updates to come. I'll be working on a couple more bug fixes and a new, smaller, widget for the next version.

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)