1package com.android.ex.chips;
2
3import android.content.Context;
4import android.content.res.Resources;
5import android.graphics.Bitmap;
6import android.graphics.BitmapFactory;
7import android.graphics.drawable.StateListDrawable;
8import android.net.Uri;
9import android.support.annotation.DrawableRes;
10import android.support.annotation.IdRes;
11import android.support.annotation.LayoutRes;
12import android.support.annotation.Nullable;
13import android.support.v4.view.MarginLayoutParamsCompat;
14import android.text.SpannableStringBuilder;
15import android.text.Spanned;
16import android.text.TextUtils;
17import android.text.style.ForegroundColorSpan;
18import android.text.util.Rfc822Tokenizer;
19import android.view.LayoutInflater;
20import android.view.View;
21import android.view.ViewGroup;
22import android.view.ViewGroup.MarginLayoutParams;
23import android.widget.ImageView;
24import android.widget.TextView;
25
26import com.android.ex.chips.Queries.Query;
27
28/**
29 * A class that inflates and binds the views in the dropdown list from
30 * RecipientEditTextView.
31 */
32public class DropdownChipLayouter {
33    /**
34     * The type of adapter that is requesting a chip layout.
35     */
36    public enum AdapterType {
37        BASE_RECIPIENT,
38        RECIPIENT_ALTERNATES,
39        SINGLE_RECIPIENT
40    }
41
42    public interface ChipDeleteListener {
43        void onChipDelete();
44    }
45
46    private final LayoutInflater mInflater;
47    private final Context mContext;
48    private ChipDeleteListener mDeleteListener;
49    private Query mQuery;
50    private int mAutocompleteDividerMarginStart;
51
52    public DropdownChipLayouter(LayoutInflater inflater, Context context) {
53        mInflater = inflater;
54        mContext = context;
55        mAutocompleteDividerMarginStart =
56                context.getResources().getDimensionPixelOffset(R.dimen.chip_wrapper_start_padding);
57    }
58
59    public void setQuery(Query query) {
60        mQuery = query;
61    }
62
63    public void setDeleteListener(ChipDeleteListener listener) {
64        mDeleteListener = listener;
65    }
66
67    public void setAutocompleteDividerMarginStart(int autocompleteDividerMarginStart) {
68        mAutocompleteDividerMarginStart = autocompleteDividerMarginStart;
69    }
70
71    /**
72     * Layouts and binds recipient information to the view. If convertView is null, inflates a new
73     * view with getItemLaytout().
74     *
75     * @param convertView The view to bind information to.
76     * @param parent The parent to bind the view to if we inflate a new view.
77     * @param entry The recipient entry to get information from.
78     * @param position The position in the list.
79     * @param type The adapter type that is requesting the bind.
80     * @param constraint The constraint typed in the auto complete view.
81     *
82     * @return A view ready to be shown in the drop down list.
83     */
84    public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
85        AdapterType type, String constraint) {
86        return bindView(convertView, parent, entry, position, type, constraint, null);
87    }
88
89    /**
90     * See {@link #bindView(View, ViewGroup, RecipientEntry, int, AdapterType, String)}
91     * @param deleteDrawable a {@link android.graphics.drawable.StateListDrawable} representing
92     *     the delete icon. android.R.attr.state_activated should map to the delete icon, and the
93     *     default state can map to a drawable of your choice (or null for no drawable).
94     */
95    public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
96            AdapterType type, String constraint, StateListDrawable deleteDrawable) {
97        // Default to show all the information
98        CharSequence[] styledResults =
99                getStyledResults(constraint, entry.getDisplayName(), entry.getDestination());
100        CharSequence displayName = styledResults[0];
101        CharSequence destination = styledResults[1];
102        boolean showImage = true;
103        CharSequence destinationType = getDestinationType(entry);
104
105        final View itemView = reuseOrInflateView(convertView, parent, type);
106
107        final ViewHolder viewHolder = new ViewHolder(itemView);
108
109        // Hide some information depending on the entry type and adapter type
110        switch (type) {
111            case BASE_RECIPIENT:
112                if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) {
113                    displayName = destination;
114
115                    // We only show the destination for secondary entries, so clear it only for the
116                    // first level.
117                    if (entry.isFirstLevel()) {
118                        destination = null;
119                    }
120                }
121
122                if (!entry.isFirstLevel()) {
123                    displayName = null;
124                    showImage = false;
125                }
126
127                // For BASE_RECIPIENT set all top dividers except for the first one to be GONE.
128                if (viewHolder.topDivider != null) {
129                    viewHolder.topDivider.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
130                    MarginLayoutParamsCompat.setMarginStart(
131                            (MarginLayoutParams) viewHolder.topDivider.getLayoutParams(),
132                            mAutocompleteDividerMarginStart);
133                }
134                if (viewHolder.bottomDivider != null) {
135                    MarginLayoutParamsCompat.setMarginStart(
136                            (MarginLayoutParams) viewHolder.bottomDivider.getLayoutParams(),
137                            mAutocompleteDividerMarginStart);
138                }
139                break;
140            case RECIPIENT_ALTERNATES:
141                if (position != 0) {
142                    displayName = null;
143                    showImage = false;
144                }
145                break;
146            case SINGLE_RECIPIENT:
147                destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress();
148                destinationType = null;
149        }
150
151        // Bind the information to the view
152        bindTextToView(displayName, viewHolder.displayNameView);
153        bindTextToView(destination, viewHolder.destinationView);
154        bindTextToView(destinationType, viewHolder.destinationTypeView);
155        bindIconToView(showImage, entry, viewHolder.imageView, type);
156        bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView);
157
158        return itemView;
159    }
160
161    /**
162     * Returns a new view with {@link #getItemLayoutResId(AdapterType)}.
163     */
164    public View newView(AdapterType type) {
165        return mInflater.inflate(getItemLayoutResId(type), null);
166    }
167
168    /**
169     * Returns the same view, or inflates a new one if the given view was null.
170     */
171    protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) {
172        int itemLayout = getItemLayoutResId(type);
173        switch (type) {
174            case BASE_RECIPIENT:
175            case RECIPIENT_ALTERNATES:
176                break;
177            case SINGLE_RECIPIENT:
178                itemLayout = getAlternateItemLayoutResId(type);
179                break;
180        }
181        return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false);
182    }
183
184    /**
185     * Binds the text to the given text view. If the text was null, hides the text view.
186     */
187    protected void bindTextToView(CharSequence text, TextView view) {
188        if (view == null) {
189            return;
190        }
191
192        if (text != null) {
193            view.setText(text);
194            view.setVisibility(View.VISIBLE);
195        } else {
196            view.setVisibility(View.GONE);
197        }
198    }
199
200    /**
201     * Binds the avatar icon to the image view. If we don't want to show the image, hides the
202     * image view.
203     */
204    protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view,
205        AdapterType type) {
206        if (view == null) {
207            return;
208        }
209
210        if (showImage) {
211            switch (type) {
212                case BASE_RECIPIENT:
213                    byte[] photoBytes = entry.getPhotoBytes();
214                    if (photoBytes != null && photoBytes.length > 0) {
215                        final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
216                            photoBytes.length);
217                        view.setImageBitmap(photo);
218                    } else {
219                        view.setImageResource(getDefaultPhotoResId());
220                    }
221                    break;
222                case RECIPIENT_ALTERNATES:
223                    Uri thumbnailUri = entry.getPhotoThumbnailUri();
224                    if (thumbnailUri != null) {
225                        // TODO: see if this needs to be done outside the main thread
226                        // as it may be too slow to get immediately.
227                        view.setImageURI(thumbnailUri);
228                    } else {
229                        view.setImageResource(getDefaultPhotoResId());
230                    }
231                    break;
232                case SINGLE_RECIPIENT:
233                default:
234                    break;
235            }
236            view.setVisibility(View.VISIBLE);
237        } else {
238            view.setVisibility(View.GONE);
239        }
240    }
241
242    protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient,
243            ImageView view) {
244        if (view == null) {
245            return;
246        }
247        if (drawable == null) {
248            view.setVisibility(View.GONE);
249        } else {
250            final Resources res = mContext.getResources();
251            view.setImageDrawable(drawable);
252            view.setContentDescription(
253                    res.getString(R.string.dropdown_delete_button_desc, recipient));
254            if (mDeleteListener != null) {
255                view.setOnClickListener(new View.OnClickListener() {
256                    @Override
257                    public void onClick(View view) {
258                        if (drawable.getCurrent() != null) {
259                            mDeleteListener.onChipDelete();
260                        }
261                    }
262                });
263            }
264        }
265    }
266
267    protected CharSequence getDestinationType(RecipientEntry entry) {
268        return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(),
269            entry.getDestinationLabel()).toString().toUpperCase();
270    }
271
272    /**
273     * Returns a layout id for each item inside auto-complete list.
274     *
275     * Each View must contain two TextViews (for display name and destination) and one ImageView
276     * (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
277     * {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
278     */
279    protected @LayoutRes int getItemLayoutResId(AdapterType type) {
280        switch (type) {
281            case BASE_RECIPIENT:
282                return R.layout.chips_autocomplete_recipient_dropdown_item;
283            case RECIPIENT_ALTERNATES:
284                return R.layout.chips_recipient_dropdown_item;
285            default:
286                return R.layout.chips_recipient_dropdown_item;
287        }
288    }
289
290    /**
291     * Returns a layout id for each item inside alternate auto-complete list.
292     *
293     * Each View must contain two TextViews (for display name and destination) and one ImageView
294     * (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
295     * {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
296     */
297    protected @LayoutRes int getAlternateItemLayoutResId(AdapterType type) {
298        switch (type) {
299            case BASE_RECIPIENT:
300                return R.layout.chips_autocomplete_recipient_dropdown_item;
301            case RECIPIENT_ALTERNATES:
302                return R.layout.chips_recipient_dropdown_item;
303            default:
304                return R.layout.chips_recipient_dropdown_item;
305        }
306    }
307
308    /**
309     * Returns a resource ID representing an image which should be shown when ther's no relevant
310     * photo is available.
311     */
312    protected @DrawableRes int getDefaultPhotoResId() {
313        return R.drawable.ic_contact_picture;
314    }
315
316    /**
317     * Returns an id for TextView in an item View for showing a display name. By default
318     * {@link android.R.id#title} is returned.
319     */
320    protected @IdRes int getDisplayNameResId() {
321        return android.R.id.title;
322    }
323
324    /**
325     * Returns an id for TextView in an item View for showing a destination
326     * (an email address or a phone number).
327     * By default {@link android.R.id#text1} is returned.
328     */
329    protected @IdRes int getDestinationResId() {
330        return android.R.id.text1;
331    }
332
333    /**
334     * Returns an id for TextView in an item View for showing the type of the destination.
335     * By default {@link android.R.id#text2} is returned.
336     */
337    protected @IdRes int getDestinationTypeResId() {
338        return android.R.id.text2;
339    }
340
341    /**
342     * Returns an id for ImageView in an item View for showing photo image for a person. In default
343     * {@link android.R.id#icon} is returned.
344     */
345    protected @IdRes int getPhotoResId() {
346        return android.R.id.icon;
347    }
348
349    /**
350     * Returns an id for ImageView in an item View for showing the delete button. In default
351     * {@link android.R.id#icon1} is returned.
352     */
353    protected @IdRes int getDeleteResId() { return android.R.id.icon1; }
354
355    /**
356     * Given a constraint and results, tries to find the constraint in those results, one at a time.
357     * A foreground font color style will be applied to the section that matches the constraint. As
358     * soon as a match has been found, no further matches are attempted.
359     *
360     * @param constraint A string that we will attempt to find within the results.
361     * @param results Strings that may contain the constraint. The order given is the order used to
362     *     search for the constraint.
363     *
364     * @return An array of CharSequences, the length determined by the length of results. Each
365     *     CharSequence will either be a styled SpannableString or just the input String.
366     */
367    protected CharSequence[] getStyledResults(@Nullable String constraint, String... results) {
368        if (isAllWhitespace(constraint)) {
369            return results;
370        }
371
372        CharSequence[] styledResults = new CharSequence[results.length];
373        boolean foundMatch = false;
374        for (int i = 0; i < results.length; i++) {
375            String result = results[i];
376            if (result == null) {
377                continue;
378            }
379
380            if (!foundMatch) {
381                int index = result.toLowerCase().indexOf(constraint.toLowerCase());
382                if (index != -1) {
383                    SpannableStringBuilder styled = SpannableStringBuilder.valueOf(result);
384                    ForegroundColorSpan highlightSpan =
385                            new ForegroundColorSpan(mContext.getResources().getColor(
386                                    R.color.chips_dropdown_text_highlighted));
387                    styled.setSpan(highlightSpan,
388                            index, index + constraint.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
389                    styledResults[i] = styled;
390                    foundMatch = true;
391                    continue;
392                }
393            }
394            styledResults[i] = result;
395        }
396        return styledResults;
397    }
398
399    private static boolean isAllWhitespace(@Nullable String string) {
400        if (TextUtils.isEmpty(string)) {
401            return true;
402        }
403
404        for (int i = 0; i < string.length(); ++i) {
405            if (!Character.isWhitespace(string.charAt(i))) {
406                return false;
407            }
408        }
409
410        return true;
411    }
412
413    /**
414     * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the
415     * corresponding views.
416     */
417    protected class ViewHolder {
418        public final TextView displayNameView;
419        public final TextView destinationView;
420        public final TextView destinationTypeView;
421        public final ImageView imageView;
422        public final ImageView deleteView;
423        public final View topDivider;
424        public final View bottomDivider;
425
426        public ViewHolder(View view) {
427            displayNameView = (TextView) view.findViewById(getDisplayNameResId());
428            destinationView = (TextView) view.findViewById(getDestinationResId());
429            destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId());
430            imageView = (ImageView) view.findViewById(getPhotoResId());
431            deleteView = (ImageView) view.findViewById(getDeleteResId());
432            topDivider = view.findViewById(R.id.chip_autocomplete_top_divider);
433            bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider);
434        }
435    }
436}
437