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