teaching machines

CS 491 Lecture 3 – Publish and polish

September 13, 2011 by . Filed under cs491 mobile, fall 2011, lectures.

Agenda

Playing media

There are a couple of ways to play media using stock Android software. The class MediaPlayer is the simplest, and it can play resources given their ID. We have a resource in res/raw/pop1.mp3, so our MediaPlayer construction and invocation looks like:

MediaPlayer player = MediaPlayer.create(this, R.raw.pop1);
player.start();

Be mindful of a few things here. First, you need to release old MediaPlayer instances, otherwise you’ll run out of memory. The release() method frees up the resources, but you can’t just call it after start(), as it’s non-blocking. Instead, you add a listener which gets called when the player’s done playing:

player.setOnCompletionListener(new OnCompletionListener() {
  public void onCompletion(MediaPlayer player) {
    player.release();
  }
});

The second gotcha is that the create() method makes a blocking call to MediaPlayer.prepare(). If the preparation takes any amount of time, the user will experience an annoying hiccup in her interaction, trash your app, and demand a refund. And you can’t just tell the user to live with it. The Android system itself will report an Application Not Responding — or ANR — dialog giving the user the option to force close. Let’s trigger an ANR to see how it works. We’ll make a new Activity that creates a button whose click-handling is never finished:

public class ANRTest extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Button button = new Button(this);
    button.setText("We only click once!");
    button.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View view) {
        while (true);
      }
    });

    setContentView(button);
  }
}

Pressing the button once lands us in the infinite loop. That itself is fine. But if we press it again, the touch event won’t be handled. If it remains unhandled for over 5 seconds, we’ll get an ANR dialog.

Back to MediaPlayer now. The Android documentation says that MediaPlayer.prepare() is fine for file-based resources. MediaPlayers playing streaming data over the Internet, however, should go about playing in a different way.

Persisting data

As we rotate a device running PopMusic, we’ll find that the state of all the bubbles is lost. This happens because by default an Activity is torn down and recreated when the viewing mode changes. This default behavior can be overridden in XML, in something called the manifest. This file enumerates all Activitys in our app and informs the user what it intends to do (access contacts, send text messages, etc.). More on it later. We find our Activity node and add this attribute:

configChanges="orientation"

With this we are stating that our code will determine how to handle orientation changes. The default behavior is to let Activity’s onConfigurationChanged method do the work. Let’s write another little tester:

public class OrientationChangeTest extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    TextView label = new TextView(this);
    label.setText("I'm a big teapot.");
    setContentView(label);
  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
      Toast.makeText(this, "landscape", Toast.LENGTH_LONG).show();
    } else {
      Toast.makeText(this, "portrait", Toast.LENGTH_LONG).show();
    }
  }
}

This gives us an illusion that things are working, but handling configurations ourselves is frowned upon. What’s not so great about this solution is that there are a lot of configuration changes that can happen, and more to come probably. Keyboards slide out and slide in, language and locales change on the fly, and font sizes change. All these settings are handled elegantly by the Android system if they are known at startup (it’ll pull from a landscape layout in landscape mode, a portrait layout in portrait mode, an English resource file in an English locale). We’re almost always better off resigning ourselves to fate, letting our app close down, and restoring the state when it comes back to life. The real fix is to persist our data.

We can store state locally several ways: through preferences, in an SQLiteDatabase, or a file of our own devising. Hmm… All of these choices are a little complicated and are topics for another day. Let’s take the easy way out this round and just force our app to be in either portrait or landscape mode all the time. Head back to the manifest and this attribute to your activity tag:

android:screenOrientation="portrait"

While we’re at it, let’s make it fullscreen:

android:theme="@android:style/Theme.NoTitleBar.Fullscreen"

This is a cheap solution that only obviates our need for persisting data on screen reorientations. The bubble state will still get wiped on other configuration changes and changes to Activity’s lifecycle.

Autoporting

A major feature of Android market is that you can sell your apps to people all over the world and probably in the ISS too. However, English is spoken by about only a quarter of the people on Earth. (Don’t ask for a source.) When your app gets downloaded by a non-English speaker, what’s going to happen?

Android comes to the rescue here. By pushing your text and other resources out to specially-named resource directories, the system will sample the ones that match the current configuration. These directories are named things like values-en (for English text), values-es (for Spanish text), drawable-ja (for Japanese images), and drawable-xdpi (for images on real high-density screens). When you reference your resources through their identifiers (R..), you will get the resource for the current configuration automatically, without resorting to a mess of conditional statements.

(Of course, messes still occur in the resource directory. You might need a drawable-port-notouch-12key directory, which holds images for portrait-oriented devices lacking a touch screen and only having a number pad input. Blech.)

Let’s add a little welcome Toast to our PopMusic app with something like:

Toast.makeText(this, R.string.instructions, Toast.LENGTH_LONG).show();

Now, let’s define the instructions string in two places. First, in values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="greeting">Pop some bubbles!</string>
</resources>

The unadorned directories like values and drawable are considered the defaults. The default directories should have definitions for every resource. Otherwise a resource might end up undefined and your app will fail.

Now, let’s place this in values-es/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="greeting">¡Estalle algunas pompas!</string>
</resources>

Now if we had chosen to handle locale changes ourselves, we’d have to manually reconfigure everything that depended on resources. Otherwise, we’d still be referencing resources from the previous configuration.

Another common configuration we can support are different screen densities or sizes. My Asus Transformer will pull drawables from drawable-xlarge, if available, so let’s make some bigger bubble images and drop them in there.

Publishing

All right. Let’s publish! We’re only a few steps from releasing our immature PopMusic from the world:

  1. Go to the Manifest editor and run the Export Wizard.
  2. Sign the app with a key. The key is the developer’s “signature.” Two apps signed with the same signature can share resources. This is particularly useful for building commercial-free app pairs. Instead of maintaining two separate code bases, the commercial app can just be a simple license installer that unlocks features of the already downloaded free app. The shared signature allows the commercial app to install the license in the free app’s domain. (It may write an entry to a database or preference, for example.)
  3. Go to http://market.android.com/publish and type-type-type.

TODO

Haiku

Cartesian products
bane of portability
crank and ABS?