teaching machines

CS 491 Lecture 6 – For all Intents and Preferences

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

Agenda

Refactoring Ketchup

I cleaned up our mess of Ketchup a bit. The biggest issue was properly sequencing the user interaction so that the timer doesn’t start until team names have been entered. Team names are displayed on the screen with TextViews. After the point is awarded, we check if the goal was reached and the game is over. If not, we prepare for a new round. If so, we offer the chance to play another game.

Implicit Intents

Android has a service-oriented architecture. When you need a task accomplished, you generally don’t care so much about who performs it or how it’s done. You just want the result. Android provides this infrastructure through Intent objects. Activities can advertise that they satisfy certain Intents in their application’s manifest. Client activities can request certain Intents be satisfied.

Let’s just look at this from the client activity’s perspective initially. Common tasks that apps need performed include dialing a number, viewing a webpage, showing a map, and searching the web for some terms. Let’s make a little tester activity that demonstrates how we request our intents be satisfied:

public class ImplicitIntentTest extends ListActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    String[] items = {
      "Dial",
      "View",
      "Map",
      "Search",
    };
    setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items));
  }

  @Override
  protected void onListItemClick(ListView listView, View itemView, int position, long id) {
    Intent intent;
    if (position == 0) {
      intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:7158675309"));
    } else if (position == 1) {
      intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.uwec.edu"));
    } else if (position == 2) {
      intent = new Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=105 Garfield Ave, Eau Claire, WI"));
    } else if (position == 3) {
      intent = new Intent(Intent.ACTION_WEB_SEARCH);
      intent.putExtra(SearchManager.QUERY, "foo kittens");
    } else {
      intent = null;
    }
    startActivity(intent);
  }
}

You can see that we communicate the particulars of our intents in several ways. In some cases, we can pass a uniform resource identifier (URI). In others, we can pass “extras.” Our activity will be paused while the new activity loads up and does its job. We can hit the back button to return to our activity wherever it left off.

Which new activity is run depends on the user’s preferences and how activities have declared themselves in their manifest. If multiple browsers are installed and the user has not picked a default, then ACTION_VIEW will trigger a dialog to choose between browser activities.

There’s also an ACTION_CALL. If we try it out, our app will crash because it’s overreaching its permissions. For an app to make a call, it must declare so in the manifest with a <uses-permission> element.

<uses-permission android:name="android.permission.CALL_PHONE" />

Explicit Intents

Sometimes we know exactly which activity we want to run, usually because we’re just switching between screens in our own app. In this case, we want to use explicit intents. Let’s add a main game screen to Ketchup that will eventually have a few buttons on it. For now, a Play button is sufficient.

It’s this new screen that we want to show when the app first starts up, not the game play screen. To declare an activity as the starting activity, we add an intent-filter element to it in the manifest:

<intent-filter>
  <action android:name="android.intent.action.MAIN" />
  <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

The MAIN element states that the activity is a top-level one depending on no prior interaction and the LAUNCHER element causes it to be listed in the applications launcher. For many apps, only one activity is branded with these two tags.

Now, in our main activity, when we want to spin off the play activity, we can make new intent, explicit this time, like so:

Intent intent = new Intent(this, KetchupActivity.class);
startActivity(intent);

Just like Hypercard.

Adding a menu

These instructions are simply a distilled version of the Android Dev Guide.

  1. Create a file res/menu/menu.xml in your Android project. Fill it with your menu options:
    <?xml version="1.0" encoding="utf-8"?>
    <menu
      xmlns:android="http://schemas.android.com/apk/res/android">
      <item
        android:id="@+id/settings"
        android:title="Settings" />
      <item
        android:id="@+id/editScheduled"
        android:title="Edit scheduled" />
    </menu>

    Probably you should make icons (omitted here) and reference title strings in strings.xml (literal here).

  2. Add an onCreateOptionsMenu method to the Activity which should have the menu:
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
      MenuInflater inflater = getMenuInflater();
      inflater.inflate(R.menu.menu, menu);
      return true;
    }
  3. Add a menu item handler to the same Activity:
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
      switch (item.getItemId()) {
        case R.id.settings:
          // load settings
          return true;
        case R.id.editScheduled:
          // load editor
          return true;
        default:
          return super.onOptionsItemSelected(item);
      }
    }

Preferences

Okay, let’s make a preferences screen. Since each screen has its own activity, we’ll need to make a new one. Thankfully, Android provides a PreferencesActivity which does most of the heavy-lifting. We end up with something very simple:

public class KetchupPreferencesActivity extends PreferenceActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.settings);
  }
}

The preferences themselves are best defined in XML. There are several kinds of preference widgets builtin, including checkbox preferences, edit text preferences, and ringtone preferences. We’ll stick to the first two for today. Our first jab at R.xml.settings ends up looking like:

<PreferenceScreen
  xmlns:android="http://schemas.android.com/apk/res/android">
  <CheckBoxPreference
    android:key="@string/showTimer"
    android:title="Show timer"
    android:summary="Display a countdown to end of round"
    android:defaultValue="false" />
</PreferenceScreen>

All preferences have keys that we use to look them up, which here I’ve defined externally in R.values.strings.xml. In the our game activity, we can fetch the preference values with this code:

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
isTimeShown = prefs.getBoolean(getString(R.string.showTimer), false);

TODO

Haiku

I wrote bad software
Android came and fixed all that