Android: MultiSelectListPreference for SDK < 11

Supporting old versions of Android is getting more and more headache nowadays. Happily, there is a pretty strong community, which continue to contribute various solutions.

In my project, I wanted to use MultiSelectListPreference in order to give a user possibility to select a number of options.
It turned out to be, that Android with SDK less than 11 doesn’t support this class. It means, that all Android devices with OS version less than 4.0 will not have this feature.

Searching Internet helps a lot and so was this time.
I found a pretty interesting solution created by Krzysztof Suszyński
Here is the link to github https://gist.github.com/cardil/4754571

I used the code and it really worked nice for me.
Except, I needed to change the code of the MultiSelectListPreference implementation:

the line “entryChecked = new boolean[getEntries().length];” should be moved from Class initialization to onPrepareDialogBuilder
and sjould look as following:

@Override
    protected void onPrepareDialogBuilder(Builder builder) {
entryChecked = new boolean[getEntries().length];
...
}

this is done in order to make dynamic MultiSelectListPreference working.
So, the whole code in my case looked as following:

package pl.wavesoftware.widget;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnMultiChoiceClickListener;
import android.content.res.TypedArray;
import android.preference.ListPreference;
import android.util.AttributeSet;

public class MultiSelectListPreferenceLegacy extends ListPreference {

    private String separator;
    private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001";
    private boolean[] entryChecked;

    public MultiSelectListPreferenceLegacy(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);

        separator = DEFAULT_SEPARATOR;
    }

    public MultiSelectListPreferenceLegacy(Context context) {
        this(context, null);
    }

    @Override
    protected void onPrepareDialogBuilder(Builder builder) {
        entryChecked = new boolean[getEntries().length];
        CharSequence[] entries = getEntries();
        CharSequence[] entryValues = getEntryValues();
        if (entries == null || entryValues == null
                || entries.length != entryValues.length) {
            throw new IllegalStateException(
                    "MultiSelectListPreference requires an entries array and an entryValues "
                            + "array which are both the same length");
        }

        restoreCheckedEntries();
        OnMultiChoiceClickListener listener = new DialogInterface.OnMultiChoiceClickListener() {
            public void onClick(DialogInterface dialog, int which, boolean val) {
                entryChecked[which] = val;
            }
        };
        builder.setMultiChoiceItems(entries, entryChecked, listener);
    }

    private CharSequence[] unpack(CharSequence val) {
        if (val == null || "".equals(val)) {
            return new CharSequence[0];
        } else {
            return ((String) val).split(separator);
        }
    }

    /**
     * Gets the entries values that are selected
     *
     * @return the selected entries values
     */
    public CharSequence[] getCheckedValues() {
        return unpack(getValue());
    }

    private void restoreCheckedEntries() {
        CharSequence[] entryValues = getEntryValues();

        // Explode the string read in sharedpreferences
        CharSequence[] vals = unpack(getValue());

        if (vals != null) {
            List valuesList = Arrays.asList(vals);
            for (int i = 0; i < entryValues.length; i++) {
                CharSequence entry = entryValues[i];
                entryChecked[i] = valuesList.contains(entry);
            }
        }
    }

    @Override
    protected void onDialogClosed(boolean positiveResult) {
        List values = new ArrayList();

        CharSequence[] entryValues = getEntryValues();
        if (positiveResult && entryValues != null) {
            for (int i = 0; i < entryValues.length; i++) {
                if (entryChecked[i] == true) {
                    String val = (String) entryValues[i];
                    values.add(val);
                }
            }

            String value = join(values, separator);
            setSummary(prepareSummary(values));
            setValueAndEvent(value);
        }
    }

    private void setValueAndEvent(String value) {
        if (callChangeListener(unpack(value))) {
            setValue(value);
        }
    }

    private CharSequence prepareSummary(List joined) {
        List titles = new ArrayList();
        CharSequence[] entryTitle = getEntries();
        CharSequence[] entryValues = getEntryValues();
        int ix = 0;
        for (CharSequence value : entryValues) {
            if (joined.contains(value)) {
                titles.add((String) entryTitle[ix]);
            }
            ix += 1;
        }
        return join(titles, ", ");
    }

    @Override
    protected Object onGetDefaultValue(TypedArray typedArray, int index) {
        return typedArray.getTextArray(index);
    }

    @Override
    protected void onSetInitialValue(boolean restoreValue,
                                     Object rawDefaultValue) {
        String value = null;
        CharSequence[] defaultValue;
        if (rawDefaultValue == null) {
            defaultValue = new CharSequence[0];
        } else {
            defaultValue = (CharSequence[]) rawDefaultValue;
        }
        List joined = Arrays.asList(defaultValue);
        String joinedDefaultValue = join(joined, separator);
        if (restoreValue) {
            value = getPersistedString(joinedDefaultValue);
        } else {
            value = joinedDefaultValue;
        }

        setSummary(prepareSummary(Arrays.asList(unpack(value))));
        setValueAndEvent(value);
    }

    /**
     * Joins array of object to single string by separator
     *
     * Credits to kurellajunior on this post
     * http://snippets.dzone.com/posts/show/91
     *
     * @param iterable
     *            any kind of iterable ex.: ["a", "b", "c"]
     * @param separator
     *            separetes entries ex.: ","
     * @return joined string ex.: "a,b,c"
     */
    protected static String join(Iterable<?> iterable, String separator) {
        Iterator<?> oIter;
        if (iterable == null || (!(oIter = iterable.iterator()).hasNext()))
            return "";
        StringBuilder oBuilder = new StringBuilder(String.valueOf(oIter.next()));
        while (oIter.hasNext())
            oBuilder.append(separator).append(oIter.next());
        return oBuilder.toString();
    }

}

NOTE: I changed the name of the class in order to use both variants – new and legacy ones.
and now, the implementation of the code.

First, I created Preferences class in order to select proper settings:

package com.kosherdev.koshergator;

import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.*;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import com.kosherdev.koshergator.actionbar.ActionBarPreferenceActivity;
import com.kosherdev.koshergator.constants.KosherSources;
import com.kosherdev.koshergator.helpers.Helpers;
import pl.wavesoftware.widget.MultiSelectListPreferenceLegacy;

import java.util.*;

public class Preferences extends PreferenceActivity {
    public static final String KEY_SOURCES = "key_sources";
    public static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setPreferenceScreen(createPreferenceHierarchy());

    }

    private PreferenceScreen createPreferenceHierarchy() {
        // Root
        PreferenceScreen root = getPreferenceManager().createPreferenceScreen(this);

        List entriesList = KosherSources.getEntries();
        List entriesValuesList = KosherSources.getEntriesValues();
        Set valuesSet = KosherSources.getSet();

        // kosher sources
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
        {
            String[] entries = new String[ entriesList.size() ];
            String[] entriesValues= new String[ entriesValuesList.size() ];
            entriesList.toArray( entries );
            entriesValuesList.toArray( entriesValues );

            MultiSelectListPreference kosherSourcesPref = new MultiSelectListPreference(this);
            kosherSourcesPref.setEntries(entries);
            kosherSourcesPref.setEntryValues(entriesValues);
            kosherSourcesPref.setKey(KEY_SOURCES);
            kosherSourcesPref.setTitle(R.string.settings_sources);
            kosherSourcesPref.setSummary(R.string.settings_sources_summary);
            final SharedPreferences sharedPrefs =
                    PreferenceManager.getDefaultSharedPreferences(this);
            if (!sharedPrefs.contains(KEY_SOURCES)) {
                kosherSourcesPref.setValues(valuesSet);
            }
            root.addPreference(kosherSourcesPref);
        }else{

            CharSequence[]  entries = new CharSequence[entriesList.size()];
            CharSequence[]  entriesValues = new CharSequence[entriesValuesList.size()];
            entriesList.toArray( entries );
            entriesValuesList.toArray( entriesValues );

            MultiSelectListPreferenceLegacy kosherSourcesPref = new MultiSelectListPreferenceLegacy(this);
            kosherSourcesPref.setEntries(entries);
            kosherSourcesPref.setEntryValues(entriesValues);
            kosherSourcesPref.setKey(KEY_SOURCES);
            kosherSourcesPref.setTitle(R.string.settings_sources);
            kosherSourcesPref.setSummary(R.string.settings_sources_summary);
            final SharedPreferences sharedPrefs =
                    PreferenceManager.getDefaultSharedPreferences(this);
            if (!sharedPrefs.contains(KEY_SOURCES)) {
                List joined = Arrays.asList(entriesValues);
                String joinedDefaultValue = join(joined, DEFAULT_SEPARATOR);
                kosherSourcesPref.setValue(joinedDefaultValue);
            }
            root.addPreference(kosherSourcesPref);
        }
        return root;
    }

    public static String join(Iterable<?> iterable, String separator) {
        Iterator<?> oIter;
        if (iterable == null || (!(oIter = iterable.iterator()).hasNext()))
            return "";
        StringBuilder oBuilder = new StringBuilder(String.valueOf(oIter.next()));
        while (oIter.hasNext())
            oBuilder.append(separator).append(oIter.next());
        return oBuilder.toString();
    }

}

I copied class join function and DEFAULT_SEPARATOR from Krzysztof’s code and made it public in order to use it later in my code.

And here we go – usage of saved preferences in my code:

Set sourcesSet = new HashSet();
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
        {
            sourcesSet = sharedPrefs.getStringSet(Preferences.KEY_SOURCES, KosherSources.getSet());

        }
            else
        {
            CharSequence[]  entriesValues = new CharSequence[KosherSources.getSet().size()];
            KosherSources.getSet().toArray( entriesValues );
            List joined = Arrays.asList(entriesValues);
            String joinedDefaultValue = Preferences.join(joined, Preferences.DEFAULT_SEPARATOR);
            String sourcesPrefs = sharedPrefs.getString(Preferences.KEY_SOURCES, joinedDefaultValue);
            String[] explodeSourcesPrefs = sourcesPrefs.split(Preferences.DEFAULT_SEPARATOR);
            for (int i=0; i < explodeSourcesPrefs.length; i++)
            {
                sourcesSet.add(explodeSourcesPrefs[i]);
            }

        }

As a result in both cases we get the same result – Set of Strings.

1 comment

  1. Great solution!!! I was a little lost when I reviewed the “usage of saved preferences” due to the lack of source of KosherSources but was able to figure that part out myself. Over all your site was a BIG help. Thanks!

Leave a Reply

%d bloggers like this: