Tuesday, November 30, 2010

Elegant Preference Summaries

Why can't you just tell your Preference object to show it's value in it's summary? Well, because you can't. I'm sorry I don't have a better answer to that question, however, I will share my solution for auto populating your Preference summaries with the set values for each Preference.

Overview
There are 2 main issues to solve here and they are both similar. First we want all of our Preference objects to have their summaries set to whatever their value is when you first launch your PreferenceActivity. Of course if you only want to use your Preference summary for a description of the Preference, then you can do so statically in your Preference layout XML file using the attribute android:summary. Second, you want to dynamically update the summary of any Preference as soon as you change it's value.

Preamble
Lets start with a sample preference layout.



android:title="Preference Sub Screen 1">
android:title="Dogs"
android:entries="@array/dogs"
android:entryValues="@array/dogs"/>
android:title="Cats"
android:entries="@array/cats"
android:entryValues="@array/cats"/>

android:key="pref_edit1"
android:title="Username"/>
android:key="pref_edit2"
android:title="Password"
android:password="true"/>

Notice in the above XML code I didn't set the summary attribute for any of the Preferences. Also notice that I have a nested PreferenceScreen. The technique I'm about to show you uses recursion to drill down through your defined PreferenceScreens and auto fill all your summary attributes dynamically and setting them to the current value for each Preference.

Step 1 (Auto fill all your summaries)
In your PreferenceActivity, you will need a method to recursivly travel though your preferences and set all their summaries. Preferences in Android are layed out inside PreferenceScreen. So the main Preference screen is a PreferenceScreen object. This can contain various Preference objects such as ListPreference, EditTextPreference, CheckBoxPreference, etc). It can also contain PreferenceScreen objects. So it makes sense to start our method by passing in the root PreferenceScreen. We can get the number of Preferences in a PreferenceScreen by calling PreferenceScreen.getPreferenceCount(), then loop over all the preferences and setting their summaries to their values. In this example I'm only doing ListPreferences and EditTextPreferences, but you could extend it to cover other preference objects as well.

    private void setSummaryAll(PreferenceScreen pScreen) {
// Setup the initial values
for (int i = 0; i < pScreen.getPreferenceCount(); i++) {
Preference pref = pScreen.getPreference(i);
if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
pref.setSummary(listPref.getEntry());
} else if (pref instanceof EditTextPreference) {
if (((EditTextPreference) pref).getEditText().getInputType() == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)) {
EditTextPreference etPref = (EditTextPreference) pref;
String maskedPw = "";
if (etPref.getText() != null) {
for (int j = 0; j < etPref.getText().length(); j++) {
maskedPw = maskedPw + "*";
}
pref.setSummary(maskedPw);
}
} else {
EditTextPreference etPref = (EditTextPreference) pref;
pref.setSummary(etPref.getText());
}
} else if (pref instanceof PreferenceScreen) {
setSummaryAll((PreferenceScreen) pref);
}
}
}

The recursion comes in when we come across a PreferenceScreen object. We just call setSummaryAll() and pass in the PreferenceScreen. This is a nice clean approach to making sure we don't miss setting a summary on a Preference that is buried down in a sub PreferenceScreen somewhere.

Step 2
Now that we have set dynamically the summary attributes on PreferenceActivity launch, we also need to handle the case where the user changes the value of a Preference. We want the summary to be updated immediately. To do this, we need to implement the OnSharedPreferenceChangeListener interface.
public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {

You will need to implement the onSharedPreferenceChanged method as well. This is where you will handle setting the summary for any preference that has changed. The code is similar to the code in step 1, except this time you are passed in the key to the Preference object that has been changed and you only need to set the summary for that one Preference.

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Preference pref = findPreference(key);

if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
pref.setSummary(listPref.getEntry());
} else if (pref instanceof EditTextPreference) {
if (((EditTextPreference) pref).getEditText().getInputType() == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)) {
EditTextPreference etPref = (EditTextPreference) pref;
String maskedPw = "";
if (etPref.getText() != null) {
for (int j = 0; j < etPref.getText().length(); j++) {
maskedPw = maskedPw + "*";
}
pref.setSummary(maskedPw);
}
} else {
EditTextPreference etPref = (EditTextPreference) pref;
pref.setSummary(etPref.getText());
}
}
}

One thing you should notice in the code above is that for any EditTextPreference it encounters, it will check if the password attribute is set and if it is, will in turn encode the password to "*"'s in the summary attribute. That's it, now all your preference summaries will be set to their values automatically and with password protection! For completeness, here is my entire PreferenceActivity class.

package com.brockoli.android.prefstest;import android.content.SharedPreferences;import android.content.SharedPreferences.OnSharedPreferenceChangeListener;import android.os.Bundle;import android.preference.EditTextPreference;import android.preference.ListPreference;import android.preference.Preference;import android.preference.PreferenceActivity;import android.preference.PreferenceScreen;import android.text.InputType;public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {    /**     * Called when activity is started     */    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        addPreferencesFromResource(R.layout.prefs);    }    private void setSummaryAll(PreferenceScreen pScreen) {        // Setup the initial values        for (int i = 0; i < pScreen.getPreferenceCount(); i++) {            Preference pref = pScreen.getPreference(i);            if (pref instanceof ListPreference) {                ListPreference listPref = (ListPreference) pref;                pref.setSummary(listPref.getEntry());            } else if (pref instanceof EditTextPreference) {                if (((EditTextPreference) pref).getEditText().getInputType() == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)) {                    EditTextPreference etPref = (EditTextPreference) pref;                    String maskedPw = "";                    for (int j = 0; j < etPref.getText().length(); j++) {                        maskedPw = maskedPw + "*";                    }                    pref.setSummary(maskedPw);                } else {                    EditTextPreference etPref = (EditTextPreference) pref;                    pref.setSummary(etPref.getText());                }            } else if (pref instanceof PreferenceScreen) {                setSummaryAll((PreferenceScreen) pref);            }        }    }    @Override    protected void onResume() {        super.onResume();        setSummaryAll(getPreferenceScreen());        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);    }    @Override    protected void onPause() {        super.onPause();        // Unregister the listener whenever a key changes        getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);    }    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {        Preference pref = findPreference(key);        if (pref instanceof ListPreference) {            ListPreference listPref = (ListPreference) pref;            pref.setSummary(listPref.getEntry());        } else if (pref instanceof EditTextPreference) {            if (((EditTextPreference) pref).getEditText().getInputType() == (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)) {                EditTextPreference etPref = (EditTextPreference) pref;                String maskedPw = "";                for (int j = 0; j < etPref.getText().length(); j++) {                    maskedPw = maskedPw + "*";                }                pref.setSummary(maskedPw);            } else {                EditTextPreference etPref = (EditTextPreference) pref;                pref.setSummary(etPref.getText());            }        }    }}


3 comments:

  1. Excellent snippet; very useful.

    One minor change I would suggest: Instead of PreferenceScreen as your container type, use PreferenceGroup. Your code will skip any preferences inside of any PreferenceCategory objects.

    ReplyDelete
  2. This code is just what I've been looking for - Thanks for the tutorial! One tiny thing that I noticed is that the xmlns android name space indicator is missing from your sample preference xml.

    Since I'm an Android newbie, this tripped me up

    Thanks again!

    ReplyDelete
  3. This came through as a Google+ and i had a ticket open for it. so thanks

    I tweekt the code to cover RingTonePreference and also GroupPreference

    private void setSummaryAll(PreferenceGroup pScreen) {
    // Setup the initial values
    SharedPreferences mgr;
    mgr = PreferenceManager.getDefaultSharedPreferences(this);

    for (int i = 0; i < pScreen.getPreferenceCount(); i++) {
    Preference pref = pScreen.getPreference(i);
    if (pref instanceof ListPreference) {
    ListPreference listPref = (ListPreference) pref;
    if (!(listPref.getEntry() == null || listPref.getEntry().length() == 0)) {
    pref.setSummary(listPref.getEntry());
    }
    } else if (pref instanceof EditTextPreference) {
    EditTextPreference etPref = (EditTextPreference) pref;
    if (!(etPref.getText() == null || etPref.getText().length() == 0)) {
    pref.setSummary(etPref.getText());
    }
    // Some code is also available to replace passwords with "***",
    // but you should not store passwords in preferences
    } else if (pref instanceof RingtonePreference) {
    RingtonePreference rtPref = (RingtonePreference) pref;
    String uri;
    uri = mgr.getString(rtPref.getKey(), null);
    if (uri != null) {
    Ringtone ringtone = RingtoneManager.getRingtone(this, Uri.parse(uri));
    pref.setSummary(ringtone.getTitle(this));
    }

    // Some code is also available to replace passwords with "***",
    // but you should not store passwords in preferences
    } else if (pref instanceof PreferenceGroup) {
    setSummaryAll((PreferenceGroup) pref);
    }
    }
    }

    ReplyDelete