com.albedinsky.android.setting.SettingSelectionDialogPreference.java Source code

Java tutorial

Introduction

Here is the source code for com.albedinsky.android.setting.SettingSelectionDialogPreference.java

Source

/*
 * =================================================================================================
 *                             Copyright (C) 2016 Martin Albedinsky
 * =================================================================================================
 *         Licensed under the Apache License, Version 2.0 or later (further "License" only).
 * -------------------------------------------------------------------------------------------------
 * You may use this file only in compliance with the License. More details and copy of this License 
 * you may obtain at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 * You can redistribute, modify or publish any part of the code written within this file but as it 
 * is described in the License, the software distributed under the License is distributed on an 
 * "AS IS" BASIS, WITHOUT WARRANTIES or CONDITIONS OF ANY KIND.
 * 
 * See the License for the specific language governing permissions and limitations under the License.
 * =================================================================================================
 */
package com.albedinsky.android.setting;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Build;
import android.support.annotation.ArrayRes;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StyleRes;
import android.util.AttributeSet;
import android.view.View;

import com.albedinsky.android.dialog.Dialog;
import com.albedinsky.android.dialog.SelectionDialog;
import com.albedinsky.android.dialog.adapter.DialogSelectionAdapter;
import com.albedinsky.android.setting.R;

import org.json.JSONArray;
import org.json.JSONException;

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

/**
 * A {@link SettingDialogPreference} implementation that may be used to allow to a user to select
 * its preferred values, either in <b>single</b> or <b>multiple</b> choice mode, for a specific
 * setting preference. Entries that should be available for selection may be specified via
 * {@link #setEntries(CharSequence[])} along with theirs corresponding entry values that should be
 * specified via {@link #setEntryValues(CharSequence[])}.
 * <p>
 * This preference implementation by default displays the preferred entry values as its summary text,
 * using {@link SummaryTextBuilder}, that may be specified via {@link #setSummaryTextBuilder(SummaryTextBuilder)},
 * to build the summary text from the selected entry values. If there are no preferred entry values
 * selected yet, the standard summary text is displayed. The selected entry values are persisted as
 * {@link String} in Json Array format. The preferred entry values may be specified via {@link #setSelection(long[])}
 * where the passed array should contain indexes of entry values specified via {@link #setEntryValues(CharSequence[])}
 * to be persisted. The current selection array may be obtained via {@link #getSelection()}. Array
 * of persisted entry values may be obtained via {@link #getSelecedEntryValues()}. Outside of context
 * of the selection preference it may be obtained via {@link #selectedEntryValuesFromPersistedValues(String)}
 * method which accepts {@link String} containing persisted entry values in Json Array format.
 * <p>
 * When {@link #handleOnDialogButtonClick(Dialog, int)} is called, this preference implementation
 * handles only {@link SelectionDialog} type of dialog. If its {@link Dialog#BUTTON_POSITIVE} button
 * has been clicked, the selection array provided via {@link SelectionDialog#getSelection()} is set
 * as selection for this preference via {@link #setSelection(long[])}.
 *
 * <h3>Default value</h3>
 * Default value for this preference is parsed as {@link String} which should contain entry values
 * that should be by default selected, in Json Array format like {@code "["entry_value_1", "entry_value_2"]"}.
 * See {@link TypedArray#getString(int)}.
 *
 * <h3>Xml attributes</h3>
 * See {@link R.styleable#Ui_Settings_SelectionDialogPreference SettingSelectionDialogPreference Attributes}
 *
 * <h3>Dialog Xml attributes</h3>
 * <ul>
 * <li>{@link R.attr#dialogSelectionMode dialogSelectionMode}</li>
 * <li>{@link R.attr#dialogEmptySelectionAllowed dialogEmptySelectionAllowed}</li>
 * </ul>
 *
 * <h3>Default style attribute</h3>
 * {@link R.attr#uiSettingSelectionDialogPreferenceStyle uiSettingSelectionDialogPreferenceStyle}
 *
 * @author Martin Albedinsky
 */
public class SettingSelectionDialogPreference extends SettingDialogPreference<SelectionDialog.SelectionOptions> {

    /**
     * Interface ===================================================================================
     */

    /**
     * Interface for builder that is used by {@link SettingSelectionDialogPreference} to build its
     * summary text from the current selection of entry items.
     *
     * @author Martin Albedinsky
     * @see #setSummaryTextBuilder(SummaryTextBuilder)
     */
    public interface SummaryTextBuilder {

        /**
         * Clears the current content of this builder.
         * <p>
         * This is called by the associated selection preference whenever a new summary text is about
         * to be build.
         *
         * @return This builder to allow methods chaining.
         */
        SummaryTextBuilder clear();

        /**
         * Appends the given <var>entry</var> into content of this builder. If this builder uses
         * separator to separate multiple entries, such separator will be included accordingly.
         * <p>
         * This is called by the associated selection preference for each selected entry.
         *
         * @param entry The entry to be appended.
         * @return This builder to allow methods chaining.
         */
        SummaryTextBuilder appendEntry(@NonNull CharSequence entry);

        /**
         * Builds summary text from the current content (entries) appended into this builder.
         * <p>
         * This is called by the associated selection preference when all currently selected entries
         * have been appended into this builder.
         *
         * @return Summary text with entries.
         */
        @NonNull
        CharSequence build();
    }

    /**
     * Constants ===================================================================================
     */

    /**
     * Log TAG.
     */
    // private static final String TAG = "SettingSelectionDialogPreference";

    /**
     * Default separator for entry items presented in the summary text.
     */
    private static final String SUMMARY_ENTRIES_SEPARATOR = ", ";

    /**
     * Static members ==============================================================================
     */

    /**
     * Members =====================================================================================
     */

    /**
     * Array containing entries from which are created {@link #mDialogItems} to be displayed in the
     * selection dialog.
     */
    private CharSequence[] mEntries;

    /**
     * Array containing values where each value is associated with one entry item. Value for the
     * associated selected entry is persisted in the preferences.
     */
    private CharSequence[] mEntryValues;

    /**
     * Boolean flag indicating whether the selection value for this preference has been set or not.
     * This flag is used to handle case when the same value is being specified for this preference,
     * but for the first time, to properly refresh view of this preference and notify listeners about
     * the change.
     */
    private boolean mSelectionSet;

    /**
     * Current selection value specified for this preference. This may be either value specified by
     * the user, default value or persisted value.
     * <p>
     * Note, that this selection contains indexes of the preferred entry values from the {@link #mEntryValues}
     * array. The selection is created from the persisted Json array containing selected entry values
     * via {@link #createSelectionFromPersistedValues(String)} or transformed into persistable Json
     * array of entry values via {@link #createPersistableValuesFromSelection(long[])} and persisted
     * as {@link String} via {@link #persistString(String)}.
     */
    private long[] mSelection;

    /**
     * List containing selectable items converted from array of {@link #mEntries} to be displayed in
     * the selection dialog.
     */
    private List<DialogSelectionAdapter.Item> mDialogItems;

    /**
     * Builder that is used to build a summary text for the current selected items.
     */
    private SummaryTextBuilder mSummaryTextBuilder;

    /**
     * Constructors ================================================================================
     */

    /**
     * Same as {@link #SettingSelectionDialogPreference(Context, AttributeSet)} without attributes.
     */
    public SettingSelectionDialogPreference(@NonNull Context context) {
        this(context, null);
    }

    /**
     * Same as {@link #SettingSelectionDialogPreference(Context, AttributeSet, int)} with
     * {@link R.attr#uiSettingSelectionDialogPreferenceStyle} as attribute for default style.
     */
    public SettingSelectionDialogPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.uiSettingSelectionDialogPreferenceStyle);
    }

    /**
     * Same as {@link #SettingSelectionDialogPreference(Context, AttributeSet, int, int)} with {@code 0}
     * as default style.
     */
    public SettingSelectionDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.init(context, attrs, defStyleAttr, 0);
    }

    /**
     * Creates a new instance of SettingSelectionDialogPreference within the given <var>context</var>.
     *
     * @param context      Context in which will be the new setting preference presented.
     * @param attrs        Set of Xml attributes used to configure the new instance of this preference.
     * @param defStyleAttr An attribute which contains a reference to a default style resource for
     *                     this preference within a theme of the given context.
     * @param defStyleRes  Resource id of the default style for the new preference.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public SettingSelectionDialogPreference(@NonNull Context context, @Nullable AttributeSet attrs,
            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.init(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Methods =====================================================================================
     */

    /**
     * Called from one of constructors of this setting preference to perform its initialization.
     * <p>
     * Initialization is done via parsing of the specified <var>attrs</var> set and obtaining for
     * this preference specific data from it that can be used to configure this new preference instance.
     * The specified <var>defStyleAttr</var> and <var>defStyleRes</var> are used to obtain default
     * data from the current theme provided by the specified <var>context</var>.
     */
    private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        final TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.Ui_Settings_SelectionDialogPreference, defStyleAttr, defStyleRes);
        setEntries(typedArray.getTextArray(R.styleable.Ui_Settings_SelectionDialogPreference_android_entries));
        setEntryValues(
                typedArray.getTextArray(R.styleable.Ui_Settings_SelectionDialogPreference_android_entryValues));
        typedArray.recycle();
        this.mSummaryTextBuilder = new DefaultSummaryTextBuilder(SUMMARY_ENTRIES_SEPARATOR);
    }

    /**
     */
    @NonNull
    @Override
    protected SelectionDialog.SelectionOptions onCreateDialogOptions(@NonNull Resources resources) {
        return new SelectionDialog.SelectionOptions(resources).title(getTitle())
                .selectionMode(DialogSelectionAdapter.SINGLE).emptySelectionAllowed(true);
    }

    /**
     */
    @Override
    @SuppressWarnings("ResourceType")
    protected void onConfigureDialogOptions(@NonNull SelectionDialog.SelectionOptions options,
            @NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr,
            @StyleRes int defStyleRes) {
        super.onConfigureDialogOptions(options, context, attrs, defStyleAttr, defStyleRes);
        final TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.Ui_Settings_SelectionDialogPreference, defStyleAttr, defStyleRes);
        for (int i = 0; i < typedArray.getIndexCount(); i++) {
            final int index = typedArray.getIndex(i);
            if (index == R.styleable.Ui_Settings_SelectionDialogPreference_dialogSelectionMode) {
                options.selectionMode(typedArray.getInt(index, options.selectionMode()));
            } else if (index == R.styleable.Ui_Settings_SelectionDialogPreference_dialogEmptySelectionAllowed) {
                options.emptySelectionAllowed(typedArray.getBoolean(index, options.shouldAllowEmptySelection()));
            }
        }
        typedArray.recycle();
    }

    /**
     * Same as {@link #setEntries(CharSequence[])} for resource id.
     *
     * @param resId Resource id of the desired text array with entries.
     */
    public void setEntries(@ArrayRes int resId) {
        setEntries(getContext().getResources().getTextArray(resId));
    }

    /**
     * Specifies an array of entries that should be displayed in the associated selection dialog.
     *
     * @param entries The desired array of entries. May be {@code null} to clear the current ones.
     * @see android.R.attr#entries
     * @see #setEntryValues(CharSequence[])
     */
    public void setEntries(@Nullable CharSequence[] entries) {
        this.mEntries = entries;
        if (entries == null) {
            this.mDialogItems = null;
        } else {
            this.mDialogItems = new ArrayList<>(entries.length);
            for (int i = 0; i < entries.length; i++) {
                final CharSequence entry = entries[i];
                mDialogItems.add(new SelectionDialog.TextItem(i, entry));
            }
        }
    }

    /**
     * Returns the array of entries specified for this preference.
     *
     * @return Array with entries. May be {@code null} if no entries have been specified.
     * @see #setEntries(CharSequence[])
     * @see #getEntryValues()
     */
    @Nullable
    public CharSequence[] getEntries() {
        return mEntries;
    }

    /**
     * Same as {@link #setEntryValues(CharSequence[])} for resource id.
     *
     * @param resId Resource id of the desired text array with values for entries.
     */
    public void setEntryValues(@ArrayRes int resId) {
        setEntryValues(getContext().getResources().getTextArray(resId));
    }

    /**
     * Specifies an array of entry values where each value should be associated with corresponding
     * entry from the entries array specified via {@link #setEntries(CharSequence[])}.
     * <p>
     * <b>Note</b>, that this method does not check if the entries and entry values arrays are
     * consistent, that is that they are equal in length.
     *
     * @param entryValues The desired array of values associated with entries. May be {@code null}
     *                    to clear the current ones.
     * @see android.R.attr#entryValues
     */
    public void setEntryValues(@Nullable CharSequence[] entryValues) {
        this.mEntryValues = entryValues;
    }

    /**
     * Returns the array of entry values associated with the entries specified for this preference.
     *
     * @return Array with entry values. May be {@code null} if no entry values have been specified.
     * @see #setEntryValues(CharSequence[])
     * @see #getEntries()
     */
    @Nullable
    public CharSequence[] getEntryValues() {
        return mEntryValues;
    }

    /**
     * Sets a builder that should be used by this preference to build its summary text for the current
     * selected entry items.
     * <p>
     * By default, this preference uses internal implementation of the summary builder which
     * separates the entry items in the summary text by ', '.
     *
     * @param textBuilder The desired builder for the summary text. May be {@code null} to use the
     *                    default one.
     */
    public void setSummaryTextBuilder(@Nullable SummaryTextBuilder textBuilder) {
        this.mSummaryTextBuilder = textBuilder != null ? textBuilder
                : new DefaultSummaryTextBuilder(SUMMARY_ENTRIES_SEPARATOR);
    }

    /**
     */
    @Override
    protected Object onGetDefaultValue(@NonNull TypedArray typedArray, int index) {
        return typedArray.getText(index);
    }

    /**
     */
    @Override
    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
        if (restorePersistedValue) {
            final String persistedValues = getPersistedString(null);
            setSelection(
                    persistedValues != null ? createSelectionFromPersistedValues(persistedValues) : mSelection);
        } else {
            setSelection(createSelectionFromPersistedValues((String) defaultValue));
        }
    }

    /**
     * Creates an array of selected entry values from the given <var>persistedValues</var> String.
     *
     * @param persistedValues The string with entry values in Json Array format persisted for a
     *                        particular {@link SettingSelectionDialogPreference}.
     * @return Array of entry values that have been persisted or {@code null} if no preferred values
     * have been selected/persisted yet.
     */
    @Nullable
    public static String[] selectedEntryValuesFromPersistedValues(@NonNull String persistedValues) {
        try {
            final JSONArray persistedArray = new JSONArray(persistedValues);
            final String[] selectedValues = new String[persistedArray.length()];
            for (int i = 0; i < persistedArray.length(); i++) {
                selectedValues[i] = persistedArray.getString(i);
            }
            return selectedValues;
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Creates the selection array for this preference from the given <var>persistedValues</var>.
     *
     * @param persistedValues The persisted selection values, created via {@link #createPersistableValuesFromSelection(long[])}.
     * @return Selection array containing indexes of the selected entries.
     */
    private long[] createSelectionFromPersistedValues(String persistedValues) {
        try {
            final JSONArray persistedArray = new JSONArray(persistedValues);
            final long[] selection = new long[persistedArray.length()];
            for (int i = 0; i < persistedArray.length(); i++) {
                final String selectedValue = persistedArray.getString(i);
                for (int j = 0; j < mEntryValues.length; j++) {
                    if (mEntryValues[j].equals(selectedValue)) {
                        selection[i] = j;
                        break;
                    }
                }
            }
            return selection;
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Creates the persistable values string from the given <var>selection</var> array.
     *
     * @param selection The selection to be transformed into persistable values string.
     * @return String containing array of entry values according to the specified selection in the
     * Json Array format.
     */
    private String createPersistableValuesFromSelection(long[] selection) {
        final JSONArray persistableArray = new JSONArray();
        for (final long itemIndex : selection) {
            if (((int) itemIndex) < mEntryValues.length) {
                persistableArray.put(mEntryValues[(int) itemIndex]);
            }
        }
        return persistableArray.toString();
    }

    /**
     * Sets a selection array containing indexes of preferred entry values that have been specified
     * via {@link #setEntryValues(CharSequence[])} for this preference.
     *
     * @param selection The desired selection array with indexes of the preferred entry values to
     *                  be persisted as {@link String} in Json Array format.
     * @see #getSelection()
     */
    public void setSelection(@Nullable long[] selection) {
        final boolean changed = !Arrays.equals(mSelection, selection);
        final String selectionValues = createPersistableValuesFromSelection(selection);
        if (callChangeListener(selectionValues) && (changed || !mSelectionSet)) {
            this.mSelection = selection;
            this.mSelectionSet = true;
            persistString(selectionValues);
            if (changed) {
                notifyChanged();
            }
        }
    }

    /**
     * Returns the array of indexes of the preferred entry values of this preference.
     *
     * @return Selection array either specified by the user, as default value or the persisted one.
     * @see #setSelection(long[])
     * @see #getSelecedEntryValues()
     * @see #getDialogOptions()
     */
    @Nullable
    public long[] getSelection() {
        return mSelection;
    }

    /**
     * Returns the array of preferred entry values of this preference.
     *
     * @return Array containing entry values that have been selected either by user, as default value
     * or the persisted one.
     * @see #getSelection()
     */
    @Nullable
    public CharSequence[] getSelecedEntryValues() {
        if (mSelectionSet) {
            final CharSequence[] selectedValues = new CharSequence[mSelection.length];
            for (int i = 0; i < mSelection.length; i++) {
                selectedValues[i] = mEntryValues[(int) mSelection[i]];
            }
            return selectedValues;
        }
        return null;
    }

    /**
     */
    @Override
    public void onBindView(View view) {
        super.onBindView(view);
        synchronizeSummaryView(view);
    }

    /**
     */
    @Nullable
    @Override
    protected CharSequence onGetSummaryText() {
        if (mSelectionSet && mSelection.length > 0) {
            mSummaryTextBuilder.clear();
            for (final long itemIndex : mSelection) {
                mSummaryTextBuilder.appendEntry(mEntries[(int) itemIndex]);
            }
            return mSummaryTextBuilder.build();
        }
        return super.onGetSummaryText();
    }

    /**
     */
    @NonNull
    @Override
    public SelectionDialog.SelectionOptions getDialogOptions() {
        final SelectionDialog.SelectionOptions options = super.getDialogOptions();
        options.items(mDialogItems);
        if (mSelectionSet) {
            options.selection(mSelection);
        }
        return options;
    }

    /**
     */
    @Override
    protected boolean onHandleDialogButtonClick(@NonNull Dialog dialog, @Dialog.Button int button) {
        if (dialog instanceof SelectionDialog) {
            switch (button) {
            case Dialog.BUTTON_POSITIVE:
                setSelection(((SelectionDialog) dialog).getSelection());
                break;
            }
            return true;
        }
        return super.onHandleDialogButtonClick(dialog, button);
    }

    /**
     * Inner classes ===============================================================================
     */

    /**
     * A {@link SummaryTextBuilder} implementation that is used as default builder by the selection
     * preference.
     */
    private static final class DefaultSummaryTextBuilder implements SummaryTextBuilder {

        /**
         * String builder used when building the summary text.
         */
        private final StringBuilder builder;

        /**
         * Separator used to separate entry items.
         */
        private final String separator;

        /**
         * Creates a new instance of DefaultSummaryTextBuilder with the specified <var>separator</var>.
         *
         * @param separator The desired separator used to separate entry items.
         */
        private DefaultSummaryTextBuilder(@NonNull String separator) {
            this.builder = new StringBuilder(64);
            this.separator = separator;
        }

        /**
         */
        @Override
        public SummaryTextBuilder clear() {
            builder.setLength(0);
            return this;
        }

        /**
         */
        @Override
        public SummaryTextBuilder appendEntry(@NonNull CharSequence entry) {
            if (builder.length() > 0) {
                builder.append(separator);
            }
            builder.append(entry);
            return this;
        }

        /**
         */
        @NonNull
        @Override
        public CharSequence build() {
            return builder.toString();
        }
    }
}