Preference Summary or Secondary Text

In android settings guidelines we can read :

Secondary text below is for status, not description…

Before Ice Cream Sandwich, we often displayed secondary text below a label to further describe it or provide instructions. Starting in Ice Cream Sandwich, we're using secondary text for status,unless it's a checkbox setting.

Unfortunately, at time of writitng, there doesn't seem to be a simple, automated way of doing this.
In this post we are going to look at how to achieve it. We can get an example from AdvancedPreferences.java in the Android code samples (API Demos).

  1. Step 1: Create a class called MyPreferenceFragment
    This class extends PreferenceFragment and implements onSharedPreferenceChangeListener as shown below.
    
    public class MyPreferenceFragment extends PreferenceFragment
          implements OnSharedPreferenceChangeListener{
        
         @Override
          public void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              addPreferencesFromResource(R.xml.preference_summary);
          }
    
    

  2. Step 2: Listen for changes in preferences
    We can use registerOnSharedPreferenceChangeListener method like this:
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);

    You need to register it in onResume and similarly you also need to unregister the listener which is done in onPause method.
    
          @Override
          public void onResume() {
              super.onResume();
    
              // Set up a listener whenever a key changes
              getPreferenceScreen().getSharedPreferences()
                 .registerOnSharedPreferenceChangeListener(this);
            
              initSummary();
          }
    
          @Override
          protected void onPause() {
               super.onPause();
    
              // Unregister the listener whenever a key changes
              getPreferenceScreen().getSharedPreferences()
                 .unregisterOnSharedPreferenceChangeListener(this);
          }
    

  3. Step 3: Implement onSharedPreferenceChanged method
    Implementing OnSharedPreferenceChangeListener we must use the onSharedPreferenceChanged method. Here you can listen for specific preference keys and use setSummary on the related preference.

    Method onSharedPreferenceChanged has two parameters:
    • Instance of SharedPreferences having the key and value of the Preferences defined in the preferences screen via xml
    • Key of the preferences whose value has changed.
    
          @Override
          public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
    			String key) {
    
               //update summary  
    	   updatePrefsSummary(sharedPreferences, findPreference(key));
          }
    
    

  4. Step 4: Set summary for each preference
    We can use a method like this:
    
       /**
         * Update summary
         * 
         * @param sharedPreferences
         * @param pref
         */
         protected void updatePrefsSummary(SharedPreferences sharedPreferences,
    		Preference pref) {
    
    	if (pref == null)
    		return;
    
    	if (pref instanceof ListPreference) {
    		// List Preference
    		ListPreference listPref = (ListPreference) pref;
    		listPref.setSummary(listPref.getEntry());
    
    	} else if (pref instanceof EditTextPreference) {
    		// EditPreference
     	        EditTextPreference editTextPref = (EditTextPreference) pref;
    		editTextPref.setSummary(editTextPref.getText());
    
    	} else if (pref instanceof MultiSelectListPreference) {
    		// MultiSelectList Preference
    		MultiSelectListPreference mlistPref = (MultiSelectListPreference) pref;
    		String summaryMListPref = "";
    		String and = "";
    
    		// Retrieve values
    		Set<String> values = mlistPref.getValues();
    		for (String value : values) {
    			// For each value retrieve index
    			int index = mlistPref.findIndexOfValue(value);
    			// Retrieve entry from index
    			CharSequence mEntry = index >= 0
    					&& mlistPref.getEntries() != null ? mlistPref
    					.getEntries()[index] : null;
    			if (mEntry != null) {
    				// add summary
    				summaryMListPref = summaryMListPref + and + mEntry;
    				and = ";";
    			}
    		}
    		// set summary
    		mlistPref.setSummary(summaryMListPref);
    
    	} else if (pref instanceof RingtonePreference) {
    		// RingtonePreference
    		RingtonePreference rtPref = (RingtonePreference) pref;
    		String uri;
    		if (rtPref != null) {
    			uri = sharedPreferences.getString(rtPref.getKey(), null);
    			if (uri != null) {
    				Ringtone ringtone = RingtoneManager.getRingtone(
    						getActivity(), Uri.parse(uri));
    					pref.setSummary(ringtone.getTitle(getActivity()));
    			}
    		}
    
    	} else if (pref instanceof NumberPickerPreference) {
    		// My NumberPicker Preference
    		NumberPickerPreference nPickerPref = (NumberPickerPreference) pref;
    		nPickerPref.setSummary(nPickerPref.getValue());
    	}
    }
    

  5. Step 5: Init initial values
    You'll also need to update the values when you register the listener to ensure you get the right initial values.
    In this way you are sure that summary is processed and updated appropriately even when screen comes up for the first time.

    We can use a method like this:
    
            /*
    	 * Init summary
    	 */
    	protected void initSummary() {
    		for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
    			initPrefsSummary(getPreferenceManager().getSharedPreferences(),
    					getPreferenceScreen().getPreference(i));
    		}
    	}
    
    	/*
    	 * Init single Preference
    	 */
    	protected void initPrefsSummary(SharedPreferences sharedPreferences,
    			Preference p) {
    		if (p instanceof PreferenceCategory) {
    			PreferenceCategory pCat = (PreferenceCategory) p;
    			for (int i = 0; i < pCat.getPreferenceCount(); i++) {
    			     initPrefsSummary(sharedPreferences, pCat.getPreference(i));
    			}
    		} else {
    			updatePrefsSummary(sharedPreferences, p);
    		}
    	}
    

The core of example is updatePrefsSummary method.
Here we set summary for each type of Preference.

In RingtonePreference, I have choosen to display title of ringtone, and in MultiSelectListPreference I have used a token like ";". It is just an example.

In your custom preference you can do everything, but you have to pay attention to setSummary(int value) method.
In my case I have an integer value, so I had to override the method.

        @Override
	public void setSummary(int value) {
		setSummary(String.valueOf(value)+" " + minutes);
	}
This is necessary because the standard method does this:

    /**
     * Sets the summary for this Preference with a resource ID. 
     * 
     * @see #setSummary(CharSequence)
     * @param summaryResId The summary as a resource.
     */
    public void setSummary(int summaryResId) {
        setSummary(mContext.getString(summaryResId));
    }
A note about ListPreference. In android doc we can read:

If the summary has a String formatting marker in it (i.e. “%s” or “%1$s”), then the current entry value will be substituted in its place when it’s retrieved.

As a result, if you set “summary” that contains “%” char (like "5%"), you can have java.util.UnknownFormatConversionException: Conversion: because it may be an unknown format.
The reason is here (standard method in ListPreference)

@Override
    public CharSequence getSummary() {
        final CharSequence entry = getEntry();
        if (mSummary == null || entry == null) {
            return super.getSummary();
        } else {
            return String.format(mSummary, entry);
        }
    }
If you want to use value like "5%" in ListPreference, in API Level 11 or higher, you need to be carefuf.
I suggest you create a subclass of ListPreference, override getSummary() method and return whatever you need, skipping the call to String.format().
Alternatively you can obtain percent character in String.format() by specifying "%%".

A note about use of passwords with Shared Preferences. Passwords are always a tricky thing to store, and I'd be particularly wary of storing them as clear text.
If possible I'd consider modifying the server to use a negotiated token for providing access, something like OAuth.
However if you use Shared Preferences with passwords, you have to change the above code if you don't want to see password in summary as clear text.

A final note about RingtonePreference.
The initial value is loaded by initPrefsSummary() and it is correct.
However it does not fire onSharedPreferenceChanged when a ringtone is selected. If you see SharedPreferences.java source file, you can note this:

Note: currently this class does not support use across multiple processes. This will be added later.

For our purposes we can do a workaround.
We can use OnPreferenceChangeListener interface with ringtonePreference.


   class RingToneOnPreferenceChangeListener implements OnPreferenceChangeListener{
		
     @Override
     public boolean onPreferenceChange(Preference pref, Object newValue) {
	if (newValue!=null && newValue instanceof String){
	    String uri=(String)newValue;
	    Ringtone ringtone = RingtoneManager.getRingtone(getActivity(), Uri.parse(uri));
	    pref.setSummary(ringtone.getTitle(getActivity()));	
	}
	return true;
     }
   }

       protected void initPrefsSummary(SharedPreferences sharedPreferences,Preference p){
           if (p instanceof PreferenceCategory){
                PreferenceCategory pCat = (PreferenceCategory)p;
                for(int i=0;i<pCat.getPreferenceCount();i++){
                    initPrefsSummary(sharedPreferences,pCat.getPreference(i));
                }
            }else{
                updatePrefsSummary(sharedPreferences,p);
                if (p instanceof RingtonePreference)
                	p.setOnPreferenceChangeListener(
                  new RingToneOnPreferenceChangeListener());
            }
      }

Implementing OnPreferenceChangeListener and using the onPreferenceChange method you can listen when a RingtonePreference has been changed by the user and you can use setSummary on the related preference.

Wrapping up, in this example we have seen how set summary in common android preferences using OnSharedPreferenceChangeListener.


You can get full code from GitHub:

Comments

Popular posts from this blog

AntiPattern: freezing a UI with Broadcast Receiver

How to centralize the support libraries dependencies in gradle

NotificationListenerService and kitkat