Search code examples
androidsharedpreferencesandroid-preferences

Custom Preference with a StringSet


I have a list of folders I'd like to display in a preference as a list of strings. I'm storing the folders via SharedPreferences.Editor.putStringSet(). The user can click a row to remove that entry.

I'm not sure how to display the values in a custom Preference; a ListView would be ideal. The built in preferences don't support this use case and Preference.getPersistedStringSet is hidden "pending API approval", apparently for years, so a custom Preference won't work easily.

I could use a delimited string with a static helper method within the custom Preference to ensure a call to SharedPreferences.Editor.putString() (instead of putStringSet) is formatted properly, but that's a bit sloppy. Any better ideas?


Solution

  • So I created a custom DialogPreference as the existing views would have been a poor/confusing user experience. It will actually link to (and requires) a StringSet preference key and allow the user to remove entries. You could modify the onClick to have it behave differently.

    public class RemovableListPreference extends DialogPreference implements OnClickListener
    {
        private static final String androidns="http://schemas.android.com/apk/res/android";
    
        private ListView mListView;
        private ArrayAdapter<String> mAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1);
        private TextView mSplashText,mValueText;
        private String mPreferenceKey;
        private Context mContext;
    
        private String mDialogMessage;
    
        public RemovableListPreference(Context context, AttributeSet attrs) {
    
            super(context,attrs);
            mContext = context;
    
            // Message attribute for the alert dialog
            int mDialogMessageId = attrs.getAttributeResourceValue(androidns, "dialogMessage", 0);
            if(mDialogMessageId == 0)
                mDialogMessage = attrs.getAttributeValue(androidns, "dialogMessage");
            else
                mDialogMessage = mContext.getString(mDialogMessageId);
    
            // Key attribute for the view, used to automatically update the preferences
            int preferenceId = attrs.getAttributeResourceValue(androidns, "key", -1);
            if (preferenceId == -1)
                throw new RuntimeException(RemovableListPreference.class.getSimpleName() + " requires a preference key (android:key)");
            else
                mPreferenceKey = mContext.getString(preferenceId);
        }
    
        @Override
        protected View onCreateDialogView() {
    
            LinearLayout.LayoutParams params;
            LinearLayout layout = new LinearLayout(mContext);
            layout.setOrientation(LinearLayout.VERTICAL);
            layout.setPadding(6,6,6,6);
    
            mSplashText = new TextView(mContext);
            if (mDialogMessage != null)
                mSplashText.setText(mDialogMessage);
            layout.addView(mSplashText);
    
            mValueText = new TextView(mContext);
            mValueText.setGravity(Gravity.CENTER_HORIZONTAL);
            mValueText.setTextSize(32);
            params = new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.MATCH_PARENT);
            layout.addView(mValueText, params);
    
            mListView = new ListView(mContext);
            mListView.setDivider(getDivider());
    
            mListView.setAdapter(mAdapter);
            mListView.setOnItemClickListener(new AdapterView.OnItemClickListener()
            {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id)
                {
                    mAdapter.remove(mAdapter.getItem(position));
                }
            });
            layout.addView(mListView, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
    
            return layout;
        }
    
        public void setEntries(Set<String> entries)
        {
            mAdapter.clear();
            mAdapter.addAll(entries);
        }
    
        @Override
        public void showDialog(Bundle state) {
    
            super.showDialog(state);
    
            SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(mContext);
            Set<String> excludedFolders = pref.getStringSet(mContext.getString(R.string.KEY_EXCLUDED_FOLDERS), new HashSet<String>());
            mAdapter.clear();
            mAdapter.addAll(excludedFolders);
    
            Button positiveButton = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
            positiveButton.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            if (mPreferenceKey != null)
            {
                SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(mContext).edit();
                Set<String> rows = new HashSet<>();
                for (int i = 0; i < mAdapter.getCount(); ++i)
                {
                    rows.add(mAdapter.getItem(i));
                }
                editor.putStringSet(mPreferenceKey, rows);
                editor.apply();
            }
            ((AlertDialog) getDialog()).dismiss();
        }
    
        private Drawable getDivider() {
            int[] attrs = { android.R.attr.listDivider };
            TypedArray a = mContext.obtainStyledAttributes(attrs);
            Drawable divider = a.getDrawable(0);
            a.recycle();
    
            return divider;
        }
    }
    

    Ex. header:

    <com.anthonymandra.widget.RemovableListPreference
        android:key="@string/KEY_EXCLUDED_FOLDERS"
        android:negativeButtonText=""
        android:title="@string/excludedFolders"
        android:summary="@string/excludedSummary"
        android:dialogTitle="@string/excludedFolders"
        android:dialogMessage="@string/excludedMessage"/>