teaching machines

CS 491 Lecture 10 – Rattle Part III

October 6, 2011 by . Filed under cs491 mobile, fall 2011, lectures.

Agenda

Cleanup

I’ve cleaned up a few things since last time. I added a Play button to the EditActivity, pushed the song-playing routine out to a Utilities class, and made the title editable. I renamed Song.getName to Song.getTitle and Song.getRTTTL to Song.getMusic. Song.getRTTTL now composes and returns the RTTTL-encoded song. The MediaPlayer is released on finish, and I upgrade the database by wiping the old stuff and recreating it. (Not a nice thing to do to your users.)

Creating a custom adapter

An adapter is the bridge between your model (data) and its views. Write now, we’re using the ArrayAdapter with a builtin layout. Let’s scrap this and use our own, which will put a play button on each menu item. We’ll need to perform these steps:

Getting results from an Activity

The EditActivity can change our songs. Our model in MainActivity needs to updated when this happens. We can get a hooked when a spawned Activity returns if we spawn it with:

startActivityForResult(intent, REQUEST_EDIT);

REQUEST_EDIT is something we define to identify which Activity is finishing when our host Activity’s onActivityResult method is called:

@Override
protected void onActivityResult(int requestCode,
                                int resultCode,
                                Intent data) {
  if (requestCode == REQUEST_EDIT) {
    Song song = songsDB.getSong(edited.getID());
    edited.setMusic(song.getMusic());
    edited.setName(song.getName());
  }
}

With this code, we are always blindly updating the song even if it hasn’t changed. How can we take a smarter approach?

Triggering a list update

We’re now updating our model on edit. We also must update our view. (We are the controller, I guess.) That can be done with:

adapter.notifyDataSetChanged();

Catching forgot-to-save

Our edit activity conservatively saves only when the Save button is clicked. But what happens if the user backs out of the Activity not having saved changes that she meant to save? (The answer is a lot of pain.) How can we fix this problem?

Adding a contextual menu

Let’s rewrite our long-click handler to pop up a contextual menu for storing the song as a ringtone and deleting the song. First, let’s make some resources in res/values/strings.xml for our menu item labels:

<string name="storeRingtone">Store as ringtone</string>
<string name="delete">Delete</string>

And our array resources in res/values/arrays.xml:

<string-array name="songMenu">
  <item>@string/storeRingtone</item>
  <item>@string/delete</item>
</string-array>

Now we can popup the dialog:

AlertDialog.Builder dialog = new AlertDialog.Builder(this);

dialog.setTitle(song.getName());
dialog.setItems(R.array.songMenu, new DialogInterface.OnClickListener() {
  @Override
  public void onClick(DialogInterface dialog,
                      int which) {
    // TODO
  }
});

dialog.show();

We can now write the handling code.

Writing a ringtone

To register a ringtone with your device, you have to write it out to a readable file. The sdcard is a prime spot for this. Files on the sdcard are accessible by other applications. We also have to trigger the internal media library of the newly saved ringtone.

private void storeAsRingtone(Song song) {
  File dir = new File("/sdcard/media/audio/ringtones/");
  if (!dir.exists()) {
    dir.mkdirs();
  }

  File file = new File(dir, song.getName() + ".rtttl");

  try {
    FileOutputStream out = new FileOutputStream(file.getAbsolutePath());
    out.write(song.getRTTTL().getBytes());
    out.close();
  } catch (IOException e) {
    Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
    // TODO failed
    return;
  }

  sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));

  ContentValues values = new ContentValues();
  values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
  values.put(MediaStore.MediaColumns.TITLE, song.getName());
  values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/rtttl");
  values.put(MediaStore.Audio.Media.ARTIST, "Rattler");
  values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
  values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);
  values.put(MediaStore.Audio.Media.IS_ALARM, true);
  values.put(MediaStore.Audio.Media.IS_MUSIC, false);

  getContentResolver().insert(MediaStore.Audio.Media.getContentUriForPath(file.getAbsolutePath()), values);
}

When we invoke this method, our app will crash. Why? Writing to the sdcard is one of those things that an app needs permission to do. Let’s add this to the manifest:

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

There we have it. Our own little ringtone composer!

TODO