1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package android.support.v14.preference;
18
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.content.res.TypedArray;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.support.annotation.ArrayRes;
25import android.support.annotation.NonNull;
26import android.support.v4.content.SharedPreferencesCompat;
27import android.support.v4.content.res.TypedArrayUtils;
28import android.support.v7.preference.DialogPreference;
29import android.util.AttributeSet;
30
31import java.util.Collections;
32import java.util.HashSet;
33import java.util.Set;
34
35/**
36 * A {@link android.support.v7.preference.Preference} that displays a list of entries as
37 * a dialog.
38 * <p>
39 * This preference will store a set of strings into the SharedPreferences.
40 * This set will contain one or more values from the
41 * {@link #setEntryValues(CharSequence[])} array.
42 *
43 * @attr name android:entries
44 * @attr name android:entryValues
45 */
46public class MultiSelectListPreference extends DialogPreference {
47    private CharSequence[] mEntries;
48    private CharSequence[] mEntryValues;
49    private Set<String> mValues = new HashSet<>();
50
51    public MultiSelectListPreference(
52            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
53        super(context, attrs, defStyleAttr, defStyleRes);
54
55        final TypedArray a = context.obtainStyledAttributes(attrs,
56                android.support.v7.preference.R.styleable.MultiSelectListPreference, defStyleAttr,
57                defStyleRes);
58
59        mEntries = TypedArrayUtils.getTextArray(a,
60                android.support.v7.preference.R.styleable.MultiSelectListPreference_entries,
61                android.support.v7.preference.R.styleable.MultiSelectListPreference_android_entries);
62
63        mEntryValues = TypedArrayUtils.getTextArray(a,
64                android.support.v7.preference.R.styleable.MultiSelectListPreference_entryValues,
65                android.support.v7.preference.R.styleable.MultiSelectListPreference_android_entryValues);
66
67        a.recycle();
68    }
69
70    public MultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
71        this(context, attrs, defStyleAttr, 0);
72    }
73
74    public MultiSelectListPreference(Context context, AttributeSet attrs) {
75        this(context, attrs, TypedArrayUtils.getAttr(context,
76                android.support.v7.preference.R.attr.dialogPreferenceStyle,
77                android.R.attr.dialogPreferenceStyle));
78    }
79
80    public MultiSelectListPreference(Context context) {
81        this(context, null);
82    }
83
84    /**
85     * Attempts to persist a set of Strings to the {@link android.content.SharedPreferences}.
86     * <p>
87     * This will check if this Preference is persistent, get an editor from
88     * the {@link android.preference.PreferenceManager}, put in the strings, and check if we should
89     * commit (and commit if so).
90     *
91     * @param values The values to persist.
92     * @return True if the Preference is persistent. (This is not whether the
93     *         value was persisted, since we may not necessarily commit if there
94     *         will be a batch commit later.)
95     * @see #getPersistedString
96     *
97     * @hide
98     */
99    protected boolean persistStringSet(Set<String> values) {
100        if (shouldPersist()) {
101            // Shouldn't store null
102            if (values.equals(getPersistedStringSet(null))) {
103                // It's already there, so the same as persisting
104                return true;
105            }
106
107            SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit();
108            editor.putStringSet(getKey(), values);
109            SharedPreferencesCompat.EditorCompat.getInstance().apply(editor);
110            return true;
111        }
112        return false;
113    }
114
115    /**
116     * Attempts to get a persisted set of Strings from the
117     * {@link android.content.SharedPreferences}.
118     * <p>
119     * This will check if this Preference is persistent, get the SharedPreferences
120     * from the {@link android.preference.PreferenceManager}, and get the value.
121     *
122     * @param defaultReturnValue The default value to return if either the
123     *            Preference is not persistent or the Preference is not in the
124     *            shared preferences.
125     * @return The value from the SharedPreferences or the default return
126     *         value.
127     * @see #persistStringSet(Set)
128     *
129     * @hide
130     */
131    protected Set<String> getPersistedStringSet(Set<String> defaultReturnValue) {
132        if (!shouldPersist()) {
133            return defaultReturnValue;
134        }
135
136        return getPreferenceManager().getSharedPreferences()
137                .getStringSet(getKey(), defaultReturnValue);
138    }
139
140    /**
141     * Sets the human-readable entries to be shown in the list. This will be
142     * shown in subsequent dialogs.
143     * <p>
144     * Each entry must have a corresponding index in
145     * {@link #setEntryValues(CharSequence[])}.
146     *
147     * @param entries The entries.
148     * @see #setEntryValues(CharSequence[])
149     */
150    public void setEntries(CharSequence[] entries) {
151        mEntries = entries;
152    }
153
154    /**
155     * @see #setEntries(CharSequence[])
156     * @param entriesResId The entries array as a resource.
157     */
158    public void setEntries(@ArrayRes int entriesResId) {
159        setEntries(getContext().getResources().getTextArray(entriesResId));
160    }
161
162    /**
163     * The list of entries to be shown in the list in subsequent dialogs.
164     *
165     * @return The list as an array.
166     */
167    public CharSequence[] getEntries() {
168        return mEntries;
169    }
170
171    /**
172     * The array to find the value to save for a preference when an entry from
173     * entries is selected. If a user clicks on the second item in entries, the
174     * second item in this array will be saved to the preference.
175     *
176     * @param entryValues The array to be used as values to save for the preference.
177     */
178    public void setEntryValues(CharSequence[] entryValues) {
179        mEntryValues = entryValues;
180    }
181
182    /**
183     * @see #setEntryValues(CharSequence[])
184     * @param entryValuesResId The entry values array as a resource.
185     */
186    public void setEntryValues(@ArrayRes int entryValuesResId) {
187        setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
188    }
189
190    /**
191     * Returns the array of values to be saved for the preference.
192     *
193     * @return The array of values.
194     */
195    public CharSequence[] getEntryValues() {
196        return mEntryValues;
197    }
198
199    /**
200     * Sets the value of the key. This should contain entries in
201     * {@link #getEntryValues()}.
202     *
203     * @param values The values to set for the key.
204     */
205    public void setValues(Set<String> values) {
206        mValues.clear();
207        mValues.addAll(values);
208
209        persistStringSet(values);
210    }
211
212    /**
213     * Retrieves the current value of the key.
214     */
215    public Set<String> getValues() {
216        return mValues;
217    }
218
219    /**
220     * Returns the index of the given value (in the entry values array).
221     *
222     * @param value The value whose index should be returned.
223     * @return The index of the value, or -1 if not found.
224     */
225    public int findIndexOfValue(String value) {
226        if (value != null && mEntryValues != null) {
227            for (int i = mEntryValues.length - 1; i >= 0; i--) {
228                if (mEntryValues[i].equals(value)) {
229                    return i;
230                }
231            }
232        }
233        return -1;
234    }
235
236    protected boolean[] getSelectedItems() {
237        final CharSequence[] entries = mEntryValues;
238        final int entryCount = entries.length;
239        final Set<String> values = mValues;
240        boolean[] result = new boolean[entryCount];
241
242        for (int i = 0; i < entryCount; i++) {
243            result[i] = values.contains(entries[i].toString());
244        }
245
246        return result;
247    }
248
249    @Override
250    protected Object onGetDefaultValue(TypedArray a, int index) {
251        final CharSequence[] defaultValues = a.getTextArray(index);
252        final Set<String> result = new HashSet<>();
253
254        for (final CharSequence defaultValue : defaultValues) {
255            result.add(defaultValue.toString());
256        }
257
258        return result;
259    }
260
261    @Override
262    protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
263        setValues(restoreValue ? getPersistedStringSet(mValues) : (Set<String>) defaultValue);
264    }
265
266    @Override
267    protected Parcelable onSaveInstanceState() {
268        final Parcelable superState = super.onSaveInstanceState();
269        if (isPersistent()) {
270            // No need to save instance state
271            return superState;
272        }
273
274        final SavedState myState = new SavedState(superState);
275        myState.values = getValues();
276        return myState;
277    }
278
279    @Override
280    protected void onRestoreInstanceState(Parcelable state) {
281        if (state == null || !state.getClass().equals(SavedState.class)) {
282            // Didn't save state for us in onSaveInstanceState
283            super.onRestoreInstanceState(state);
284            return;
285        }
286
287        SavedState myState = (SavedState) state;
288        super.onRestoreInstanceState(myState.getSuperState());
289        setValues(myState.values);
290    }
291
292    private static class SavedState extends BaseSavedState {
293        Set<String> values;
294
295        public SavedState(Parcel source) {
296            super(source);
297            final int size = source.readInt();
298            values = new HashSet<>();
299            String[] strings = new String[size];
300            source.readStringArray(strings);
301
302            Collections.addAll(values, strings);
303        }
304
305        public SavedState(Parcelable superState) {
306            super(superState);
307        }
308
309        @Override
310        public void writeToParcel(@NonNull Parcel dest, int flags) {
311            super.writeToParcel(dest, flags);
312            dest.writeInt(values.size());
313            dest.writeStringArray(values.toArray(new String[values.size()]));
314        }
315
316        public static final Parcelable.Creator<SavedState> CREATOR =
317                new Parcelable.Creator<SavedState>() {
318                    public SavedState createFromParcel(Parcel in) {
319                        return new SavedState(in);
320                    }
321
322                    public SavedState[] newArray(int size) {
323                        return new SavedState[size];
324                    }
325                };
326    }
327}
328