1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.contacts.quickcontact;
17
18import android.animation.Animator;
19import android.animation.Animator.AnimatorListener;
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.app.Activity;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.Resources;
26import android.graphics.ColorFilter;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.os.Bundle;
30import android.support.v7.widget.CardView;
31import android.text.Spannable;
32import android.text.TextUtils;
33import android.transition.ChangeBounds;
34import android.transition.Fade;
35import android.transition.Transition;
36import android.transition.Transition.TransitionListener;
37import android.transition.TransitionManager;
38import android.transition.TransitionSet;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.util.Property;
42import android.view.ContextMenu.ContextMenuInfo;
43import android.view.LayoutInflater;
44import android.view.MotionEvent;
45import android.view.View;
46import android.view.ViewConfiguration;
47import android.view.ViewGroup;
48import android.widget.ImageView;
49import android.widget.LinearLayout;
50import android.widget.LinearLayout.LayoutParams;
51import android.widget.RelativeLayout;
52import android.widget.TextView;
53
54import com.android.contacts.R;
55import com.android.contacts.common.dialog.CallSubjectDialog;
56
57import java.util.ArrayList;
58import java.util.List;
59
60/**
61 * Display entries in a LinearLayout that can be expanded to show all entries.
62 */
63public class ExpandingEntryCardView extends CardView {
64
65    private static final String TAG = "ExpandingEntryCardView";
66    private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200;
67    private static final int DURATION_COLLAPSE_ANIMATION_FADE_OUT = 75;
68    private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100;
69
70    public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300;
71    public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300;
72
73    private static final Property<View, Integer> VIEW_LAYOUT_HEIGHT_PROPERTY =
74            new Property<View, Integer>(Integer.class, "height") {
75                @Override
76                public void set(View view, Integer height) {
77                    LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
78                            view.getLayoutParams();
79                    params.height = height;
80                    view.setLayoutParams(params);
81                }
82
83                @Override
84                public Integer get(View view) {
85                    return view.getLayoutParams().height;
86                }
87            };
88
89    /**
90     * Entry data.
91     */
92    public static final class Entry {
93        // No action when clicking a button is specified.
94        public static final int ACTION_NONE = 1;
95        // Button action is an intent.
96        public static final int ACTION_INTENT = 2;
97        // Button action will open the call with subject dialog.
98        public static final int ACTION_CALL_WITH_SUBJECT = 3;
99
100        private final int mId;
101        private final Drawable mIcon;
102        private final String mHeader;
103        private final String mSubHeader;
104        private final Drawable mSubHeaderIcon;
105        private final String mText;
106        private final Drawable mTextIcon;
107        private Spannable mPrimaryContentDescription;
108        private final Intent mIntent;
109        private final Drawable mAlternateIcon;
110        private final Intent mAlternateIntent;
111        private Spannable mAlternateContentDescription;
112        private final boolean mShouldApplyColor;
113        private final boolean mIsEditable;
114        private final EntryContextMenuInfo mEntryContextMenuInfo;
115        private final Drawable mThirdIcon;
116        private final Intent mThirdIntent;
117        private final String mThirdContentDescription;
118        private final int mIconResourceId;
119        private final int mThirdAction;
120        private final Bundle mThirdExtras;
121
122        public Entry(int id, Drawable mainIcon, String header, String subHeader,
123                Drawable subHeaderIcon, String text, Drawable textIcon,
124                Spannable primaryContentDescription, Intent intent,
125                Drawable alternateIcon, Intent alternateIntent,
126                Spannable alternateContentDescription, boolean shouldApplyColor, boolean isEditable,
127                EntryContextMenuInfo entryContextMenuInfo, Drawable thirdIcon, Intent thirdIntent,
128                String thirdContentDescription, int thirdAction, Bundle thirdExtras,
129                int iconResourceId) {
130            mId = id;
131            mIcon = mainIcon;
132            mHeader = header;
133            mSubHeader = subHeader;
134            mSubHeaderIcon = subHeaderIcon;
135            mText = text;
136            mTextIcon = textIcon;
137            mPrimaryContentDescription = primaryContentDescription;
138            mIntent = intent;
139            mAlternateIcon = alternateIcon;
140            mAlternateIntent = alternateIntent;
141            mAlternateContentDescription = alternateContentDescription;
142            mShouldApplyColor = shouldApplyColor;
143            mIsEditable = isEditable;
144            mEntryContextMenuInfo = entryContextMenuInfo;
145            mThirdIcon = thirdIcon;
146            mThirdIntent = thirdIntent;
147            mThirdContentDescription = thirdContentDescription;
148            mThirdAction = thirdAction;
149            mThirdExtras = thirdExtras;
150            mIconResourceId = iconResourceId;
151        }
152
153        Drawable getIcon() {
154            return mIcon;
155        }
156
157        String getHeader() {
158            return mHeader;
159        }
160
161        String getSubHeader() {
162            return mSubHeader;
163        }
164
165        Drawable getSubHeaderIcon() {
166            return mSubHeaderIcon;
167        }
168
169        public String getText() {
170            return mText;
171        }
172
173        Drawable getTextIcon() {
174            return mTextIcon;
175        }
176
177        Spannable getPrimaryContentDescription() {
178            return mPrimaryContentDescription;
179        }
180
181        Intent getIntent() {
182            return mIntent;
183        }
184
185        Drawable getAlternateIcon() {
186            return mAlternateIcon;
187        }
188
189        Intent getAlternateIntent() {
190            return mAlternateIntent;
191        }
192
193        Spannable getAlternateContentDescription() {
194            return mAlternateContentDescription;
195        }
196
197        boolean shouldApplyColor() {
198            return mShouldApplyColor;
199        }
200
201        boolean isEditable() {
202            return mIsEditable;
203        }
204
205        int getId() {
206            return mId;
207        }
208
209        EntryContextMenuInfo getEntryContextMenuInfo() {
210            return mEntryContextMenuInfo;
211        }
212
213        Drawable getThirdIcon() {
214            return mThirdIcon;
215        }
216
217        Intent getThirdIntent() {
218            return mThirdIntent;
219        }
220
221        String getThirdContentDescription() {
222            return mThirdContentDescription;
223        }
224
225        int getIconResourceId() {
226            return mIconResourceId;
227        }
228
229        public int getThirdAction() {
230            return mThirdAction;
231        }
232
233        public Bundle getThirdExtras() {
234            return mThirdExtras;
235        }
236    }
237
238    public interface ExpandingEntryCardViewListener {
239        void onCollapse(int heightDelta);
240        void onExpand();
241        void onExpandDone();
242    }
243
244    private View mExpandCollapseButton;
245    private TextView mExpandCollapseTextView;
246    private TextView mTitleTextView;
247    private CharSequence mExpandButtonText;
248    private CharSequence mCollapseButtonText;
249    private OnClickListener mOnClickListener;
250    private OnCreateContextMenuListener mOnCreateContextMenuListener;
251    private boolean mIsExpanded = false;
252    /**
253     * The max number of entries to show in a collapsed card. If there are less entries passed in,
254     * then they are all shown.
255     */
256    private int mCollapsedEntriesCount;
257    private ExpandingEntryCardViewListener mListener;
258    private List<List<Entry>> mEntries;
259    private int mNumEntries = 0;
260    private boolean mAllEntriesInflated = false;
261    private List<List<View>> mEntryViews;
262    private LinearLayout mEntriesViewGroup;
263    private final ImageView mExpandCollapseArrow;
264    private int mThemeColor;
265    private ColorFilter mThemeColorFilter;
266    /**
267     * Whether to prioritize the first entry type. If prioritized, we should show at least two
268     * of this entry type.
269     */
270    private boolean mShowFirstEntryTypeTwice;
271    private boolean mIsAlwaysExpanded;
272    /** The ViewGroup to run the expand/collapse animation on */
273    private ViewGroup mAnimationViewGroup;
274    private LinearLayout mBadgeContainer;
275    private final List<ImageView> mBadges;
276    private final List<Integer> mBadgeIds;
277    private final int mDividerLineHeightPixels;
278    /**
279     * List to hold the separators. This saves us from reconstructing every expand/collapse and
280     * provides a smoother animation.
281     */
282    private List<View> mSeparators;
283    private LinearLayout mContainer;
284
285    private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
286        @Override
287        public void onClick(View v) {
288            if (mIsExpanded) {
289                collapse();
290            } else {
291                expand();
292            }
293        }
294    };
295
296    public ExpandingEntryCardView(Context context) {
297        this(context, null);
298    }
299
300    public ExpandingEntryCardView(Context context, AttributeSet attrs) {
301        super(context, attrs);
302        LayoutInflater inflater = LayoutInflater.from(context);
303        View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
304        mEntriesViewGroup = (LinearLayout)
305                expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
306        mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
307        mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container);
308
309        mExpandCollapseButton = inflater.inflate(
310                R.layout.quickcontact_expanding_entry_card_button, this, false);
311        mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text);
312        mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow);
313        mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener);
314        mBadgeContainer = (LinearLayout) mExpandCollapseButton.findViewById(R.id.badge_container);
315        mDividerLineHeightPixels = getResources()
316                .getDimensionPixelSize(R.dimen.divider_line_height);
317
318        mBadges = new ArrayList<ImageView>();
319        mBadgeIds = new ArrayList<Integer>();
320    }
321
322    public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
323            boolean isExpanded, boolean isAlwaysExpanded, ExpandingEntryCardViewListener listener,
324            ViewGroup animationViewGroup) {
325        initialize(entries, numInitialVisibleEntries, isExpanded, isAlwaysExpanded,
326                listener, animationViewGroup, /* showFirstEntryTypeTwice = */ false);
327    }
328
329    /**
330     * Sets the Entry list to display.
331     *
332     * @param entries The Entry list to display.
333     */
334    public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
335            boolean isExpanded, boolean isAlwaysExpanded,
336            ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup,
337            boolean showFirstEntryTypeTwice) {
338        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
339        mIsExpanded = isExpanded;
340        mIsAlwaysExpanded = isAlwaysExpanded;
341        // If isAlwaysExpanded is true, mIsExpanded should be true
342        mIsExpanded |= mIsAlwaysExpanded;
343        mEntryViews = new ArrayList<List<View>>(entries.size());
344        mEntries = entries;
345        mNumEntries = 0;
346        mAllEntriesInflated = false;
347        mShowFirstEntryTypeTwice = showFirstEntryTypeTwice;
348        for (List<Entry> entryList : mEntries) {
349            mNumEntries += entryList.size();
350            mEntryViews.add(new ArrayList<View>());
351        }
352        mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries);
353        // We need a separator between each list, but not after the last one
354        if (entries.size() > 1) {
355            mSeparators = new ArrayList<>(entries.size() - 1);
356        }
357        mListener = listener;
358        mAnimationViewGroup = animationViewGroup;
359
360        if (mIsExpanded) {
361            updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0);
362            inflateAllEntries(layoutInflater);
363        } else {
364            updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0);
365            inflateInitialEntries(layoutInflater);
366        }
367        insertEntriesIntoViewGroup();
368        applyColor();
369    }
370
371    /**
372     * Sets the text for the expand button.
373     *
374     * @param expandButtonText The expand button text.
375     */
376    public void setExpandButtonText(CharSequence expandButtonText) {
377        mExpandButtonText = expandButtonText;
378        if (mExpandCollapseTextView != null && !mIsExpanded) {
379            mExpandCollapseTextView.setText(expandButtonText);
380        }
381    }
382
383    /**
384     * Sets the text for the expand button.
385     *
386     * @param expandButtonText The expand button text.
387     */
388    public void setCollapseButtonText(CharSequence expandButtonText) {
389        mCollapseButtonText = expandButtonText;
390        if (mExpandCollapseTextView != null && mIsExpanded) {
391            mExpandCollapseTextView.setText(mCollapseButtonText);
392        }
393    }
394
395    @Override
396    public void setOnClickListener(OnClickListener listener) {
397        mOnClickListener = listener;
398    }
399
400    @Override
401    public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) {
402        mOnCreateContextMenuListener = listener;
403    }
404
405    private List<View> calculateEntriesToRemoveDuringCollapse() {
406        final List<View> viewsToRemove = getViewsToDisplay(true);
407        final List<View> viewsCollapsed = getViewsToDisplay(false);
408        viewsToRemove.removeAll(viewsCollapsed);
409        return viewsToRemove;
410    }
411
412    private void insertEntriesIntoViewGroup() {
413        mEntriesViewGroup.removeAllViews();
414
415        for (View view : getViewsToDisplay(mIsExpanded)) {
416            mEntriesViewGroup.addView(view);
417        }
418
419        removeView(mExpandCollapseButton);
420        if (mCollapsedEntriesCount < mNumEntries
421                && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) {
422            mContainer.addView(mExpandCollapseButton, -1);
423        }
424    }
425
426    /**
427     * Returns the list of views that should be displayed. This changes depending on whether
428     * the card is expanded or collapsed.
429     */
430    private List<View> getViewsToDisplay(boolean isExpanded) {
431        final List<View> viewsToDisplay = new ArrayList<View>();
432        if (isExpanded) {
433            for (int i = 0; i < mEntryViews.size(); i++) {
434                List<View> viewList = mEntryViews.get(i);
435                if (i > 0) {
436                    View separator;
437                    if (mSeparators.size() <= i - 1) {
438                        separator = generateSeparator(viewList.get(0));
439                        mSeparators.add(separator);
440                    } else {
441                        separator = mSeparators.get(i - 1);
442                    }
443                    viewsToDisplay.add(separator);
444                }
445                for (View view : viewList) {
446                    viewsToDisplay.add(view);
447                }
448            }
449        } else {
450            // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the
451            // number of entries that need to be added that are not the head element of a list
452            // to reach mCollapsedEntriesCount.
453            int numInViewGroup = 0;
454            int extraEntries = mCollapsedEntriesCount - mEntryViews.size();
455            for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount;
456                    i++) {
457                List<View> entryViewList = mEntryViews.get(i);
458                if (i > 0) {
459                    View separator;
460                    if (mSeparators.size() <= i - 1) {
461                        separator = generateSeparator(entryViewList.get(0));
462                        mSeparators.add(separator);
463                    } else {
464                        separator = mSeparators.get(i - 1);
465                    }
466                    viewsToDisplay.add(separator);
467                }
468                viewsToDisplay.add(entryViewList.get(0));
469                numInViewGroup++;
470
471                int indexInEntryViewList = 1;
472                if (mShowFirstEntryTypeTwice && i == 0 && entryViewList.size() > 1) {
473                    viewsToDisplay.add(entryViewList.get(1));
474                    numInViewGroup++;
475                    extraEntries--;
476                    indexInEntryViewList++;
477                }
478
479                // Insert entries in this list to hit mCollapsedEntriesCount.
480                for (int j = indexInEntryViewList;
481                        j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount &&
482                        extraEntries > 0;
483                        j++) {
484                    viewsToDisplay.add(entryViewList.get(j));
485                    numInViewGroup++;
486                    extraEntries--;
487                }
488            }
489        }
490
491        formatEntryIfFirst(viewsToDisplay);
492        return viewsToDisplay;
493    }
494
495    private void formatEntryIfFirst(List<View> entriesViewGroup) {
496        // If no title and the first entry in the group, add extra padding
497        if (TextUtils.isEmpty(mTitleTextView.getText()) &&
498                entriesViewGroup.size() > 0) {
499            final View entry = entriesViewGroup.get(0);
500            entry.setPadding(entry.getPaddingLeft(),
501                    getResources().getDimensionPixelSize(
502                            R.dimen.expanding_entry_card_item_padding_top) +
503                    getResources().getDimensionPixelSize(
504                            R.dimen.expanding_entry_card_null_title_top_extra_padding),
505                    entry.getPaddingRight(),
506                    entry.getPaddingBottom());
507        }
508    }
509
510    private View generateSeparator(View entry) {
511        View separator = new View(getContext());
512        Resources res = getResources();
513
514        separator.setBackgroundColor(res.getColor(
515                R.color.divider_line_color_light));
516        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
517                ViewGroup.LayoutParams.MATCH_PARENT, mDividerLineHeightPixels);
518        // The separator is aligned with the text in the entry. This is offset by a default
519        // margin. If there is an icon present, the icon's width and margin are added
520        int marginStart = res.getDimensionPixelSize(
521                R.dimen.expanding_entry_card_item_padding_start);
522        ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon);
523        if (entryIcon.getVisibility() == View.VISIBLE) {
524            int imageWidthAndMargin =
525                    res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) +
526                    res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing);
527            marginStart += imageWidthAndMargin;
528        }
529        layoutParams.setMarginStart(marginStart);
530        separator.setLayoutParams(layoutParams);
531        return separator;
532    }
533
534    private CharSequence getExpandButtonText() {
535        if (!TextUtils.isEmpty(mExpandButtonText)) {
536            return mExpandButtonText;
537        } else {
538            // Default to "See more".
539            return getResources().getText(R.string.expanding_entry_card_view_see_more);
540        }
541    }
542
543    private CharSequence getCollapseButtonText() {
544        if (!TextUtils.isEmpty(mCollapseButtonText)) {
545            return mCollapseButtonText;
546        } else {
547            // Default to "See less".
548            return getResources().getText(R.string.expanding_entry_card_view_see_less);
549        }
550    }
551
552    /**
553     * Inflates the initial entries to be shown.
554     */
555    private void inflateInitialEntries(LayoutInflater layoutInflater) {
556        // If the number of collapsed entries equals total entries, inflate all
557        if (mCollapsedEntriesCount == mNumEntries) {
558            inflateAllEntries(layoutInflater);
559        } else {
560            // Otherwise inflate the top entry from each list
561            // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached.
562            int numInflated = 0;
563            int extraEntries = mCollapsedEntriesCount - mEntries.size();
564            for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) {
565                List<Entry> entryList = mEntries.get(i);
566                List<View> entryViewList = mEntryViews.get(i);
567
568                entryViewList.add(createEntryView(layoutInflater, entryList.get(0),
569                        /* showIcon = */ View.VISIBLE));
570                numInflated++;
571
572                int indexInEntryViewList = 1;
573                if (mShowFirstEntryTypeTwice && i == 0 && entryList.size() > 1) {
574                    entryViewList.add(createEntryView(layoutInflater, entryList.get(1),
575                        /* showIcon = */ View.INVISIBLE));
576                    numInflated++;
577                    extraEntries--;
578                    indexInEntryViewList++;
579                }
580
581                // Inflate entries in this list to hit mCollapsedEntriesCount.
582                for (int j = indexInEntryViewList; j < entryList.size()
583                        && numInflated < mCollapsedEntriesCount
584                        && extraEntries > 0; j++) {
585                    entryViewList.add(createEntryView(layoutInflater, entryList.get(j),
586                            /* showIcon = */ View.INVISIBLE));
587                    numInflated++;
588                    extraEntries--;
589                }
590            }
591        }
592    }
593
594    /**
595     * Inflates all entries.
596     */
597    private void inflateAllEntries(LayoutInflater layoutInflater) {
598        if (mAllEntriesInflated) {
599            return;
600        }
601        for (int i = 0; i < mEntries.size(); i++) {
602            List<Entry> entryList = mEntries.get(i);
603            List<View> viewList = mEntryViews.get(i);
604            for (int j = viewList.size(); j < entryList.size(); j++) {
605                final int iconVisibility;
606                final Entry entry = entryList.get(j);
607                // If the entry does not have an icon, mark gone. Else if it has an icon, show
608                // for the first Entry in the list only
609                if (entry.getIcon() == null) {
610                    iconVisibility = View.GONE;
611                } else if (j == 0) {
612                    iconVisibility = View.VISIBLE;
613                } else {
614                    iconVisibility = View.INVISIBLE;
615                }
616                viewList.add(createEntryView(layoutInflater, entry, iconVisibility));
617            }
618        }
619        mAllEntriesInflated = true;
620    }
621
622    public void setColorAndFilter(int color, ColorFilter colorFilter) {
623        mThemeColor = color;
624        mThemeColorFilter = colorFilter;
625        applyColor();
626    }
627
628    public void setEntryHeaderColor(int color) {
629        if (mEntries != null) {
630            for (List<View> entryList : mEntryViews) {
631                for (View entryView : entryList) {
632                    TextView header = (TextView) entryView.findViewById(R.id.header);
633                    if (header != null) {
634                        header.setTextColor(color);
635                    }
636                }
637            }
638        }
639    }
640
641    /**
642     * The ColorFilter is passed in along with the color so that a new one only needs to be created
643     * once for the entire activity.
644     * 1. Title
645     * 2. Entry icons
646     * 3. Expand/Collapse Text
647     * 4. Expand/Collapse Button
648     */
649    public void applyColor() {
650        if (mThemeColor != 0 && mThemeColorFilter != null) {
651            // Title
652            if (mTitleTextView != null) {
653                mTitleTextView.setTextColor(mThemeColor);
654            }
655
656            // Entry icons
657            if (mEntries != null) {
658                for (List<Entry> entryList : mEntries) {
659                    for (Entry entry : entryList) {
660                        if (entry.shouldApplyColor()) {
661                            Drawable icon = entry.getIcon();
662                            if (icon != null) {
663                                icon.mutate();
664                                icon.setColorFilter(mThemeColorFilter);
665                            }
666                        }
667                        Drawable alternateIcon = entry.getAlternateIcon();
668                        if (alternateIcon != null) {
669                            alternateIcon.mutate();
670                            alternateIcon.setColorFilter(mThemeColorFilter);
671                        }
672                        Drawable thirdIcon = entry.getThirdIcon();
673                        if (thirdIcon != null) {
674                            thirdIcon.mutate();
675                            thirdIcon.setColorFilter(mThemeColorFilter);
676                        }
677                    }
678                }
679            }
680
681            // Expand/Collapse
682            mExpandCollapseTextView.setTextColor(mThemeColor);
683            mExpandCollapseArrow.setColorFilter(mThemeColorFilter);
684        }
685    }
686
687    private View createEntryView(LayoutInflater layoutInflater, final Entry entry,
688            int iconVisibility) {
689        final EntryView view = (EntryView) layoutInflater.inflate(
690                R.layout.expanding_entry_card_item, this, false);
691
692        view.setContextMenuInfo(entry.getEntryContextMenuInfo());
693        if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) {
694            view.setContentDescription(entry.getPrimaryContentDescription());
695        }
696
697        final ImageView icon = (ImageView) view.findViewById(R.id.icon);
698        icon.setVisibility(iconVisibility);
699        if (entry.getIcon() != null) {
700            icon.setImageDrawable(entry.getIcon());
701        }
702        final TextView header = (TextView) view.findViewById(R.id.header);
703        if (!TextUtils.isEmpty(entry.getHeader())) {
704            header.setText(entry.getHeader());
705        } else {
706            header.setVisibility(View.GONE);
707        }
708
709        final TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
710        if (!TextUtils.isEmpty(entry.getSubHeader())) {
711            subHeader.setText(entry.getSubHeader());
712        } else {
713            subHeader.setVisibility(View.GONE);
714        }
715
716        final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
717        if (entry.getSubHeaderIcon() != null) {
718            subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
719        } else {
720            subHeaderIcon.setVisibility(View.GONE);
721        }
722
723        final TextView text = (TextView) view.findViewById(R.id.text);
724        if (!TextUtils.isEmpty(entry.getText())) {
725            text.setText(entry.getText());
726        } else {
727            text.setVisibility(View.GONE);
728        }
729
730        final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
731        if (entry.getTextIcon() != null) {
732            textIcon.setImageDrawable(entry.getTextIcon());
733        } else {
734            textIcon.setVisibility(View.GONE);
735        }
736
737        if (entry.getIntent() != null) {
738            view.setOnClickListener(mOnClickListener);
739            view.setTag(new EntryTag(entry.getId(), entry.getIntent()));
740        }
741
742        if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) {
743            // Remove the click effect
744            view.setBackground(null);
745        }
746
747        // If only the header is visible, add a top margin to match icon's top margin.
748        // Also increase the space below the header for visual comfort.
749        if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE &&
750                text.getVisibility() == View.GONE) {
751            RelativeLayout.LayoutParams headerLayoutParams =
752                    (RelativeLayout.LayoutParams) header.getLayoutParams();
753            headerLayoutParams.topMargin = (int) (getResources().getDimension(
754                    R.dimen.expanding_entry_card_item_header_only_margin_top));
755            headerLayoutParams.bottomMargin += (int) (getResources().getDimension(
756                    R.dimen.expanding_entry_card_item_header_only_margin_bottom));
757            header.setLayoutParams(headerLayoutParams);
758        }
759
760        // Adjust the top padding size for entries with an invisible icon. The padding depends on
761        // if there is a sub header or text section
762        if (iconVisibility == View.INVISIBLE &&
763                (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) {
764            view.setPaddingRelative(view.getPaddingStart(),
765                    getResources().getDimensionPixelSize(
766                            R.dimen.expanding_entry_card_item_no_icon_margin_top),
767                    view.getPaddingEnd(),
768                    view.getPaddingBottom());
769        } else if (iconVisibility == View.INVISIBLE &&  TextUtils.isEmpty(entry.getSubHeader())
770                && TextUtils.isEmpty(entry.getText())) {
771            view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(),
772                    view.getPaddingBottom());
773        }
774
775        final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate);
776        final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon);
777
778        if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) {
779            alternateIcon.setImageDrawable(entry.getAlternateIcon());
780            alternateIcon.setOnClickListener(mOnClickListener);
781            alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent()));
782            alternateIcon.setVisibility(View.VISIBLE);
783            alternateIcon.setContentDescription(entry.getAlternateContentDescription());
784        }
785
786        if (entry.getThirdIcon() != null && entry.getThirdAction() != Entry.ACTION_NONE) {
787            thirdIcon.setImageDrawable(entry.getThirdIcon());
788            if (entry.getThirdAction() == Entry.ACTION_INTENT) {
789                thirdIcon.setOnClickListener(mOnClickListener);
790                thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent()));
791            } else if (entry.getThirdAction() == Entry.ACTION_CALL_WITH_SUBJECT) {
792                thirdIcon.setOnClickListener(new View.OnClickListener() {
793                    @Override
794                    public void onClick(View v) {
795                        Object tag = v.getTag();
796                        if (!(tag instanceof Bundle)) {
797                            return;
798                        }
799
800                        Context context = getContext();
801                        if (context instanceof Activity) {
802                            CallSubjectDialog.start((Activity) context, entry.getThirdExtras());
803                        }
804                    }
805                });
806                thirdIcon.setTag(entry.getThirdExtras());
807            }
808            thirdIcon.setVisibility(View.VISIBLE);
809            thirdIcon.setContentDescription(entry.getThirdContentDescription());
810        }
811
812        // Set a custom touch listener for expanding the extra icon touch areas
813        view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon));
814        view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
815
816        return view;
817    }
818
819    private void updateExpandCollapseButton(CharSequence buttonText, long duration) {
820        if (mIsExpanded) {
821            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
822                    "rotation", 180);
823            animator.setDuration(duration);
824            animator.start();
825        } else {
826            final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
827                    "rotation", 0);
828            animator.setDuration(duration);
829            animator.start();
830        }
831        updateBadges();
832
833        mExpandCollapseTextView.setText(buttonText);
834    }
835
836    private void updateBadges() {
837        if (mIsExpanded) {
838            mBadgeContainer.removeAllViews();
839        } else {
840            int numberOfMimeTypesShown = mCollapsedEntriesCount;
841            if (mShowFirstEntryTypeTwice && mEntries.size() > 0
842                    && mEntries.get(0).size() > 1) {
843                numberOfMimeTypesShown--;
844            }
845            // Inflate badges if not yet created
846            if (mBadges.size() < mEntries.size() - numberOfMimeTypesShown) {
847                for (int i = numberOfMimeTypesShown; i < mEntries.size(); i++) {
848                    Drawable badgeDrawable = mEntries.get(i).get(0).getIcon();
849                    int badgeResourceId = mEntries.get(i).get(0).getIconResourceId();
850                    // Do not add the same badge twice
851                    if (badgeResourceId != 0 && mBadgeIds.contains(badgeResourceId)) {
852                        continue;
853                    }
854                    if (badgeDrawable != null) {
855                        ImageView badgeView = new ImageView(getContext());
856                        LinearLayout.LayoutParams badgeViewParams = new LinearLayout.LayoutParams(
857                                (int) getResources().getDimension(
858                                        R.dimen.expanding_entry_card_item_icon_width),
859                                (int) getResources().getDimension(
860                                        R.dimen.expanding_entry_card_item_icon_height));
861                        badgeViewParams.setMarginEnd((int) getResources().getDimension(
862                                R.dimen.expanding_entry_card_badge_separator_margin));
863                        badgeView.setLayoutParams(badgeViewParams);
864                        badgeView.setImageDrawable(badgeDrawable);
865                        mBadges.add(badgeView);
866                        mBadgeIds.add(badgeResourceId);
867                    }
868                }
869            }
870            mBadgeContainer.removeAllViews();
871            for (ImageView badge : mBadges) {
872                mBadgeContainer.addView(badge);
873            }
874        }
875    }
876
877    private void expand() {
878        ChangeBounds boundsTransition = new ChangeBounds();
879        boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
880
881        Fade fadeIn = new Fade(Fade.IN);
882        fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
883        fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);
884
885        TransitionSet transitionSet = new TransitionSet();
886        transitionSet.addTransition(boundsTransition);
887        transitionSet.addTransition(fadeIn);
888
889        transitionSet.excludeTarget(R.id.text, /* exclude = */ true);
890
891        final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
892                this : mAnimationViewGroup;
893
894        transitionSet.addListener(new TransitionListener() {
895            @Override
896            public void onTransitionStart(Transition transition) {
897                mListener.onExpand();
898            }
899
900            @Override
901            public void onTransitionEnd(Transition transition) {
902                mListener.onExpandDone();
903            }
904
905            @Override
906            public void onTransitionCancel(Transition transition) {
907            }
908
909            @Override
910            public void onTransitionPause(Transition transition) {
911            }
912
913            @Override
914            public void onTransitionResume(Transition transition) {
915            }
916        });
917
918        TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
919
920        mIsExpanded = true;
921        // In order to insert new entries, we may need to inflate them for the first time
922        inflateAllEntries(LayoutInflater.from(getContext()));
923        insertEntriesIntoViewGroup();
924        updateExpandCollapseButton(getCollapseButtonText(),
925                DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
926    }
927
928    private void collapse() {
929        final List<View> views = calculateEntriesToRemoveDuringCollapse();
930
931        // This animation requires layout changes, unlike the expand() animation: the action bar
932        // might get scrolled open in order to fill empty space. As a result, we can't use
933        // ChangeBounds here. Instead manually animate view height and alpha. This isn't as
934        // efficient as the bounds and translation changes performed by ChangeBounds. Nonetheless, a
935        // reasonable frame-rate is achieved collapsing a dozen elements on a user Svelte N4. So the
936        // performance hit doesn't justify writing a less maintainable animation.
937        final AnimatorSet set = new AnimatorSet();
938        final List<Animator> animators = new ArrayList<Animator>(views.size());
939        int totalSizeChange = 0;
940        for (View viewToRemove : views) {
941            final ObjectAnimator animator = ObjectAnimator.ofObject(viewToRemove,
942                    VIEW_LAYOUT_HEIGHT_PROPERTY, null, viewToRemove.getHeight(), 0);
943            totalSizeChange += viewToRemove.getHeight();
944            animator.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
945            animators.add(animator);
946            viewToRemove.animate().alpha(0).setDuration(DURATION_COLLAPSE_ANIMATION_FADE_OUT);
947        }
948        set.playTogether(animators);
949        set.start();
950        set.addListener(new AnimatorListener() {
951            @Override
952            public void onAnimationStart(Animator animation) {
953            }
954
955            @Override
956            public void onAnimationEnd(Animator animation) {
957                // Now that the views have been animated away, actually remove them from the view
958                // hierarchy. Reset their appearance so that they look appropriate when they
959                // get added back later.
960                insertEntriesIntoViewGroup();
961                for (View view : views) {
962                    if (view instanceof EntryView) {
963                        VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, LayoutParams.WRAP_CONTENT);
964                    } else {
965                        VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, mDividerLineHeightPixels);
966                    }
967                    view.animate().cancel();
968                    view.setAlpha(1);
969                }
970            }
971
972            @Override
973            public void onAnimationCancel(Animator animation) {
974            }
975
976            @Override
977            public void onAnimationRepeat(Animator animation) {
978            }
979        });
980
981        mListener.onCollapse(totalSizeChange);
982        mIsExpanded = false;
983        updateExpandCollapseButton(getExpandButtonText(),
984                DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
985    }
986
987    /**
988     * Returns whether the view is currently in its expanded state.
989     */
990    public boolean isExpanded() {
991        return mIsExpanded;
992    }
993
994    /**
995     * Sets the title text of this ExpandingEntryCardView.
996     * @param title The title to set. A null title will result in the title being removed.
997     */
998    public void setTitle(String title) {
999        if (mTitleTextView == null) {
1000            Log.e(TAG, "mTitleTextView is null");
1001        }
1002        mTitleTextView.setText(title);
1003        mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
1004        findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ?
1005                View.GONE : View.VISIBLE);
1006        // If the title is set after children have been added, reset the top entry's padding to
1007        // the default. Else if the title is cleared after children have been added, set
1008        // the extra top padding
1009        if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
1010            View firstEntry = mEntriesViewGroup.getChildAt(0);
1011            firstEntry.setPadding(firstEntry.getPaddingLeft(),
1012                    getResources().getDimensionPixelSize(
1013                            R.dimen.expanding_entry_card_item_padding_top),
1014                    firstEntry.getPaddingRight(),
1015                    firstEntry.getPaddingBottom());
1016        } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
1017            View firstEntry = mEntriesViewGroup.getChildAt(0);
1018            firstEntry.setPadding(firstEntry.getPaddingLeft(),
1019                    getResources().getDimensionPixelSize(
1020                            R.dimen.expanding_entry_card_item_padding_top) +
1021                            getResources().getDimensionPixelSize(
1022                                    R.dimen.expanding_entry_card_null_title_top_extra_padding),
1023                    firstEntry.getPaddingRight(),
1024                    firstEntry.getPaddingBottom());
1025        }
1026    }
1027
1028    public boolean shouldShow() {
1029        return mEntries != null && mEntries.size() > 0;
1030    }
1031
1032    public static final class EntryView extends RelativeLayout {
1033        private EntryContextMenuInfo mEntryContextMenuInfo;
1034
1035        public EntryView(Context context) {
1036            super(context);
1037        }
1038
1039        public EntryView(Context context, AttributeSet attrs) {
1040            super(context, attrs);
1041        }
1042
1043        public void setContextMenuInfo(EntryContextMenuInfo info) {
1044            mEntryContextMenuInfo = info;
1045        }
1046
1047        @Override
1048        protected ContextMenuInfo getContextMenuInfo() {
1049            return mEntryContextMenuInfo;
1050        }
1051    }
1052
1053    public static final class EntryContextMenuInfo implements ContextMenuInfo {
1054        private final String mCopyText;
1055        private final String mCopyLabel;
1056        private final String mMimeType;
1057        private final long mId;
1058        private final boolean mIsSuperPrimary;
1059
1060        public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id,
1061                boolean isSuperPrimary) {
1062            mCopyText = copyText;
1063            mCopyLabel = copyLabel;
1064            mMimeType = mimeType;
1065            mId = id;
1066            mIsSuperPrimary = isSuperPrimary;
1067        }
1068
1069        public String getCopyText() {
1070            return mCopyText;
1071        }
1072
1073        public String getCopyLabel() {
1074            return mCopyLabel;
1075        }
1076
1077        public String getMimeType() {
1078            return mMimeType;
1079        }
1080
1081        public long getId() {
1082            return mId;
1083        }
1084
1085        public boolean isSuperPrimary() {
1086            return mIsSuperPrimary;
1087        }
1088    }
1089
1090    static final class EntryTag {
1091        private final int mId;
1092        private final Intent mIntent;
1093
1094        public EntryTag(int id, Intent intent) {
1095            mId = id;
1096            mIntent = intent;
1097        }
1098
1099        public int getId() {
1100            return mId;
1101        }
1102
1103        public Intent getIntent() {
1104            return mIntent;
1105        }
1106    }
1107
1108    /**
1109     * This custom touch listener increases the touch area for the second and third icons, if
1110     * they are present. This is necessary to maintain other properties on an entry view, like
1111     * using a top padding on entry. Based off of {@link android.view.TouchDelegate}
1112     */
1113    private static final class EntryTouchListener implements View.OnTouchListener {
1114        private final View mEntry;
1115        private final ImageView mAlternateIcon;
1116        private final ImageView mThirdIcon;
1117        /** mTouchedView locks in a view on touch down */
1118        private View mTouchedView;
1119        /** mSlop adds some space to account for touches that are just outside the hit area */
1120        private int mSlop;
1121
1122        public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) {
1123            mEntry = entry;
1124            mAlternateIcon = alternateIcon;
1125            mThirdIcon = thirdIcon;
1126            mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop();
1127        }
1128
1129        @Override
1130        public boolean onTouch(View v, MotionEvent event) {
1131            View touchedView = mTouchedView;
1132            boolean sendToTouched = false;
1133            boolean hit = true;
1134            boolean handled = false;
1135
1136            switch (event.getAction()) {
1137                case MotionEvent.ACTION_DOWN:
1138                    if (hitThirdIcon(event)) {
1139                        mTouchedView = mThirdIcon;
1140                        sendToTouched = true;
1141                    } else if (hitAlternateIcon(event)) {
1142                        mTouchedView = mAlternateIcon;
1143                        sendToTouched = true;
1144                    } else {
1145                        mTouchedView = mEntry;
1146                        sendToTouched = false;
1147                    }
1148                    touchedView = mTouchedView;
1149                    break;
1150                case MotionEvent.ACTION_UP:
1151                case MotionEvent.ACTION_MOVE:
1152                    sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1153                    if (sendToTouched) {
1154                        final Rect slopBounds = new Rect();
1155                        touchedView.getHitRect(slopBounds);
1156                        slopBounds.inset(-mSlop, -mSlop);
1157                        if (!slopBounds.contains((int) event.getX(), (int) event.getY())) {
1158                            hit = false;
1159                        }
1160                    }
1161                    break;
1162                case MotionEvent.ACTION_CANCEL:
1163                    sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1164                    mTouchedView = null;
1165                    break;
1166            }
1167            if (sendToTouched) {
1168                if (hit) {
1169                    event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2);
1170                } else {
1171                    // Offset event coordinates to be outside the target view (in case it does
1172                    // something like tracking pressed state)
1173                    event.setLocation(-(mSlop * 2), -(mSlop * 2));
1174                }
1175                handled = touchedView.dispatchTouchEvent(event);
1176            }
1177            return handled;
1178        }
1179
1180        private boolean hitThirdIcon(MotionEvent event) {
1181            if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
1182                return mThirdIcon.getVisibility() == View.VISIBLE &&
1183                        event.getX() < mThirdIcon.getRight();
1184            } else {
1185                return mThirdIcon.getVisibility() == View.VISIBLE &&
1186                        event.getX() > mThirdIcon.getLeft();
1187            }
1188        }
1189
1190        /**
1191         * Should be used after checking if third icon was hit
1192         */
1193        private boolean hitAlternateIcon(MotionEvent event) {
1194            // LayoutParams used to add the start margin to the touch area
1195            final RelativeLayout.LayoutParams alternateIconParams =
1196                    (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams();
1197            if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
1198                return mAlternateIcon.getVisibility() == View.VISIBLE &&
1199                        event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin;
1200            } else {
1201                return mAlternateIcon.getVisibility() == View.VISIBLE &&
1202                        event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin;
1203            }
1204        }
1205    }
1206}
1207