/* * Copyright 2014 - 2020 Michael Rapp * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package de.mrapp.android.preference; import android.annotation.TargetApi; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnMultiChoiceClickListener; import android.content.SharedPreferences.Editor; import android.content.res.TypedArray; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import androidx.annotation.AttrRes; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; import de.mrapp.android.dialog.AbstractButtonBarDialog; import de.mrapp.android.dialog.builder.AbstractButtonBarDialogBuilder; import de.mrapp.android.dialog.builder.AbstractListDialogBuilder; import de.mrapp.android.util.view.AbstractSavedState; import de.mrapp.util.Condition; /** * A preference, which allows to select multiple values from a list. The selected values are * persisted as a {@link Set} in the shared preferences. They will only be persisted, if confirmed * by the user. * * @author Michael Rapp * @since 1.7.0 */ public class MultiChoiceListPreference extends AbstractListPreference { /** * A data structure, which allows to save the internal state of a {@link * MultiChoiceListPreference}. */ public static class SavedState extends AbstractSavedState { /** * A creator, which allows to create instances of the class {@link * MultiChoiceListPreference} from parcels. */ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override public SavedState createFromParcel(final Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(final int size) { return new SavedState[size]; } }; /** * The saved value of the attribute "values". */ public Set<String> values; /** * Creates a new data structure, which allows to store the internal state of an {@link * MultiChoiceListPreference}. This constructor is called by derived classes when saving * their states. * * @param superState * The state of the superclass of this view, as an instance of the type {@link * Parcelable}. The state may not be null */ public SavedState(@NonNull final Parcelable superState) { super(superState); } /** * Creates a new data structure, which allows to store the internal state of an {@link * MultiChoiceListPreference}. This constructor is used when reading from a parcel. It reads * the state of the superclass. * * @param source * The parcel to read read from as a instance of the class {@link Parcel}. The * parcel may not be null */ public SavedState(@NonNull final Parcel source) { super(source); List<String> list = new ArrayList<>(); source.readStringList(list); values = new HashSet<>(list); } @Override public final void writeToParcel(final Parcel destination, final int flags) { super.writeToParcel(destination, flags); destination.writeStringList(new ArrayList<>(values)); } } /** * The currently persisted values of the preference. */ private Set<String> values; /** * A set, which contains the indices of the currently selected list items. */ private Set<Integer> selectedIndices; /** * Initializes the preference. */ private void initialize() { selectedIndices = null; setNegativeButtonText(android.R.string.cancel); setPositiveButtonText(android.R.string.ok); } /** * Loads and returns the currently persisted set, which belongs to the preference's key, from * the shared preferences. * * @param defaultValue * The default value, which should be returned, if no set with the preference's key is * currently persisted, as an instance of the type {@link Set} * @return The currently persisted set or the given default value as an instance of the type * {@link Set} */ private Set<String> getPersistedSet(@Nullable final Set<String> defaultValue) { if (!shouldPersist()) { return defaultValue; } return getPreferenceManager().getSharedPreferences().getStringSet(getKey(), defaultValue); } /** * Return the indices of the entries, which correspond to specific values. * * @param values * A set, which contains the values of the entries, whose indices should be returned, as * an instance of the type {@link Set} * @return A list, which contains the indices of the entries, the given values correspond to, as * an instance of the type {@link List} */ private List<Integer> indicesOf(@Nullable final Set<String> values) { List<Integer> indices = new ArrayList<>(); if (values != null && getEntryValues() != null) { for (String value : values) { int index = indexOf(value); if (index >= 0) { indices.add(index); } } } return indices; } /** * Persists a specific set in the shared preferences by using the preference's key. * * @param set * The set, which should be persisted, as an instance of the type {@link Set} * @return True, if the given set has been persisted, false otherwise */ private boolean persistSet(@Nullable final Set<String> set) { if (set != null && shouldPersist()) { if (set.equals(getPersistedSet(null))) { return true; } Editor editor = getPreferenceManager().getSharedPreferences().edit(); editor.putStringSet(getKey(), set); editor.apply(); return true; } return false; } /** * Creates and returns a listener, which allows to observe when list items are selected or * unselected by the user. * * @return The listener, which has been created, as an instance of the type {@link * OnMultiChoiceClickListener} */ private OnMultiChoiceClickListener createListItemListener() { return new OnMultiChoiceClickListener() { @Override public void onClick(final DialogInterface dialog, final int which, final boolean isChecked) { if (isChecked) { selectedIndices.add(which); } else { selectedIndices.remove(which); } } }; } /** * Creates a new preference, which allows to select multiple values from a list. * * @param context * The context, which should be used by the preference, as an instance of the class * {@link Context}. The context may not be null */ public MultiChoiceListPreference(@NonNull final Context context) { this(context, null); } /** * Creates a new preference, which allows to select multiple values from a list. * * @param context * The context, which should be used by the preference, as an instance of the class * {@link Context}. The context may not be null * @param attributeSet * The attributes of the XML tag that is inflating the preference, as an instance of the * type {@link AttributeSet} or null, if no attributes are available */ public MultiChoiceListPreference(@NonNull final Context context, @Nullable final AttributeSet attributeSet) { this(context, attributeSet, R.attr.dialogPreferenceStyle); } /** * Creates a new preference, which allows to select multiple values from a list. * * @param context * The context, which should be used by the preference, as an instance of the class * {@link Context}. The context may not be null * @param attributeSet * The attributes of the XML tag that is inflating the preference, as an instance of the * type {@link AttributeSet} or null, if no attributes are available * @param defaultStyle * The default style to apply to this preference. If 0, no style will be applied (beyond * what is included in the theme). This may either be an attribute resource, whose value * will be retrieved from the current theme, or an explicit style resource */ public MultiChoiceListPreference(@NonNull final Context context, @Nullable final AttributeSet attributeSet, @AttrRes final int defaultStyle) { super(context, attributeSet, defaultStyle); initialize(); } /** * Creates a new preference, which allows to select multiple values from a list. * * @param context * The context, which should be used by the preference, as an instance of the class * {@link Context}. The context may not be null * @param attributeSet * The attributes of the XML tag that is inflating the preference, as an instance of the * type {@link AttributeSet} or null, if no attributes are available * @param defaultStyle * The default style to apply to this preference. If 0, no style will be applied (beyond * what is included in the theme). This may either be an attribute resource, whose value * will be retrieved from the current theme, or an explicit style resource * @param defaultStyleResource * A resource identifier of a style resource that supplies default values for the * preference, used only if the default style is 0 or can not be found in the theme. Can * be 0 to not look for defaults */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public MultiChoiceListPreference(@NonNull final Context context, @Nullable final AttributeSet attributeSet, @AttrRes final int defaultStyle, @StyleRes final int defaultStyleResource) { super(context, attributeSet, defaultStyle, defaultStyleResource); initialize(); } /** * Returns the currently persisted values of the preference. * * @return A set, which contains the currently persisted values of the preference, as an * instance of the type {@link Set} */ public final Set<String> getValues() { return values; } /** * Sets the current values of the preference. By setting values, they will be persisted. * * @param values * A set, which contains the values, which should be set, as an instance of the type * {@link Set} */ public final void setValues(@Nullable final Set<String> values) { if (values != null && !values.equals(this.values)) { this.values = values; persistSet(this.values); notifyChanged(); } } /** * Adds a new value to the preference. By adding a value, the changes will be persisted. * * @param value * The value, which should be added, as a {@link String}. The value may not be null */ public final void addValue(@NonNull final String value) { Condition.INSTANCE.ensureNotNull(value, "The value may not be null"); if (this.values != null) { if (this.values.add(value)) { if (persistSet(this.values)) { notifyChanged(); } } } else { Set<String> newValues = new HashSet<>(); newValues.add(value); setValues(newValues); } } /** * Removes a specific value from the preference. By removing a value, the changes will be * persisted. * * @param value * The value, which should be removed, as a {@link String}. The value may not be null */ public final void removeValue(@NonNull final String value) { Condition.INSTANCE.ensureNotNull(value, "The value may not be null"); if (this.values != null) { if (this.values.remove(value)) { if (persistSet(this.values)) { notifyChanged(); } } } } /** * Adds all values, which are contained by a specific collection, to the preference. By adding * values, the changes will be persisted. * * @param values * A collection, which contains the values, which should be added, as an instance of the * type {@link Collection} or an empty collection, if no values should be added */ public final void addAllValues(@NonNull final Collection<String> values) { Condition.INSTANCE.ensureNotNull(values, "The values may not be null"); if (this.values != null) { if (this.values.addAll(values)) { if (persistSet(this.values)) { notifyChanged(); } } } else { Set<String> newValues = new HashSet<>(values); setValues(newValues); } } /** * Removes all values, which are contained by a specific collection, from the preference. By * removing values, the changes will be persisted. * * @param values * A collection, which contains the values, which should be removed, as an instance of * the type {@link Collection} or an empty collection, if no values should be removed */ public final void removeAllValues(@NonNull final Collection<String> values) { Condition.INSTANCE.ensureNotNull(values, "The values may not be null"); if (this.values != null) { if (this.values.removeAll(values)) { if (persistSet(this.values)) { notifyChanged(); } } } } /** * Returns the entries, the currently persisted values of the preference belong to. * * @return An array, which contains the entries, the currently persisted values of the * preference belong to, as an array of the type {@link CharSequence} */ public final CharSequence[] getSelectedEntries() { List<Integer> indices = indicesOf(values); Collections.sort(indices); if (!indices.isEmpty()) { CharSequence[] selectedEntries = new CharSequence[indices.size()]; int currentIndex = 0; for (int index : indices) { selectedEntries[currentIndex] = getEntries()[index]; currentIndex++; } return selectedEntries; } return null; } @Override public final CharSequence getSummary() { if (isValueShownAsSummary()) { CharSequence[] entries = getSelectedEntries(); if (entries != null && entries.length > 0) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < entries.length; i++) { if (i > 0) { stringBuilder.append(", "); } stringBuilder.append(entries[i]); } return stringBuilder.toString(); } return super.getSummary(); } else { return super.getSummary(); } } @Override protected final Object onGetDefaultValue(final TypedArray typedArray, final int index) { CharSequence[] defaultValues = typedArray.getTextArray(index); if (defaultValues != null) { Set<String> defaultValue = new HashSet<>(); for (CharSequence value : defaultValues) { defaultValue.add(value.toString()); } return defaultValue; } return null; } @SuppressWarnings("unchecked") @Override protected final void onSetInitialValue(final Object defaultValue) { setValues(defaultValue == null ? getPersistedSet(getValues()) : (Set<String>) defaultValue); } @Override protected final void onPrepareDialog( @NonNull final AbstractButtonBarDialogBuilder<?, ?> dialogBuilder) { super.onPrepareDialog(dialogBuilder); selectedIndices = new HashSet<>(indicesOf(values)); boolean[] checkedItems = new boolean[getEntryValues().length]; for (int selectedIndex : selectedIndices) { checkedItems[selectedIndex] = true; } ((AbstractListDialogBuilder<?, ?>) dialogBuilder) .setMultiChoiceItems(getEntries(), checkedItems, createListItemListener()); } @CallSuper @Override protected void onDialogClosed(@NonNull final AbstractButtonBarDialog dialog, final boolean positiveResult) { if (positiveResult && selectedIndices != null && getEntryValues() != null) { Set<String> newValues = new HashSet<>(); for (int selectedIndex : selectedIndices) { newValues.add(getEntryValues()[selectedIndex].toString()); } if (callChangeListener(newValues)) { setValues(newValues); } } selectedIndices = null; } @CallSuper @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); if (!isPersistent()) { SavedState savedState = new SavedState(superState); savedState.values = getValues(); return savedState; } return superState; } @CallSuper @Override protected void onRestoreInstanceState(final Parcelable state) { if (state instanceof SavedState) { SavedState savedState = (SavedState) state; setValues(savedState.values); super.onRestoreInstanceState(savedState.getSuperState()); } else { super.onRestoreInstanceState(state); } } }