CS 491 Lecture 7 – Persistence of Ketchup
Agenda
- reflecting preferences in summaries
- actually setting default preference values
- inspecting files with adb
- restoring a paused game
- incorporating word categories
- passing information to spawned Activitys
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
first steps, birthdays, my wedding…
don’t rotate your phone!