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.

Similar Posts:

    None Found

5 Comments

  1. MichaƂ:

    Thanks, neat trick with that AsyncTask Activity handle!

  2. Faison:

    I’ve been scouring the internets trying to find an explanation of how to use a custom Application object and yours seems to be the only detailed one around. You have explained this in a way that’s easy to follow and with the best possible medium, bacon. Thank you very much, I can now make progress with my project :D

    ~Faison

  3. Brent:

    Nice write up. Eric Burke has written an excellent tutorial on another way to do this that does not require a custom application object and uses the model/listener approach. http://jnb.ociweb.com/jnb/jnbJan2009.html

  4. Android:

    Very detailed discussion. This is really helpful even for newbies like me. The concept and technical areas are well explained.

  5. Rob:

    Hey,
    This is a great post, however im still struggling with the ‘has leaked internal.policy.PhoneWindow$DecorView$ added here’ error.
    I have setup all my calls exactly as you have.

    The only difference being that my AsyncTask is called from a button OnClickListener