January 2, 2013

Android Programming for Beginners: Part 2

In the first part of this two-part series on getting started with Android coding, you set up your development environment, built a basic countdown app, and got acquainted with the Android API. In this second article we'll have a closer look at the structure of an Android app, create a menu, and write a second activity to input a countdown time. We'll also look at running your app on a physical phone.

Menu options

There are three Android menu types. The Options Menu is the menu that appears when you hit the menu button (on older Android) or is shown in the Action bar (newer Android), and you can also access contextual menus and popup menus. We're going to use the Options Menu to allow you to set the count down time.

Eclipse creates an stub Options Menu method with a new main Activity:

public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.menu.activity_main, menu);
  return true;
}

getMenuInflater() allows you to follow best practice and create your menu in XML rather than in code. Edit res/menu/activity_main.xml:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/set_time" android:title="@string/set_time"></item>
</menu>

and add the @string reference to res/values/strings.xml. You can fiddle around more with menu ordering and other options in the Layout tab, if you want.

onMenuItemSelected()is fired when the user chooses a menu item:

public boolean onMenuItemSelected(int id, MenuItem item) {
  switch(item.getItemId()) {
    case R.id.set_time:
      setTime();
      return true;
    default:
      // we don't have any other menu items
  }
  return super.onMenuItemSelected(id, item);
}

All we do is get the menu item ID (set in the XML above) and act accordingly. We're now ready to write the setTime()method, which will call another Activity.

 android 2 menuMenu item is showing at the bottom of the screen.

Activities

First, a little bit of background. Android is structured as a whole bunch of modules, the idea being that parts of one app can easily hook into parts of other apps, maximising code reuse. There are four main application components:

  1. Activites: provide a screen and UI for a particular action. An app has at least one main Activity, and may have lots of other associated Activities.
  2. Services: run in the background doing something (checking email, playing music, etc), without a UI.
  3. Broadcast Receiver: receive announcements broadcast by the system and do something accordingly.
  4. Content Provider: makes data available to other apps.

Intents are messages which are used to jump into a module, or to pass information between modules. We're going to set up a second Activity to enter the time to count down for, using a scroller widget, called using the menu and setTime():

private void setTime() {
  Intent i = new Intent(getBaseContext(), CountdownSetTime.class);
  startActivityForResult(i, SET_TIME_REQUEST_ID);    
}

This Intent simply starts the new CountdownSetTime Activity, and tells the current Activity to expect a result. In CountdownSetTime.java, the work is done in the onCreate()method:

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.set_time);
  context = this.getApplicationContext();
  Spinner spinner = (Spinner) findViewById(R.id.spinner);
  ArrayList<Integer> spinnerList = new ArrayList<Integer>();
  for (int i = MIN; i <= MAX; i++) {
    spinnerList.add(i);
  }
  ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(context,
          android.R.layout.simple_spinner_item, spinnerList);
  adapter.setDropDownViewResource(
          android.R.layout.simple_spinner_dropdown_item);
  spinner.setAdapter(adapter);
  spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
    public void onItemSelected(AdapterView<?> parent,
      View view, int pos, long id) {
      secondsSet = (Integer)parent.getItemAtPosition(pos);
    }
    public void onNothingSelected(AdapterView<?> parent) {
      // Do nothing.
    }
};

The XML layout in res/layout/set_time.xmllooks like this:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  >
  <Spinner
    android:id="@+id/spinner"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="50px"
    android:text="@string/spinner_text"
  />
  <Button
    android:id="@+id/ok_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/spinner"
    android:layout_marginLeft="10dip"
        android:text="@string/ok_text" />
  <Button
    android:id="@+id/cancel_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/spinner"
    android:layout_toRightOf="@id/ok_button"
    android:layout_marginLeft="10dip"
        android:text="@string/cancel_text" />
</RelativeLayout>

It defines a Spinner and two buttons, within a RelativeLayout. A Spinner displays data; an array holds the data; and an ArrayAdapter translates between the two. Our Spinner just displays numbers (seconds to count down), so the data is held by an Integer ArrayList, holding the integers between our MIN and MAX values. android.R.layout.simple_spinner_item and android.R.layout.simple_spinner_dropdown_itemare stock Android layout resources that set up the look of the spinner item and the dropdown. You could also choose to create your own resources.

onItemSelectedListener() sets up a Listener to act when an item is picked, setting the secondsSet class variable. To pass this value back to the original Activity, we set up OK and Cancel buttons. You've already seen the code for a button and its OnClickListener in the previous article, so here I'll just show the onClick()method for the OK button:

public void onClick(View view) {
  Intent i = new Intent();
  Bundle bundle = new Bundle();
  bundle.putInt(CountdownActivity.SET_TIME_KEY, secondsSet);
  i.putExtras(bundle);
  setResult(RESULT_OK, i);
  finish();
}

A Bundle is used to store information in an Intent, so it can be passed between Activities. Each value (here we just have a single Integer) is stored with a String key. For the cancel button, no Bundle is needed. Just create an Intent, set the result as RESULT_CANCELLED, and call finish().

android 2 spinnerChoosing the number of seconds on a hardware phone.

You'll also need to register the new Activity by adding this line to AndroidManifest.xml:

<activity android:name=".CountdownEnterTime"></activity> 

Finally, then, we need something to handle the Intent back in CountdownActivity; this is what the onActivityResult()method is for:

private int countdownSeconds = 10;  // default value of 10 secs
[ .... ]
protected void onActivityResult(int requestCode, int resultCode, Intent i) {
  super.onActivityResult(requestCode, resultCode, i);
  if (resultCode == RESULT_CANCELED) {
    return;
  }
  assert resultCode == RESULT_OK;
  switch(requestCode) {
    case SET_TIME_REQUEST_ID:
      Bundle extras = i.getExtras();
      countdownSeconds = extras.getInt(SET_TIME_KEY);
      countdownDisplay.setText(Long.toString(countdownSeconds));
      break;
    default:
      // do nothing; we don't expect any other results
    }
  }
}

Check for RESULT_CANCELLED first, as this will be the same for any returning Activity, and you will always ignore it and return. The assertstatement makes it clear that beyond this point, the result is assumed to be OK. If any other value is returned, the method will throw an error. The number of seconds is stored in a class variable, and displayed to the user.

Finally, to make the timer do the right thing, we need to change one line in the start button's onClick()method:

showTimer(countdownSeconds * MILLIS_PER_SECOND)

If you run the app on an emulator now, you should be able to pick a time and watch it count down.

Installing on a phone

You can hook your phone up via USB and run on that rather than on the software emulator. For some uses (e.g. the accelerometer and the GPS) it is better to test it on a phone, rather than in the emulator. Just plug in the USB cable, and turn on USB Debugging in Settings / Developer Options. When you hit Run in Eclipse, you'll get the option to run it on your phone. If you want to be able to run your app in non-debugging mode, check out the Android info on publishing.

This app could obviously still use some improvement. Perhaps a start/stop button (check out the CountDownTimer API); a button rather than a menu item to set the time; a ringtone to go off when the alarm finishes; a different form of spinner; some graphical design improvements... Play around and see where you can take the code from here!