1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Paint.FontMetricsInt;
23import android.graphics.Typeface;
24import android.util.SparseArray;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.View.MeasureSpec;
28import android.view.ViewGroup;
29import android.view.ViewGroup.LayoutParams;
30import android.widget.FrameLayout;
31import android.widget.TextView;
32
33import com.android.mail.R;
34import com.android.mail.R.dimen;
35import com.android.mail.R.id;
36import com.android.mail.ui.ViewMode;
37import com.android.mail.utils.Utils;
38import com.google.common.base.Objects;
39
40/**
41 * Represents the coordinates of elements inside a CanvasConversationHeaderView
42 * (eg, checkmark, star, subject, sender, folders, etc.) It will inflate a view,
43 * and record the coordinates of each element after layout. This will allows us
44 * to easily improve performance by creating custom view while still defining
45 * layout in XML files.
46 *
47 * @author phamm
48 */
49public class ConversationItemViewCoordinates {
50    // Modes
51    static final int MODE_COUNT = 2;
52    static final int WIDE_MODE = 0;
53    static final int NORMAL_MODE = 1;
54
55    // Left-side gadget modes
56    static final int GADGET_NONE = 0;
57    static final int GADGET_CONTACT_PHOTO = 1;
58    static final int GADGET_CHECKBOX = 2;
59
60    // Attachment previews modes
61    static final int ATTACHMENT_PREVIEW_NONE = 0;
62    static final int ATTACHMENT_PREVIEW_UNREAD = 1;
63    static final int ATTACHMENT_PREVIEW_READ = 2;
64
65    // For combined views
66    private static int COLOR_BLOCK_WIDTH = -1;
67    private static int COLOR_BLOCK_HEIGHT = -1;
68
69    /**
70     * Simple holder class for an item's abstract configuration state. ListView binding creates an
71     * instance per item, and {@link #forConfig(Context, Config, SparseArray)} uses it to hide/show
72     * optional views and determine the correct coordinates for that item configuration.
73     */
74    public static final class Config {
75        private int mWidth;
76        private int mViewMode = ViewMode.UNKNOWN;
77        private int mGadgetMode = GADGET_NONE;
78        private int mAttachmentPreviewMode = ATTACHMENT_PREVIEW_NONE;
79        private boolean mShowFolders = false;
80        private boolean mShowReplyState = false;
81        private boolean mShowColorBlock = false;
82        private boolean mShowPersonalIndicator = false;
83
84        public Config setViewMode(int viewMode) {
85            mViewMode = viewMode;
86            return this;
87        }
88
89        public Config withGadget(int gadget) {
90            mGadgetMode = gadget;
91            return this;
92        }
93
94        public Config withAttachmentPreviews(int attachmentPreviewMode) {
95            mAttachmentPreviewMode = attachmentPreviewMode;
96            return this;
97        }
98
99        public Config showFolders() {
100            mShowFolders = true;
101            return this;
102        }
103
104        public Config showReplyState() {
105            mShowReplyState = true;
106            return this;
107        }
108
109        public Config showColorBlock() {
110            mShowColorBlock = true;
111            return this;
112        }
113
114        public Config showPersonalIndicator() {
115            mShowPersonalIndicator  = true;
116            return this;
117        }
118
119        public Config updateWidth(int width) {
120            mWidth = width;
121            return this;
122        }
123
124        public int getWidth() {
125            return mWidth;
126        }
127
128        public int getViewMode() {
129            return mViewMode;
130        }
131
132        public int getGadgetMode() {
133            return mGadgetMode;
134        }
135
136        public int getAttachmentPreviewMode() {
137            return mAttachmentPreviewMode;
138        }
139
140        public boolean areFoldersVisible() {
141            return mShowFolders;
142        }
143
144        public boolean isReplyStateVisible() {
145            return mShowReplyState;
146        }
147
148        public boolean isColorBlockVisible() {
149            return mShowColorBlock;
150        }
151
152        public boolean isPersonalIndicatorVisible() {
153            return mShowPersonalIndicator;
154        }
155
156        private int getCacheKey() {
157            // hash the attributes that contribute to item height and child view geometry
158            return Objects.hashCode(mWidth, mViewMode, mGadgetMode, mAttachmentPreviewMode,
159                    mShowFolders, mShowReplyState, mShowPersonalIndicator);
160        }
161
162    }
163
164    public static class CoordinatesCache {
165        private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache
166                = new SparseArray<ConversationItemViewCoordinates>();
167        private final SparseArray<View> mViewsCache = new SparseArray<View>();
168
169        public ConversationItemViewCoordinates getCoordinates(final int key) {
170            return mCoordinatesCache.get(key);
171        }
172
173        public View getView(final int layoutId) {
174            return mViewsCache.get(layoutId);
175        }
176
177        public void put(final int key, final ConversationItemViewCoordinates coords) {
178            mCoordinatesCache.put(key, coords);
179        }
180
181        public void put(final int layoutId, final View view) {
182            mViewsCache.put(layoutId, view);
183        }
184    }
185
186    /**
187     * One of either NORMAL_MODE or WIDE_MODE.
188     */
189    private final int mMode;
190
191    final int height;
192
193    // Star.
194    final int starX;
195    final int starY;
196    final int starWidth;
197
198    // Senders.
199    final int sendersX;
200    final int sendersY;
201    final int sendersWidth;
202    final int sendersHeight;
203    final int sendersLineCount;
204    final int sendersLineHeight;
205    final float sendersFontSize;
206
207    // Subject.
208    final int subjectX;
209    final int subjectY;
210    final int subjectWidth;
211    final int subjectHeight;
212    final int subjectLineCount;
213    final float subjectFontSize;
214
215    // Folders.
216    final int foldersX;
217    final int foldersXEnd;
218    final int foldersY;
219    final int foldersHeight;
220    final Typeface foldersTypeface;
221    final float foldersFontSize;
222    final int foldersTextBottomPadding;
223
224    // Info icon
225    final int infoIconXEnd;
226    final int infoIconY;
227
228    // Date.
229    final int dateXEnd;
230    final int dateY;
231    final int datePaddingLeft;
232    final float dateFontSize;
233    final int dateYBaseline;
234
235    // Paperclip.
236    final int paperclipY;
237    final int paperclipPaddingLeft;
238
239    // Color block.
240    final int colorBlockX;
241    final int colorBlockY;
242    final int colorBlockWidth;
243    final int colorBlockHeight;
244
245    // Reply state of a conversation.
246    final int replyStateX;
247    final int replyStateY;
248
249    final int personalIndicatorX;
250    final int personalIndicatorY;
251
252    final int contactImagesHeight;
253    final int contactImagesWidth;
254    final int contactImagesX;
255    final int contactImagesY;
256
257    // Attachment previews
258    public final int attachmentPreviewsX;
259    public final int attachmentPreviewsY;
260    final int attachmentPreviewsWidth;
261    final int attachmentPreviewsHeight;
262    public final int attachmentPreviewsDecodeHeight;
263
264    // Attachment previews overflow badge and count
265    public final int overflowXEnd;
266    public final int overflowYEnd;
267    public final int overflowDiameter;
268    public final float overflowFontSize;
269    public final Typeface overflowTypeface;
270
271    // Attachment previews placeholder
272    final int placeholderY;
273    public final int placeholderWidth;
274    public final int placeholderHeight;
275    // Attachment previews progress bar
276    final int progressBarY;
277    public final int progressBarWidth;
278    public final int progressBarHeight;
279
280    /**
281     * The smallest item width for which we use the "wide" layout.
282     */
283    private final int mMinListWidthForWide;
284    /**
285     * The smallest item width for which we use the "spacious" variant of the normal layout,
286     * if the normal version is used at all. Larger than {@link #mMinListWidthForWide}, we use
287     * wide mode anyway, and this value is unused.
288     */
289    private final int mMinListWidthIsSpacious;
290    private final int mFolderCellWidth;
291    private final int mFolderMinimumWidth;
292
293    private ConversationItemViewCoordinates(final Context context, final Config config,
294            final CoordinatesCache cache) {
295        Utils.traceBeginSection("CIV coordinates constructor");
296        final Resources res = context.getResources();
297        mFolderCellWidth = res.getDimensionPixelSize(R.dimen.folder_cell_width);
298        mMinListWidthForWide = res.getDimensionPixelSize(R.dimen.list_min_width_is_wide);
299        mMinListWidthIsSpacious = res.getDimensionPixelSize(
300                R.dimen.list_normal_mode_min_width_is_spacious);
301        mFolderMinimumWidth = res.getDimensionPixelSize(R.dimen.folder_minimum_width);
302
303        mMode = calculateMode(res, config);
304
305        final int layoutId;
306        if (mMode == WIDE_MODE) {
307            layoutId = R.layout.conversation_item_view_wide;
308        } else {
309            if (config.getWidth() >= mMinListWidthIsSpacious) {
310                layoutId = R.layout.conversation_item_view_normal_spacious;
311            } else {
312                layoutId = R.layout.conversation_item_view_normal;
313            }
314        }
315
316        ViewGroup view = (ViewGroup) cache.getView(layoutId);
317        if (view == null) {
318            view = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null);
319            cache.put(layoutId, view);
320        }
321
322        // Show/hide optional views before measure/layout call
323
324        final View attachmentPreviews = view.findViewById(R.id.attachment_previews);;
325        if (config.getAttachmentPreviewMode() != ATTACHMENT_PREVIEW_NONE) {
326            final LayoutParams params = attachmentPreviews.getLayoutParams();
327            attachmentPreviews.setVisibility(View.VISIBLE);
328            params.height = getAttachmentPreviewsHeight(context, config.getAttachmentPreviewMode());
329            attachmentPreviews.setLayoutParams(params);
330        } else {
331            attachmentPreviews.setVisibility(View.GONE);
332        }
333        attachmentPreviewsDecodeHeight = getAttachmentPreviewsHeight(context,
334                ATTACHMENT_PREVIEW_UNREAD);
335
336        final TextView folders = (TextView) view.findViewById(R.id.folders);
337        folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE);
338
339        // Add margin between attachment previews and folders
340        final View attachmentPreviewsBottomMargin = view
341                .findViewById(R.id.attachment_previews_bottom_margin);
342        final boolean marginVisible = config.getAttachmentPreviewMode() != ATTACHMENT_PREVIEW_NONE
343                && config.areFoldersVisible();
344        attachmentPreviewsBottomMargin.setVisibility(marginVisible ? View.VISIBLE : View.GONE);
345
346        View contactImagesView = view.findViewById(R.id.contact_image);
347
348        switch (config.getGadgetMode()) {
349            case GADGET_CONTACT_PHOTO:
350                contactImagesView.setVisibility(View.VISIBLE);
351                break;
352            case GADGET_CHECKBOX:
353                contactImagesView.setVisibility(View.GONE);
354                contactImagesView = null;
355                break;
356            default:
357                contactImagesView.setVisibility(View.GONE);
358                contactImagesView = null;
359                break;
360        }
361
362        final View replyState = view.findViewById(R.id.reply_state);
363        replyState.setVisibility(config.isReplyStateVisible() ? View.VISIBLE : View.GONE);
364
365        final View personalIndicator = view.findViewById(R.id.personal_indicator);
366        personalIndicator.setVisibility(
367                config.isPersonalIndicatorVisible() ? View.VISIBLE : View.GONE);
368
369        // Layout the appropriate view.
370        final int widthSpec = MeasureSpec.makeMeasureSpec(config.getWidth(), MeasureSpec.EXACTLY);
371        final int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
372
373        view.measure(widthSpec, heightSpec);
374        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
375
376//        Utils.dumpViewTree((ViewGroup) view);
377
378        // Records coordinates.
379
380        // Contact images view
381        if (contactImagesView != null) {
382            contactImagesWidth = contactImagesView.getWidth();
383            contactImagesHeight = contactImagesView.getHeight();
384            contactImagesX = getX(contactImagesView);
385            contactImagesY = getY(contactImagesView);
386        } else {
387            contactImagesX = contactImagesY = contactImagesWidth = contactImagesHeight = 0;
388        }
389
390        final View star = view.findViewById(R.id.star);
391        starX = getX(star);
392        starY = getY(star);
393        starWidth = star.getWidth();
394
395        final TextView senders = (TextView) view.findViewById(R.id.senders);
396        final int sendersTopAdjust = getLatinTopAdjustment(senders);
397        sendersX = getX(senders);
398        sendersY = getY(senders) + sendersTopAdjust;
399        sendersWidth = senders.getWidth();
400        sendersHeight = senders.getHeight();
401        sendersLineCount = getLineCount(senders);
402        sendersLineHeight = senders.getLineHeight();
403        sendersFontSize = senders.getTextSize();
404
405        final TextView subject = (TextView) view.findViewById(R.id.subject);
406        final int subjectTopAdjust = getLatinTopAdjustment(subject);
407        subjectX = getX(subject);
408        if (isWide()) {
409            subjectY = getY(subject) + subjectTopAdjust;
410        } else {
411            subjectY = getY(subject) + sendersTopAdjust;
412        }
413        subjectWidth = subject.getWidth();
414        subjectHeight = subject.getHeight();
415        subjectLineCount = getLineCount(subject);
416        subjectFontSize = subject.getTextSize();
417
418        if (config.areFoldersVisible()) {
419            // vertically align folders min left edge with subject
420            foldersX = subjectX;
421            foldersXEnd = getX(folders) + folders.getWidth();
422            if (isWide()) {
423                foldersY = getY(folders);
424            } else {
425                foldersY = getY(folders) + sendersTopAdjust;
426            }
427            foldersHeight = folders.getHeight();
428            foldersTypeface = folders.getTypeface();
429            foldersTextBottomPadding = res
430                    .getDimensionPixelSize(R.dimen.folders_text_bottom_padding);
431            foldersFontSize = folders.getTextSize();
432        } else {
433            foldersX = 0;
434            foldersXEnd = 0;
435            foldersY = 0;
436            foldersHeight = 0;
437            foldersTypeface = null;
438            foldersTextBottomPadding = 0;
439            foldersFontSize = 0;
440        }
441
442        final View colorBlock = view.findViewById(R.id.color_block);
443        if (config.isColorBlockVisible() && colorBlock != null) {
444            colorBlockX = getX(colorBlock);
445            colorBlockY = getY(colorBlock);
446            colorBlockWidth = colorBlock.getWidth();
447            colorBlockHeight = colorBlock.getHeight();
448        } else {
449            colorBlockX = colorBlockY = colorBlockWidth = colorBlockHeight = 0;
450        }
451
452        if (config.isReplyStateVisible()) {
453            replyStateX = getX(replyState);
454            replyStateY = getY(replyState);
455        } else {
456            replyStateX = replyStateY = 0;
457        }
458
459        if (config.isPersonalIndicatorVisible()) {
460            personalIndicatorX = getX(personalIndicator);
461            personalIndicatorY = getY(personalIndicator);
462        } else {
463            personalIndicatorX = personalIndicatorY = 0;
464        }
465
466        final View infoIcon = view.findViewById(R.id.info_icon);
467        infoIconXEnd = getX(infoIcon) + infoIcon.getWidth();
468        infoIconY = getY(infoIcon);
469
470        final TextView date = (TextView) view.findViewById(R.id.date);
471        dateXEnd = getX(date) + date.getWidth();
472        dateY = getY(date);
473        datePaddingLeft = date.getPaddingLeft();
474        dateFontSize = date.getTextSize();
475        dateYBaseline = dateY + getLatinTopAdjustment(date) + date.getBaseline();
476
477        final View paperclip = view.findViewById(R.id.paperclip);
478        paperclipY = getY(paperclip);
479        paperclipPaddingLeft = paperclip.getPaddingLeft();
480
481        if (attachmentPreviews != null) {
482            attachmentPreviewsX = subjectX;
483            attachmentPreviewsY = getY(attachmentPreviews) + sendersTopAdjust;
484            attachmentPreviewsWidth = subjectWidth;
485            attachmentPreviewsHeight = attachmentPreviews.getHeight();
486
487            // We only care about the right and bottom of the overflow count
488            final TextView overflow = (TextView) view.findViewById(id.ap_overflow);
489            final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) overflow
490                    .getLayoutParams();
491            overflowXEnd = attachmentPreviewsX + attachmentPreviewsWidth - params.rightMargin;
492            overflowYEnd = attachmentPreviewsY + attachmentPreviewsHeight - params.bottomMargin;
493            overflowDiameter = overflow.getWidth();
494            overflowFontSize = overflow.getTextSize();
495            overflowTypeface = overflow.getTypeface();
496
497            final View placeholder = view.findViewById(id.ap_placeholder);
498            placeholderWidth = placeholder.getWidth();
499            placeholderHeight = placeholder.getHeight();
500            placeholderY = attachmentPreviewsY + attachmentPreviewsHeight / 2
501                    - placeholderHeight / 2;
502
503            final View progressBar = view.findViewById(id.ap_progress_bar);
504            progressBarWidth = progressBar.getWidth();
505            progressBarHeight = progressBar.getHeight();
506            progressBarY = attachmentPreviewsY + attachmentPreviewsHeight / 2
507                    - progressBarHeight / 2;
508        } else {
509            attachmentPreviewsX = 0;
510            attachmentPreviewsY = 0;
511            attachmentPreviewsWidth = 0;
512            attachmentPreviewsHeight = 0;
513            overflowXEnd = 0;
514            overflowYEnd = 0;
515            overflowDiameter = 0;
516            overflowFontSize = 0;
517            overflowTypeface = null;
518            placeholderY = 0;
519            placeholderWidth = 0;
520            placeholderHeight = 0;
521            progressBarY = 0;
522            progressBarWidth = 0;
523            progressBarHeight = 0;
524        }
525
526        height = view.getHeight() + (isWide() ? 0 : sendersTopAdjust);
527        Utils.traceEndSection();
528    }
529
530    public int getMode() {
531        return mMode;
532    }
533
534    public boolean isWide() {
535        return mMode == WIDE_MODE;
536    }
537
538    /**
539     * Returns a negative corrective value that you can apply to a TextView's vertical dimensions
540     * that will nudge the first line of text upwards such that uppercase Latin characters are
541     * truly top-aligned.
542     * <p>
543     * N.B. this will cause other characters to draw above the top! only use this if you have
544     * adequate top margin.
545     *
546     */
547    private static int getLatinTopAdjustment(TextView t) {
548        final FontMetricsInt fmi = t.getPaint().getFontMetricsInt();
549        return (fmi.top - fmi.ascent);
550    }
551
552    /**
553     * Returns the mode of the header view (Wide/Normal).
554     */
555    private int calculateMode(Resources res, Config config) {
556        switch (config.getViewMode()) {
557            case ViewMode.CONVERSATION_LIST:
558                return config.getWidth() >= mMinListWidthForWide ? WIDE_MODE : NORMAL_MODE;
559
560            case ViewMode.SEARCH_RESULTS_LIST:
561                return res.getInteger(R.integer.conversation_list_search_header_mode);
562
563            default:
564                return res.getInteger(R.integer.conversation_header_mode);
565        }
566    }
567
568    private int getAttachmentPreviewsHeight(final Context context,
569            final int attachmentPreviewMode) {
570        final Resources res = context.getResources();
571        switch (attachmentPreviewMode) {
572            case ATTACHMENT_PREVIEW_UNREAD:
573                return (int) (isWide() ? res.getDimension(dimen.attachment_preview_height_tall_wide)
574                        : res.getDimension(dimen.attachment_preview_height_tall));
575            case ATTACHMENT_PREVIEW_READ:
576                return (int) res.getDimension(dimen.attachment_preview_height_short);
577            default:
578                return 0;
579        }
580    }
581
582    /**
583     * Returns the x coordinates of a view by tracing up its hierarchy.
584     */
585    private static int getX(View view) {
586        int x = 0;
587        while (view != null) {
588            x += (int) view.getX();
589            view = (View) view.getParent();
590        }
591        return x;
592    }
593
594    /**
595     * Returns the y coordinates of a view by tracing up its hierarchy.
596     */
597    private static int getY(View view) {
598        int y = 0;
599        while (view != null) {
600            y += (int) view.getY();
601            view = (View) view.getParent();
602        }
603        return y;
604    }
605
606    /**
607     * Returns the number of lines of this text view. Delegates to built-in TextView logic on JB+.
608     */
609    private static int getLineCount(TextView textView) {
610        if (Utils.isRunningJellybeanOrLater()) {
611            return textView.getMaxLines();
612        } else {
613            return Math.round(((float) textView.getHeight()) / textView.getLineHeight());
614        }
615    }
616
617    /**
618     * Returns the length (maximum of characters) of subject in this mode.
619     */
620    public static int getSendersLength(Context context, int mode, boolean hasAttachments) {
621        final Resources res = context.getResources();
622        if (hasAttachments) {
623            return res.getIntArray(R.array.senders_with_attachment_lengths)[mode];
624        } else {
625            return res.getIntArray(R.array.senders_lengths)[mode];
626        }
627    }
628
629    @Deprecated
630    public static int getColorBlockWidth(Context context) {
631        Resources res = context.getResources();
632        if (COLOR_BLOCK_WIDTH <= 0) {
633            COLOR_BLOCK_WIDTH = res.getDimensionPixelSize(R.dimen.color_block_width);
634        }
635        return COLOR_BLOCK_WIDTH;
636    }
637
638    @Deprecated
639    public static int getColorBlockHeight(Context context) {
640        Resources res = context.getResources();
641        if (COLOR_BLOCK_HEIGHT <= 0) {
642            COLOR_BLOCK_HEIGHT = res.getDimensionPixelSize(R.dimen.color_block_height);
643        }
644        return COLOR_BLOCK_HEIGHT;
645    }
646
647    public static boolean displaySendersInline(int mode) {
648        switch (mode) {
649            case WIDE_MODE:
650                return false;
651            case NORMAL_MODE:
652                return true;
653            default:
654                throw new IllegalArgumentException("Unknown conversation header view mode " + mode);
655        }
656    }
657
658    /**
659     * Returns coordinates for elements inside a conversation header view given
660     * the view width.
661     */
662    public static ConversationItemViewCoordinates forConfig(final Context context,
663            final Config config, final CoordinatesCache cache) {
664        final int cacheKey = config.getCacheKey();
665        ConversationItemViewCoordinates coordinates = cache.getCoordinates(cacheKey);
666        if (coordinates != null) {
667            return coordinates;
668        }
669
670        coordinates = new ConversationItemViewCoordinates(context, config, cache);
671        cache.put(cacheKey, coordinates);
672        return coordinates;
673    }
674
675    /**
676     * Return the minimum width of a folder cell with no text. Essentially this is the left+right
677     * intra-cell margin within cells.
678     *
679     */
680    public int getFolderCellWidth() {
681        return mFolderCellWidth;
682    }
683
684    /**
685     * Return the minimum width of a folder cell, period. This will affect the
686     * maximum number of folders we can display.
687     */
688    public int getFolderMinimumWidth() {
689        return mFolderMinimumWidth;
690    }
691
692    public static boolean isWideMode(int mode) {
693        return mode == WIDE_MODE;
694    }
695
696}
697