1/*
2 * Copyright (C) 2013 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import com.android.mail.R;
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.Sets;
23
24import android.app.AlertDialog;
25import android.app.Dialog;
26import android.app.DialogFragment;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.DialogInterface.OnClickListener;
30import android.os.Bundle;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.widget.AdapterView;
35import android.widget.AdapterView.OnItemClickListener;
36import android.widget.BaseAdapter;
37import android.widget.CheckedTextView;
38import android.widget.ListView;
39
40
41import java.lang.ref.WeakReference;
42import java.util.ArrayList;
43import java.util.Collection;
44import java.util.List;
45import java.util.Set;
46
47/**
48 * A {@link DialogFragment} that shows multiple values, and lets you select 0 to
49 * {@link #MAX_SELECTED_VALUES} of them.
50 */
51public abstract class LimitedMultiSelectDialogFragment extends DialogFragment {
52    public interface LimitedMultiSelectDialogListener {
53        void onSelectionChanged(Set<String> selectedValues);
54    }
55
56    private static final String ARG_ENTRIES = "entries";
57    private static final String ARG_ENTRY_VALUES = "entryValues";
58    private static final String ARG_SELECTED_VALUES = "selectedValues";
59
60    private WeakReference<LimitedMultiSelectDialogListener> mListener = null;
61
62    // Public no-args constructor needed for fragment re-instantiation
63    public LimitedMultiSelectDialogFragment() {}
64    /**
65     * Populates the arguments on a {@link LimitedMultiSelectDialogFragment}.
66     */
67    protected static void populateArguments(final LimitedMultiSelectDialogFragment fragment,
68            final ArrayList<String> entries, final ArrayList<String> entryValues,
69            final ArrayList<String> selectedValues) {
70        final Bundle args = new Bundle(3);
71        args.putStringArrayList(ARG_ENTRIES, entries);
72        args.putStringArrayList(ARG_ENTRY_VALUES, entryValues);
73        args.putStringArrayList(ARG_SELECTED_VALUES, selectedValues);
74        fragment.setArguments(args);
75    }
76
77    @Override
78    public Dialog onCreateDialog(final Bundle savedInstanceState) {
79        final List<String> selectedValuesList =
80                getArguments().getStringArrayList(ARG_SELECTED_VALUES);
81        final Set<String> selectedValues = Sets.newHashSet(selectedValuesList);
82
83        final List<String> entryValues = getArguments().getStringArrayList(ARG_ENTRY_VALUES);
84
85        final LimitedMultiSelectAdapter adapter = new LimitedMultiSelectAdapter(
86                getActivity(), getArguments().getStringArrayList(ARG_ENTRIES), entryValues,
87                getMaxSelectedValues());
88        adapter.setSelected(selectedValues);
89
90        final ListView listView = new ListView(getActivity());
91        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
92        listView.setAdapter(adapter);
93        listView.setOnItemClickListener(new OnItemClickListener() {
94            @Override
95            public void onItemClick(
96                    final AdapterView<?> parent, final View view, final int position,
97                    final long id) {
98                final String entryValue = (String) parent.getItemAtPosition(position);
99
100                if (selectedValues.contains(entryValue)) {
101                    // Remove / uncheck
102                    selectedValues.remove(entryValue);
103                    adapter.removeSelected(entryValue);
104                } else {
105                    // Add / check
106                    selectedValues.add(entryValue);
107                    adapter.addSelected(entryValue);
108                }
109
110                getArguments().putStringArrayList(ARG_SELECTED_VALUES,
111                        new ArrayList<String>(selectedValues));
112
113                adapter.notifyDataSetChanged();
114            }
115        });
116
117        // Set initial check states
118        for (final String value : selectedValues) {
119            for (int j = 0; j < entryValues.size(); j++) {
120                if (entryValues.get(j).equals(value)) {
121                    listView.setItemChecked(j, true);
122                }
123            }
124        }
125
126        return new AlertDialog.Builder(getActivity()).setTitle(getDialogTitle())
127                .setView(listView)
128                .setPositiveButton(R.string.ok, new OnClickListener() {
129                    @Override
130                    public void onClick(final DialogInterface dialog, final int which) {
131                        if (mListener != null) {
132                            final LimitedMultiSelectDialogListener listener = mListener.get();
133                            if (listener != null) {
134                                listener.onSelectionChanged(selectedValues);
135                            }
136                        }
137                    }
138                })
139                .setNegativeButton(R.string.cancel, new OnClickListener() {
140                    @Override
141                    public void onClick(final DialogInterface dialog, final int which) {
142                        dismiss();
143                    }
144                })
145                .create();
146    }
147
148    public void setListener(final LimitedMultiSelectDialogListener listener) {
149        mListener = new WeakReference<LimitedMultiSelectDialogListener>(listener);
150    }
151
152    private static class LimitedMultiSelectAdapter extends BaseAdapter {
153        private final Context mContext;
154
155        private final List<String> mEntries;
156        private final List<String> mEntryValues;
157
158        private final Set<String> mSelectedValues;
159
160        private final int mMaxSelectedValues;
161
162        public LimitedMultiSelectAdapter(final Context context, final List<String> entries,
163                final List<String> entryValues, final int maxSelectedValues) {
164            mContext = context;
165
166            mEntries = ImmutableList.copyOf(entries);
167            mEntryValues = ImmutableList.copyOf(entryValues);
168
169            mSelectedValues = Sets.newHashSetWithExpectedSize(maxSelectedValues);
170
171            mMaxSelectedValues = maxSelectedValues;
172
173            if (mEntries.size() != mEntryValues.size()) {
174                throw new IllegalArgumentException("Each entry must have a corresponding value");
175            }
176        }
177
178        @Override
179        public int getCount() {
180            return mEntries.size();
181        }
182
183        @Override
184        public String getItem(final int position) {
185            return mEntryValues.get(position);
186        }
187
188        @Override
189        public long getItemId(final int position) {
190            return getItem(position).hashCode();
191        }
192
193        @Override
194        public int getItemViewType(final int position) {
195            return 0;
196        }
197
198        @Override
199        public View getView(final int position, final View convertView, final ViewGroup parent) {
200            final CheckedTextView checkedTextView;
201
202            if (convertView == null) {
203                checkedTextView = (CheckedTextView) LayoutInflater.from(mContext)
204                        .inflate(R.layout.select_dialog_multichoice_holo, null);
205            } else {
206                checkedTextView = (CheckedTextView) convertView;
207            }
208
209            checkedTextView.setText(mEntries.get(position));
210            checkedTextView.setEnabled(isEnabled(position));
211
212            return checkedTextView;
213        }
214
215        @Override
216        public int getViewTypeCount() {
217            return 1;
218        }
219
220        @Override
221        public boolean hasStableIds() {
222            return true;
223        }
224
225        @Override
226        public boolean isEmpty() {
227            return mEntries.isEmpty();
228        }
229
230        @Override
231        public boolean areAllItemsEnabled() {
232            // If we have less than the maximum number selected, everything is
233            // enabled
234            if (mMaxSelectedValues > mSelectedValues.size()) {
235                return true;
236            }
237
238            return false;
239        }
240
241        @Override
242        public boolean isEnabled(final int position) {
243            // If we have less than the maximum selected, everything is enabled
244            if (mMaxSelectedValues > mSelectedValues.size()) {
245                return true;
246            }
247
248            // If we have the maximum selected, only the selected rows are
249            // enabled
250            if (mSelectedValues.contains(getItem(position))) {
251                return true;
252            }
253
254            return false;
255        }
256
257        public void setSelected(final Collection<String> selectedValues) {
258            mSelectedValues.clear();
259            mSelectedValues.addAll(selectedValues);
260            notifyDataSetChanged();
261        }
262
263        public void addSelected(final String selectedValue) {
264            mSelectedValues.add(selectedValue);
265            notifyDataSetChanged();
266        }
267
268        public void removeSelected(final String selectedValue) {
269            mSelectedValues.remove(selectedValue);
270            notifyDataSetChanged();
271        }
272    }
273
274    /**
275     * Gets a unique String to be used as a tag for this Fragment.
276     */
277    protected abstract String getFragmentTag();
278
279    /**
280     * Gets the maximum number of values that may be selected.
281     */
282    protected abstract int getMaxSelectedValues();
283
284    /**
285     * Gets the title of the dialog.
286     */
287    protected abstract String getDialogTitle();
288}
289