teaching machines

CS 491 Lecture 7 – Persistence of Ketchup

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

Agenda

Updating summaries

You can set a preference’s summary with:

EditTextPreference pref = (EditTextPreference) getPreferenceScreen().findPreference(getString(R.string.goal));
pref.setSummary("The first team to " + prefs.getString(getString(R.string.goal), "0") + " wins");

So, how can you update it when a preference value changes? With an OnPreferenceChangeListener! Let’s make our preference activity a listener with:

prefs.registerOnSharedPreferenceChangeListener(this);

This is also the mechanism you would use in your regular activities to respond dynamically to preference changes.

Setting default preferences

The builtin preference classes all have a defaultValue attribute that can be specified in XML. Unfortunately, these default values are not automatically set. If the user never pops open your preferences activity, the defaultValues won’t be used — unless you tickle the API with:

PreferenceManager.setDefaultValues(this, R.xml.settings, false);

This should be called in your main activity. The second argument refers to the preferences XML file from which the default values should be drawn. The third argument is called readAgain in the documentation. Setting it to false equates to “don’t set these defaults if they’ve ever been set before.” Setting it to true doesn’t restore default values.

To restore default preferences, you first must clear the preferences and then reset the defaults:

prefs.edit().clear().commit();
PreferenceManager.setDefaultValues(this, R.xml.settings, true);

Inspecting files with adb

Much of Android looks like a big black box to us right now. How are these preferences stored? Well, let’s log in to our device and find out:

adb shell
# cd /data/data/edu.uwec.cs491.ketchup
# ls -l

From the output you can see a few things. Each app is given its own Linux user by default. Only this user has read/write permissions on the shared_prefs file. I don’t know why the whole world can execute the file. There’s not really a way to open the shared_prefs file while on the device — as far as I know. But if we logout and issue

adb pull /data/data/edu.uwec.cs491.ketchup

then the binary XML preferences storage will be inflated onto our local machine. Fun.

Persisting state

With a hefty knowledge of preferences in our pocket, we can then tackle the problem of persisting data. Recall that our apps can be closed any time by the OS, either via a configuration change or if another Activity takes over. The Activity’s onPause method will get called, so that’s our place to save data. We can do so via preferences, even if there’s no corresponding UI elements to the data we’re persisting!

@Override
public void onPause() {
  super.onPause();
    
  SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit();
  editor.putLong("nSecondsLeft", timer == null ? 0 : timer.getSecondsLeft());
  editor.putString("name1", teamNames[0]);
  editor.putString("name2", teamNames[1]);
  editor.putInt("score1", teamScores[0]);
  editor.putInt("score2", teamScores[1]);
  editor.commit();
}

I’m grabbing the preferences in a different way here. Instead of getting the default shared preferences, which are available to all components of an app, I’m getting preferences specific to this activity. I open them for editing, drop in the current values, and save them.

If we recall the Activity lifecycle, we see that onResume is always called when an app is brought back to focus. We override it to check the preferences for stored state and pick up from where we left off, if need be:

@Override
public void onResume() {
  super.onResume();
  if (getPreferences(MODE_PRIVATE).getString("team1", null) != null) {
    restoreGame();
  } else {
    setUpTeams();
  }
}

Adding categories

We want to ship our app with a modestly-sized initial set of word banks. But we also want these to be editable. Resources are not the vehicle to achieve this, since they are compiled down and can’t be changed dynamically. We have a few other options: retrieve them from a web service, database, or files. Let’s go files.

The first thing we want to do is install the files and prime them. We only want to do this once! There’s no callback for one-time initialization, but we can throw our initialization in onCreate and condition it on it not having been run before. Our condition flag can be stored in preferences. Slick.

SharedPreferences prefs = getPreferences(MODE_PRIVATE);
if (!prefs.getBoolean("initialized", false)) {
  Toast.makeText(this, "DO ONE TIME!", Toast.LENGTH_LONG).show();
  initializeCategoryFiles();
  prefs.edit().putBoolean("initialized", true).commit();
}

Creating a file and writing to it is fairly straightforward. The context provides a mechanism for opening a FileOutputStream in the right place with the right permissions, so we exploit it:

private void initializeCategoryFile(String category, String[] lines) {
  FileOutputStream ostream = openFileOutput("cat_" + category + ".txt", MODE_PRIVATE);
  PrintWriter out = new PrintWriter(ostream);
  for (String line : lines) {
    out.println(line);
  }
  out.close();
}

The file will be placed at /data/data/edu.uwec.cs491/files/cat_<category>.txt. We can use adb and the cat utility to ensure the file has the right contents. (This only works on the emulator.)

With these files in place, we can iterate through the directory, picking out all the categories, and prompt the player to pick one before moving on. We’ll have to pass the chosen category to the game activity. Intent.putExtra let’s us ship the String off. Incorporate the category into the WordDatabase and voila.

Haiku

Ten years’ photos, gone
first steps, birthdays, my wedding…
don’t rotate your phone!