Friday, May 28, 2010

Phone Number Fixer for Android

One of the curses of working with computers is that you often become The IT person for friends and family. In fact, I should get a commission from Apple for all of the business I've sent their way by getting family members to switch to Mac computers. Ditto for the Firefox browser. I can't make people switch to Mac, after all that's often an expensive proposition, but it's long been a condition of my help that you never even think about using IE. For some of my less computer savvy relatives, I've learned to make IE disappear. This just made my life a lot easier (less things to fix) especially back in the heady days of weekly IE6/ActiveX exploits. Anyways... These days most folks, including friends and family, mostly use web applications and since I've had them on Firefox for a long time now, there aren't too many problems. However, recently I got an unusual request for help from a high school friend Maria.

She had just switched to a Nexus One -- a phone that she seemed to be extremely happy with. She had imported several contacts from her SIM card that had been in her old phone. The only problem was that for these contacts, their phone number was classified as "Other." That would not be a big deal, but when she would try to send a text to a number, Android's auto-complete would not include these "Other" phone numbers as part of its search domain. So she would have to completely type out the number to send the text to -- which kind of defeats the purpose of having an address book on your phone. She could manually change each of these phone numbers to be of type "Mobile", and this would solve the problem for that number. However as you might guess, she had a lot of numbers and changing each manually would be painful to say the least. And that's where I come in...

This sounded like an easy enough problem to solve. Find all of the numbers that were of type "Other" and change them to "Mobile." That might not work for everyone -- there might be some people who really want to classify some phone numbers as "Other" -- but it certainly worked for Maria (and I would guess most people, too.) So I cooked up a quick little app. First, I wanted to display all of the numbers that this was going to affect:
ContentResolver resolver = getContentResolver();
String[] fields = new String[]{People._ID, People.NAME};
Cursor cursor = resolver.query(People.CONTENT_URI, fields, null, null, People.NAME);
LinkedHashMap<Integer, String> people = new LinkedHashMap<Integer,String>();
int id = cursor.getColumnIndex(People._ID);
int nameCol = cursor.getColumnIndex(People.NAME);
if (cursor.moveToFirst()){
 do{
  people.put(cursor.getInt(id), cursor.getString(nameCol));
 }while (cursor.moveToNext());
}
cursor.close();
This gives you a LinkedHashMap whose keys are the IDs of each contact, and whose values are the names of the contacts. Why did I bother with these two bits of info? Well, we need the contacts to query the phones, and I wanted something friendly to display to Maria so she knew which numbers were about to get "fixed". Anyways, now it was easy to query the phones:
ArrayList<String> data = new ArrayList&kt;String>();
StringBuilder sb = new StringBuilder();
String[] projection = new String[]{Phones.DISPLAY_NAME, Phones.NUMBER, Phones._ID};
Uri personUri = null;
Uri phonesUri = null;
int displayName = 0;
int number = 1;
int phoneIdCol = 2;
int phoneId = -1;
int cnt = 0;
for (int personId : people.keySet()){
 personUri = ContentUris.withAppendedId(People.CONTENT_URI, personId);
 phonesUri = Uri.withAppendedPath(personUri, 
            People.Phones.CONTENT_DIRECTORY);
 cursor = resolver.query(phonesUri, projection, 
            Phones.TYPE + "=" + Phones.TYPE_OTHER, null, null);
 displayName = cursor.getColumnIndex(Phones.DISPLAY_NAME);
 number = cursor.getColumnIndex(Phones.NUMBER);
 phoneIdCol = cursor.getColumnIndex(Phones._ID);
 if (cursor.moveToFirst()){
  do {
   sb.setLength(0);
   sb.append(people.get(personId));
   sb.append(": ");
   sb.append(cursor.getString(displayName));
   sb.append('|');
   sb.append(cursor.getString(number));
   data.add(sb.toString());
   phoneId = cursor.getInt(phoneIdCol);
   cnt += updateContact(phoneId);
  } while (cursor.moveToNext());
 }
 cursor.close();
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.phone, data);
setListAdapter(adapter);
Toast.makeText(this, cnt + " phone numbers updated", Toast.LENGTH_LONG).show();
This code just loops over the contacts we got from the first query. For each of those contacts it queries to see if the contact has any phones that are of type "Other" (Phones.TYPE_OTHER). If it does, it creates a string that shows the contact's name, the phone's display name, and the phone number. This string is added to an ArrayList. Once all of the queries complete, an ArrayAdapter is created using the ArrayList of contact name/phone strings and used to populate a ListView.
You might have also noticed that a there is a counter variable being incremented, and an updateContact method. This is actually the method that fixes the phone number. I probably should have just kept track of the phoneIds and then gave the user a button to initiate the fixes, but I was lazy. Here is the code for updateContact.
private int updateContact(int phoneId){
 ContentValues values = new ContentValues();
 values.put(Phones.TYPE, Phones.TYPE_MOBILE);
 ContentResolver resolver = getContentResolver();
 Uri phoneUri = ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId);
 return resolver.update(phoneUri, values, null, null);
}
Too easy. Contacts are an example of a Content Provider in Android. Many people (including my Android in Practice co-author and author of Unlocking Android, Charlie Collins) have criticized Android for exposing too much of the database backing of Content Providers. As you can see from the examples above, you have to deal with cursors (moving, closing), queries, updates, and very thinly abstracted select and where-clauses. Maybe it would have been better to drop down directly to the SQL, or provide a more object oriented API. Now you can create your own Content Provider, and you don't have to use SQL database to back it -- but then implementing the Content Provider contract can be quite awkward.
Anyways, I found that the easiest way to get this little app to Maria was to simply put it on the Market. It's still on there if you happen to have a similar problem (maybe many people switching to Android might experience this?) If you are on your Android phone you can just select this link. If you are on your computer you can scan this QR code.

No comments: