Archive for the ‘Mobile’ Category.

Bacon Rank Android App Details

Bacon Rank is currently my most ambitious Android project from UI and background processing point of view. It has custom UI controls and it deals gracefully with long running network operations.

Let’s start with the custom SeekBar first, which is used to select the number of strips of bacon you ate. When you have a touch screen device, especially with no physical keyboard, you want to avoid situations where the user is forced to type anything. In my case SeekBar works great, since you can’t eat that many strips of bacon at one sitting. To draw the SeekBar with custom images I created bacon_seekbar.xml in the drawable folder:

<layer-list
  xmlns:android="http://schemas.android.com/apk/res/android"
>
  <item
    android:id="@+android:id/background"
    android:drawable="@drawable/progress_mediumbacon" />
  <item
    android:id="@+android:id/SecondaryProgress"
    android:drawable="@drawable/progress_rawbacon" />
  <item
    android:id="@+android:id/progress"
    android:drawable="@drawable/progress_cookedbacon" />
</layer-list>

The progress_* drawables are PNG images.

In the main layout I use the custom SeekBar and set a custom thumb (a PNG image of a skillet):

<SeekBar
    android:id="@+id/SEEKBAR"
    android:layout_below="@id/BACON_STRIPS"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:max="20"
    android:progress="0"
    android:secondaryProgress="0"
    android:paddingLeft="32px"
    android:paddingRight="32px"
    android:progressDrawable="@drawable/bacon_seekbar"
    android:thumb="@drawable/skillet"
    />

SeekBar has three states (if you count background), but I am really just using two. The weird part is that I thought I would use background and progress states, but I could not get that to work. Instead, I am using SecondaryProgress as background, and background is not visible anywhere. In onCreate I set up the SeekBar:

mSeekBar = (SeekBar)findViewById(R.id.SEEKBAR);
mSeekBar.setOnSeekBarChangeListener(this);
mSeekBar.setMax(PROGRESS_MAX);

In onProgressChanged method I update the amount in the UI and that is about it for the SeekBar. I think it looks pretty nice, but it would be better if I could make it look more 3D:

The trickiest part of the application was handling of the network activity while allowing for screen orientation changes. None of the tutorials I’ve read or the stackoverflow questions and answers regarding this seemed workable (or maybe I did not understand them). The problem is this: suppose in your activity you start an AsyncTask for the network activity. This is what is actually recommended pretty much everywhere. But suppose your user then changes the screen orientation. By default Android destroys and recreates the current activity. Your AsyncTask is still going, but it does not know about the new activity, and your new activity has no knowledge of the AsyncTask. So what is the fix?

It turns out you can create a custom application object, which will be available as long as your process is running. So in your activity you can start the AsyncTask as before, but keep a reference to the AsyncTask in the application object. The AsyncTask needs a reference to the current activity to draw on the screen, which your activity keeps up to date. Basically set it on AsyncTask creation, null it during screen orientation and activity destroy, and restore when the activity is recreated. We also want to run an indeterminate progress indicator in the UI while network activity is going. In code this looks something like this (I’m omitting many irrelevant details):

public class BaconApp extends Application {
    public BaconRank.SyncTask mSyncTask = null;
 
    // ...
}
 
public class BaconRank extends Activity implements SeekBar.OnSeekBarChangeListener {
    // ...
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        // ...
    }
 
    BaconRank.SyncTask getSyncTask() {
        return ((BaconApp)getApplication()).mSyncTask;
    }
 
    void sync(String action) {
        ((BaconApp)getApplication()).mSyncTask = (SyncTask) new SyncTask(this).execute(getUserAgent(), mUserId,
                mBaconStrips.getText().toString(),
                action);
    }
 
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        SyncTask syncTask = getSyncTask(); 
        if (syncTask != null) {
            syncTask.setActivity(null);
        }
    }
 
    @Override
    public void onRestoreInstanceState(Bundle inState) {
        super.onRestoreInstanceState(inState);
        SyncTask syncTask = getSyncTask(); 
        if (syncTask != null) {
            setProgressBarIndeterminateVisibility(true);
            syncTask.setActivity(this);
        }
    }
 
    @Override
    public void onStop() {
        super.onStop();
        SyncTask syncTask = getSyncTask(); 
        if (syncTask != null) {
            syncTask.setActivity(null);
        }
    }
 
    public class SyncTask extends AsyncTask<String, Void, JSONObject> {
        // ...
 
        public SyncTask(BaconRank activity) {
            super();
            mActivity = activity;
        }
 
        public void setActivity(BaconRank activity) {
            mActivity = activity;
        }
 
        protected void onPreExecute() {
            if (mActivity != null) {
                mActivity.setProgressBarIndeterminateVisibility(true);
            }
        }
 
        protected void onCancelled() {
            // mActivity cannot be null
            mActivity.setProgressBarIndeterminateVisibility(false);
            clearActivity();
        }
 
        void clearActivity() {
            // call in ui thread only
            ((BaconApp)mActivity.getApplication()).mSyncTask = null;            
        }
 
        protected void onPostExecute(JSONObject result) {
            if (mActivity != null) {
                // Update UI
            }
            clearActivity();
        }
 
        protected JSONObject doInBackground(String... params) {
            // Do the network stuff
        }
 
        // ...
 
    }

You will also need to specify the custom application object in the AndroidManifest.xml:

  <application
    android:name=".BaconApp"
  >

And that wraps the Bacon Rank presentation. If you know how to do anything I presented above in a better way, please leave a comment. The application is also localizeable, so if you want to translate the app into your language let me know.

If you did not read it already, you might want to read about the application running baconrank.com, the server the Android application talks to.

Bacon Rank Released

I am pleased to release Bacon Rank, the application you have been waiting for to count your bacon! (You may not have realized this, of course. I forgive you.)

I’ve been working on this application combo since last November, a few hours here and there. The server piece is a Turbogears 2 application, while the client piece is an Android application. As a whole, they form the Bacon Rank application ecosystem. The idea is that whenever you eat bacon, you submit the number of strips of bacon you ate, using the Android client, to the server. The server returns your statistics, including your rank compared to other users of the system. It is clearly a not very serious application, but it presented some interesting programming challenges.

I’ve been going through various Python web frameworks, and this time I wanted to learn about Turbogears 2. There isn’t anything especially groundbreaking about it, but I think it marks the first time I am actually running a real service. Of course it is also tied to the Android application, which makes it interesting.

The Android application is a bit more ambitious from engineering point of view. In the UI the trickiest piece was a customized SeekBar: uncooked bacon that becomes cooked as you move a skillet over it. I was also able to finally figure out how to make a background network request survive screen orientation change gracefully.

I am also experimenting with Twitter for the first time as a communications channel for the project.

In the next two posts I will explore the server and client pieces in more detail.

Nexus One Radio Follow-up

As you have probably gathered by now, my earlier announcement was an April Fools’ prank. Unfortunately I had forgotten to adjust my blog settings for Daylight Saving Time, so the URL and time on the post refer to the previous day, even thought it was posted a few minutes into April 1st.

I got the idea for the prank out of the blue around 11 pm, and had the application ready a few minutes before midnight, so I waited for that to pass to create the .apk and post it to Android Market. Most of the development time went into finding a suitable image of a radio and scaling and cropping it (as you can guess, Gimp gives me trouble). I used the Creative Commons search engine to find the image.

I was pretty satisfied with the results. According to Android Market statistics, the application was installed 10741245 times (although the real number is somewhat higher since the statistics don’t update in real time), has 7787 ratings with average around 3.5. Not bad for less than 24 hours of exposure. Some of my favorite comments were from people who were fully on board with the prank:

  • Pretty good. Can’t seem to tune below 88.9
    anakin78z
  • OMG this best radio player ever. Pandora and Slacker don’t even come close.
    freddie
  • nice hack :) Will pre-sets be available in the future? :-p
    dbrowne
  • would be a 5 if the dialer was more accurate. great job dev! :)
    semeon

There were also others who seemed to have fully fallen for it without liking it ;) :

  • Wasted about 2 minutes of my life. . Don’t bother, feeble prank !
    Ian
  • I hate you.
    Tom

Of course there were also a number of spoilsports who just gave one star and mentioned it was a prank app. Shame on you.

I got one support email asking if it would work on HTC Touch. Also the comment in the original blog post about collaborating on a droid project makes me wonder if I fooled that person or if they were in on it.

Seems like I also fooled some people on reddit.com.

Since this was a joke app just for today, I’ve unpublished the app from Android Market. So if you are one of the 300+ people who still got the app installed, you are suddenly the owner of a rarity :)

Nexus One Radio

If you have been wondering why I haven’t posted in a while, it is because I have been hard at work on my most ambitious Android project to date. When Google came out with the Nexus One phone I was quick to grab one. But one thing about the hardware has always bugged me. The Nexus One contains an FM radio, but there wasn’t any way to use it from software. Until now! I have painstakingly tracked down the hardware details and reverse engineered the API needed to get the radio to appear. The UI is pretty primitive. Go grab yourself an early build of Nexus One Radio (see ratings on Cyrket). Search for it in the Android Market!

Update: Please see the follow-up!

GPS Still on After LocationManager.removeUpdates?

My most recent Caltroid update added map view with two location listeners: one coarse (=network) and one fine (=GPS) location listener. Or so I thought. Even though I was calling LocationManager.removeUpdates() for both location listeners, I could still see the GPS icon active at the top, and logcat showed GPS activity still going on. Users were reporting drained batteries. Not good.

It took me a few false starts until it occurred to me that I was also using MyLocationOverlay to automatically put the user’s location on the map. Well, this obviously requires location updates. I had somehow falsely assumed that as soon as the map view went away, MyLocationOverlay would stop listening to location updates. That is not the case. You must explicitly call MyLocationOverlay.enableMyLocation() to start updates, and MyLocationOverlay.disableMyLocation() to stop updates.

After figuring this out the fix was easy. In my map view’s onResume() I request location updates for my two location listeners and enable MyLocationOverlay, and in onPause() I do the opposite. No more drained batteries!

Caltroid with Map

My latest update to Caltroid added two notable new features: the next available train is highlighted and scrolled into view automatically, and the simple locate function was replaced with a custom Google Map view. The map has Caltrain stations as an overlay, and the map starts out centered and zoomed so that all stations should be visible. While the map is active, it will update the users current position (using GPS and/or network location), and showing the nearest Caltrain station (as the crow flies). It is possible to select this station as the starting station from the map. Additionally, it is possible to tap the directions button on the map to get driving directions to the nearest station.

Working on the map feature was a pleasure now that I actually have an Android device.

If you look at the map view carefully, you will notice the text is written in transparent grey boxes with rounded corners. A stackoverflow question showed me how to do this with custom shape resource. I also based the map overlay largely on this blog post.

I had agonized for the longest time about how to get highlighting for the the list view. Then I stumbled into a couple of stackoverflow questions that showed me the way. In the end the best example I found was the List14 sample in Android API demos. I just need to call convertView.setBackgroundDrawable() with the right drawable, and make sure to add the following attribute for the list view: android:cacheColorHint="#00000000"

A Month of Nexus One

My first Android phone is the Nexus One. I tried to wait for an AT&T one, but the battery in my 8525 wasn’t so good anymore so I decided I could wait no longer. So far I am pretty happy with the phone.

I ordered the unsubsidized phone, which leaves me an option to go with T-Mobile or staying with AT&T. Time will tell which one I choose. T-Mobile coverage is my main concern. If I knew roaming would work well in the US without obnoxious costs, I might switch. On the other hand, I have been reasonably happy even with AT&T’s EDGE speeds. It is fast enough for checking email, do light browsing, and navigation works as well. Most of the time when I need the higher speeds I am in some location where I can use wifi.

Setting up the phone turned out to be a breeze. I just went through all the settings on first boot and everything worked. I was initially somewhat concerned about being able to put in the settings for AT&T network, but I didn’t really need to do anything fancy, just looking in the wireless options and selecting the only carrier it found (AT&T).

Since this is my first actual Android device, I put my Android apps through some tests on actual hardware. Everything worked! I’ve also been able to work on some new features (namely location related) that I didn’t much attempt with just the emulator. Some updates will roll out shortly.

Getting adp working with Nexus One turned out to be a little more work. First I updated the SDKs and the Eclipse ADT plugin, but even after this the phone was just showing with question marks in the launch dialog and would not let me copy the apk and run it. I found a thread on xda-developers which gave me the answer. I am on Ubuntu 8.04 and this is what I did:

  1. Create file /etc/udev/51-android.rules with contents SUBSYSTEM=="usb", SYSFS{idVendor}=="0bb4", MODE="0666"
  2. Restart adp: adb kill-server && adb start-server
  3. Turn on USB Debugging on the phone in Application > Development settings
  4. Connect the phone with USB cable to the computer

I get great battery life on the Nexus One. And what I mean by that is that I can go for two full days before I need to recharge with my normal usage. I keep wifi, bluetooth and GPS off unless I am actually using them. I talk less than five minutes on the phone per day, browse a handful of websites for a few minutes, and read email several times a day. Surprisingly I also find myself playing some games, probably half and hour to an hour a day. You could say on most days I am not using the phone for anything that’d require a smartphone.

During my first week the phone had some stability issues, crashing several times when I had left it in the charger overnight. But once I uninstalled some applications that kept services running (SIPDroid, Fring and some other IM clients), I haven’t had a single crash. Best uptime when I remembered to check was over 300 hours.

I’ve encountered some annoying bugs as well. During my first week the phone got into a state where tapping the messaging icon always launched the browser. The only way I could recover was to reboot. This hasn’t happened since. Far more common problem is the soft keyboard not registering taps, or thinking I pressed one of the four buttons at the bottom (back, menu, home or search, with accompanied vibration feedback). Switching back and forth between apps fixes this eventually. I also seem to have situations when I don’t seem to get any GPS information. Some people have reported that this might require a reboot, but I haven’t been trying persistently enough to confirm if this is the only cure.

Voice recognition generally works great. There have been a few bizarre mistakes, like once I did a voice search saying “swype for android” and voice recognition thought I said “life on crack”. I find I miss a physical key I could press and do arbitrary voice commands, though.

One of my major issues with the phone, besides it not working on AT&T’s 3G network, is the inability to call out with Bluetooth without handling the phone. With my 8525 running Windows Mobile 5 and 6 I was able to tap my Bluetooth headset and say a name in my addressbook, and it would make the call. With Android nothing happens when I tap the headset, and I need to find my contact by handling the phone. This seems bizarre to me, since as far as I know California law requires hands free calling, and Google is based in California. Answering calls does work as I expect.

I haven’t been thrilled with voice quality on calls, which has been a surprise. I seem to get a fair bit of static. Also the earplugs and microphone that shipped with the phone have been practically useless; the other party can hardly hear anything when I try to use them.

I haven’t bought any apps yet, although I was tempted to get Locale. However, Locale author(s?) seem to have pissed off their potential customers by silently removing the free beta when they introduced their relatively expensive paid version while at the same time apparently making the paid version worse than the free version was. I’d also be interested in being able to buy Swype, since I don’t seem to be a very good typist with the builtin soft keyboard. Unfortunately Swype is not (yet?) available on the Android Market.

I don’t want to sync my data with Google, so I am looking for some way to sync with my PC. So far I haven’t had much luck. There are apparently paid solutions for Windows, and maybe Mac, but haven’t found anything for Linux. The most promising effort to me seems to be the Funambol Android Sync Client. AFAIK you could run your own Funambol server, which would solve my sync needs nicely. Except that at this point only contact sync is supported. If my phone loses data now, I am going to be sad…

I installed the Google update which added multitouch to Google apps, and I really like it in the browser.

I haven’t found the need to root my phone yet, but I will probably do that down the line to get some of the features that are not available otherwise.

Pulling Android Market Sales Data Programmatically

Android Market handles sales through Google Checkout. I haven’t tried selling anything else online before, but what this setup provides for me as the seller leaves a lot to be desired. One issue you will have trouble with is getting the data needed to file taxes.

Google provides a Google Checkout Notification History API that lets you programmatically query sales data. For my purposes the API requests are really simple: just post a small XML document with the date range I am interested in, get back XML documents that contain my data. If there is more data that fits in a single response, look for an element that specifies the token for the next page and keep pulling until you get all data.

Below is a really simple Python script that uses M2Crypto to handle the SSL parts for the connection (needed since Python doesn’t do secure SSL out of the box). You will also need to grab certificates. You should save the script as gnotif.py, save the certificates as cacert.pem and create gnotif.ini as described in the script below all in the same directory. When you execute it, it will ask for start and end date (in YYYY-MM-DD format) and then fetch all the data, saving them in response-N.xml files, where N is a number.

#!/usr/bin/env python
# Script to query Google Checkout Notification History
# http://code.google.com/apis/checkout/developer/Google_Checkout_XML_API_Notification_History_API.html
 
# Supporting file gnotif.ini:
#[gnotif]
# merchant_id = YOUR_MERCHANT_ID_HERE
# merchant_key = YOUR_MERCHANT_KEY_HERE
 
import base64
import re
from ConfigParser import ConfigParser
 
from M2Crypto import SSL, httpslib
 
ENVIRONMENT = "https://checkout.google.com/api/checkout/v2/reports/Merchant/"
XML = """\
<notification-history-request xmlns="http://checkout.google.com/schema/2">
%(query)s
</notification-history-request>
"""
 
config = ConfigParser()
config.read('gnotif.ini')
MERCHANT_ID = config.get('gnotif', 'merchant_id')
MERCHANT_KEY = config.get('gnotif', 'merchant_key')
 
rawstr = r"""<next-page-token>(.*)</next-page-token>"""
compile_obj = re.compile(rawstr, re.MULTILINE)
 
auth = base64.encodestring('%s:%s' % (MERCHANT_ID, MERCHANT_KEY))[:-1]
 
ctx = SSL.Context('sslv3')
# If you comment out the next 2 lines, the connection won't be secure
ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, depth=9)
if ctx.load_verify_locations('cacert.pem') != 1: raise Exception('No CA certs')
 
start = raw_input('Start date: ')
end = raw_input('End date: ')
 
data = XML % {'query': """<start-time>%(start)s</start-time>
<end-time>%(end)s</end-time>""" % {'start': start, 'end': end}}
 
i = 0
 
while True:
    c = httpslib.HTTPSConnection(host='checkout.google.com', port=443, ssl_context=ctx)
    c.request('POST', ENVIRONMENT + MERCHANT_ID, data,
             {'content-type': 'application/xml; charset=UTF-8',
              'accept': 'application/xml; charset=UTF-8',
              'authorization': 'Basic ' + auth})
 
    r = c.getresponse()
 
    f=open('response-%d.xml' % i, 'w')
    result = r.read()
    f.write(result)
    f.close()
 
    print i, r.status
 
    c.close()
 
    match_obj = compile_obj.search(result)
    if match_obj:
        i += 1
        data = XML % {'query': """<next-page-token>%s</next-page-token>""" % match_obj.group(1)}
    else:
        break

As you take a look at the data you will probably notice that you are only getting the sale price information, but no information about the fees that Google is deducting. Officially it is a flat 30%, but I have found out a number of my sales have the fee as 5%. So we need to get this information somehow. Luckily you can toggle a checkbox in your Google Checkout Merchant Settings. Unfortunately there is a bug, and the transaction fee shows as $0 for Android Market sales. I have reported this to Google, and they acknowledged it, but there is no ETA on when this will be fixed.

I also haven’t found any way to programmatically query when and how much did Google Checkout actually pay me. (I can get this info from my bank, but it would be nice to query for that with the Checkout API as well.)

Last but certainly not least, working with the monster XML files returned from Google Checkout API is a real pain. If someone has a script to turn those into a format that could be imported into a spreadsheet or database that would be nice…