MessageListItem.java revision 8e779e627a2185320ab168fdce09477a73bf5f22
1/*
2 * Copyright (C) 2009 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 */
16
17package com.android.email.activity;
18
19import com.android.email.R;
20import com.android.emailcommon.utility.TextUtilities;
21
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Canvas;
27import android.graphics.Paint;
28import android.graphics.Typeface;
29import android.graphics.drawable.Drawable;
30import android.text.Layout.Alignment;
31import android.text.Spannable;
32import android.text.SpannableString;
33import android.text.SpannableStringBuilder;
34import android.text.StaticLayout;
35import android.text.TextPaint;
36import android.text.TextUtils;
37import android.text.TextUtils.TruncateAt;
38import android.text.format.DateUtils;
39import android.text.style.StyleSpan;
40import android.util.AttributeSet;
41import android.view.MotionEvent;
42import android.view.View;
43import android.view.accessibility.AccessibilityEvent;
44
45/**
46 * This custom View is the list item for the MessageList activity, and serves two purposes:
47 * 1.  It's a container to store message metadata (e.g. the ids of the message, mailbox, & account)
48 * 2.  It handles internal clicks such as the checkbox or the favorite star
49 */
50public class MessageListItem extends View {
51    // Note: messagesAdapter directly fiddles with these fields.
52    /* package */ long mMessageId;
53    /* package */ long mMailboxId;
54    /* package */ long mAccountId;
55
56    private MessagesAdapter mAdapter;
57    private MessageListItemCoordinates mCoordinates;
58    private Context mContext;
59
60    private boolean mDownEvent;
61
62    public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL =
63        "com.android.email.MESSAGE_LIST_ITEMS";
64
65    public MessageListItem(Context context) {
66        super(context);
67        init(context);
68    }
69
70    public MessageListItem(Context context, AttributeSet attrs) {
71        super(context, attrs);
72        init(context);
73    }
74
75    public MessageListItem(Context context, AttributeSet attrs, int defStyle) {
76        super(context, attrs, defStyle);
77        init(context);
78    }
79
80    // Wide mode shows sender, snippet, time, and favorite spread out across the screen
81    private static final int MODE_WIDE = MessageListItemCoordinates.WIDE_MODE;
82    // Sentinel indicating that the view needs layout
83    public static final int NEEDS_LAYOUT = -1;
84
85    private static boolean sInit = false;
86    private static final TextPaint sDefaultPaint = new TextPaint();
87    private static final TextPaint sBoldPaint = new TextPaint();
88    private static final TextPaint sDatePaint = new TextPaint();
89    private static final TextPaint sHighlightPaint = new TextPaint();
90    private static Bitmap sAttachmentIcon;
91    private static Bitmap sInviteIcon;
92    private static Bitmap sFavoriteIconOff;
93    private static Bitmap sFavoriteIconOn;
94    private static Bitmap sSelectedIconOn;
95    private static Bitmap sSelectedIconOff;
96    private static Bitmap sStateReplied;
97    private static Bitmap sStateForwarded;
98    private static Bitmap sStateRepliedAndForwarded;
99    private static String sSubjectSnippetDivider;
100    private static String sSubjectDescription;
101    private static String sSubjectEmptyDescription;
102
103    public String mSender;
104    public CharSequence mText;
105    public CharSequence mSnippet;
106    private String mSubject;
107    private String mSubjectAndDescription;
108    private StaticLayout mSubjectLayout;
109    public boolean mRead;
110    public long mTimestamp;
111    public boolean mHasAttachment = false;
112    public boolean mHasInvite = true;
113    public boolean mIsFavorite = false;
114    public boolean mHasBeenRepliedTo = false;
115    public boolean mHasBeenForwarded = false;
116    /** {@link Paint} for account color chips.  null if no chips should be drawn.  */
117    public Paint mColorChipPaint;
118
119    private int mMode = -1;
120
121    private int mViewWidth = 0;
122    private int mViewHeight = 0;
123
124    private static int sTextSize;
125    private static int sItemHeightWide;
126    private static int sItemHeightNarrow;
127
128    // Note: these cannot be shared Drawables because they are selectors which have state.
129    private Drawable mReadSelector;
130    private Drawable mUnreadSelector;
131    private Drawable mWideReadSelector;
132    private Drawable mWideUnreadSelector;
133
134    public int mSnippetLineCount = NEEDS_LAYOUT;
135    private CharSequence mFormattedSender;
136    private CharSequence mFormattedDate;
137
138    private void init(Context context) {
139        mContext = context;
140        if (!sInit) {
141            Resources r = context.getResources();
142            sSubjectDescription = r.getString(R.string.message_subject_description).concat(", ");
143            sSubjectEmptyDescription = r.getString(R.string.message_is_empty_description);
144            sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
145            sTextSize =
146                r.getDimensionPixelSize(R.dimen.message_list_item_text_size);
147            sItemHeightWide =
148                r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
149            sItemHeightNarrow =
150                r.getDimensionPixelSize(R.dimen.message_list_item_height_narrow);
151
152            sDefaultPaint.setTypeface(Typeface.DEFAULT);
153            sDefaultPaint.setTextSize(sTextSize);
154            sDefaultPaint.setAntiAlias(true);
155            sDatePaint.setTypeface(Typeface.DEFAULT);
156            sDatePaint.setAntiAlias(true);
157            sBoldPaint.setTextSize(sTextSize);
158            sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
159            sBoldPaint.setAntiAlias(true);
160            sHighlightPaint.setColor(TextUtilities.HIGHLIGHT_COLOR_INT);
161            sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment);
162            sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite);
163            sFavoriteIconOff =
164                BitmapFactory.decodeResource(r, R.drawable.btn_star_off_normal_email_holo_light);
165            sFavoriteIconOn =
166                BitmapFactory.decodeResource(r, R.drawable.btn_star_on_normal_email_holo_light);
167            sSelectedIconOff =
168                BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
169            sSelectedIconOn =
170                BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
171
172            //TODO: put the actual drawables when they exist. these are temps for visibile testing.
173            sStateReplied =
174                BitmapFactory.decodeResource(r, R.drawable.reply);
175            sStateForwarded =
176                BitmapFactory.decodeResource(r, R.drawable.forward);
177            sStateRepliedAndForwarded =
178                BitmapFactory.decodeResource(r, R.drawable.reply_all);
179
180            sInit = true;
181        }
182    }
183
184    /**
185     * Sets message subject safely, ensuring the cache is invalidated.
186     */
187    public void setSubject(String subject) {
188        if (!subject.equals(mSubject)) {
189            mSubject = subject;
190            mSubjectAndDescription = null;
191        }
192    }
193
194    /**
195     * Determine the mode of this view (WIDE or NORMAL)
196     *
197     * @param width The width of the view
198     * @return The mode of the view
199     */
200    private int getViewMode(int width) {
201        return MessageListItemCoordinates.getMode(mContext, width);
202    }
203
204    private Drawable mCurentBackground = null; // Only used by updateBackground()
205
206    private void updateBackground() {
207        final Drawable newBackground;
208        if (mRead) {
209            if (mMode == MODE_WIDE) {
210                if (mWideReadSelector == null) {
211                    mWideReadSelector = getContext().getResources()
212                            .getDrawable(R.drawable.message_list_wide_read_selector);
213                }
214                newBackground = mWideReadSelector;
215            } else {
216                if (mReadSelector == null) {
217                    mReadSelector = getContext().getResources()
218                            .getDrawable(R.drawable.message_list_read_selector);
219                }
220                newBackground = mReadSelector;
221            }
222        } else {
223            if (mMode == MODE_WIDE) {
224                if (mWideUnreadSelector == null) {
225                    mWideUnreadSelector = getContext().getResources()
226                            .getDrawable(R.drawable.message_list_wide_unread_selector);
227                }
228                newBackground = mWideUnreadSelector;
229            } else {
230                if (mUnreadSelector == null) {
231                    mUnreadSelector = getContext().getResources()
232                            .getDrawable(R.drawable.message_list_unread_selector);
233                }
234                newBackground = mUnreadSelector;
235            }
236        }
237        if (newBackground != mCurentBackground) {
238            // setBackgroundDrawable is a heavy operation.  Only call it when really needed.
239            setBackgroundDrawable(newBackground);
240            mCurentBackground = newBackground;
241        }
242    }
243
244    private void calculateDrawingData() {
245        SpannableStringBuilder ssb = new SpannableStringBuilder();
246        boolean hasSubject = false;
247        if (!TextUtils.isEmpty(mSubject)) {
248            SpannableString ss = new SpannableString(mSubject);
249            ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
250                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
251            ssb.append(ss);
252            hasSubject = true;
253        }
254        if (!TextUtils.isEmpty(mSnippet)) {
255            if (hasSubject) {
256                ssb.append(sSubjectSnippetDivider);
257            }
258            ssb.append(mSnippet);
259        }
260        mText = ssb;
261
262        sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
263        mSubjectLayout = new StaticLayout(mText, sDefaultPaint,
264                mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
265        if (mCoordinates.subjectLineCount < mSubjectLayout.getLineCount()) {
266            // TODO: ellipsize.
267            int end = mSubjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
268            mSubjectLayout = new StaticLayout(mText.subSequence(0, end),
269                    sDefaultPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
270        }
271
272        // Now, format the sender for its width
273        TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
274        int senderWidth = mCoordinates.sendersWidth;
275        // And get the ellipsized string for the calculated width
276        if (TextUtils.isEmpty(mSender)) {
277            mFormattedSender = "";
278        } else {
279            mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth,
280                    TruncateAt.END);
281        }
282        // Get a nicely formatted date string (relative to today)
283        mFormattedDate = DateUtils.getRelativeTimeSpanString(getContext(), mTimestamp).toString();
284    }
285
286    @Override
287    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
288        if (widthMeasureSpec != 0 || mViewWidth == 0) {
289            mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
290            int mode = getViewMode(mViewWidth);
291            if (mode != mMode) {
292                // If the mode has changed, set the snippet line count to indicate layout required
293                mMode = mode;
294                mSnippetLineCount = NEEDS_LAYOUT;
295            }
296            mViewHeight = measureHeight(heightMeasureSpec, mMode);
297        }
298        setMeasuredDimension(mViewWidth, mViewHeight);
299    }
300
301    /**
302     * Determine the height of this view
303     *
304     * @param measureSpec A measureSpec packed into an int
305     * @param mode The current mode of this view
306     * @return The height of the view, honoring constraints from measureSpec
307     */
308    private int measureHeight(int measureSpec, int mode) {
309        int result = 0;
310        int specMode = MeasureSpec.getMode(measureSpec);
311        int specSize = MeasureSpec.getSize(measureSpec);
312
313        if (specMode == MeasureSpec.EXACTLY) {
314            // We were told how big to be
315            result = specSize;
316        } else {
317            // Measure the text
318            if (mMode == MODE_WIDE) {
319                result = sItemHeightWide;
320            } else {
321                result = sItemHeightNarrow;
322            }
323            if (specMode == MeasureSpec.AT_MOST) {
324                // Respect AT_MOST value if that was what is called for by
325                // measureSpec
326                result = Math.min(result, specSize);
327            }
328        }
329        return result;
330    }
331
332    @Override
333    public void draw(Canvas canvas) {
334        // Update the background, before View.draw() draws it.
335        setSelected(mAdapter.isSelected(this));
336        updateBackground();
337        super.draw(canvas);
338    }
339
340    @Override
341    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
342        super.onLayout(changed, left, top, right, bottom);
343
344        mCoordinates = MessageListItemCoordinates.forWidth(mContext, mViewWidth);
345    }
346
347    @Override
348    protected void onDraw(Canvas canvas) {
349        if (mSnippetLineCount == NEEDS_LAYOUT) {
350            calculateDrawingData();
351        }
352
353        // Draw the color chip indicating the mailbox this belongs to
354        if (mColorChipPaint != null) {
355            canvas.drawRect(
356                    mCoordinates.chipX, mCoordinates.chipY,
357                    mCoordinates.chipX + mCoordinates.chipWidth,
358                    mCoordinates.chipY + mCoordinates.chipHeight,
359                    mColorChipPaint);
360        }
361
362        // Draw the checkbox
363        canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
364                mCoordinates.checkmarkX, mCoordinates.checkmarkY, sDefaultPaint);
365
366        // Draw the sender name
367        canvas.drawText(mFormattedSender, 0, mFormattedSender.length(),
368                mCoordinates.sendersX, mCoordinates.sendersY - mCoordinates.sendersAscent,
369                mRead ? sDefaultPaint : sBoldPaint);
370
371        // Draw the reply state. Draw nothing if neither replied nor forwarded.
372        if (mHasBeenRepliedTo && mHasBeenForwarded) {
373            canvas.drawBitmap(sStateRepliedAndForwarded,
374                    mCoordinates.stateX, mCoordinates.stateY, sDefaultPaint);
375        } else if (mHasBeenRepliedTo) {
376            canvas.drawBitmap(sStateReplied,
377                    mCoordinates.stateX, mCoordinates.stateY, sDefaultPaint);
378        } else if (mHasBeenForwarded) {
379            canvas.drawBitmap(sStateForwarded,
380                    mCoordinates.stateX, mCoordinates.stateY, sDefaultPaint);
381        }
382
383        // Subject and snippet.
384        sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
385        sDefaultPaint.setTypeface(Typeface.DEFAULT);
386        canvas.save();
387        canvas.translate(
388                mCoordinates.subjectX,
389                mCoordinates.subjectY);
390        mSubjectLayout.draw(canvas);
391        canvas.restore();
392
393        // Draw the date
394        sDatePaint.setTextSize(mCoordinates.dateFontSize);
395        int dateX = mCoordinates.dateXEnd
396                - (int) sDatePaint.measureText(mFormattedDate, 0, mFormattedDate.length());
397
398        canvas.drawText(mFormattedDate, 0, mFormattedDate.length(),
399                dateX, mCoordinates.dateY - mCoordinates.dateAscent, sDatePaint);
400
401        // Draw the favorite icon
402        canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff,
403                mCoordinates.starX, mCoordinates.starY, sDefaultPaint);
404
405        // TODO: deal with the icon layouts better from the coordinate class so that this logic
406        // doesn't have to exist.
407        // Draw the attachment and invite icons, if necessary.
408        int iconsLeft = dateX;
409        if (mHasAttachment) {
410            iconsLeft = iconsLeft - sAttachmentIcon.getWidth();
411            canvas.drawBitmap(sAttachmentIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint);
412        }
413        if (mHasInvite) {
414            iconsLeft -= sInviteIcon.getWidth();
415            canvas.drawBitmap(sInviteIcon, iconsLeft, mCoordinates.paperclipY, sDefaultPaint);
416        }
417
418    }
419
420    /**
421     * Called by the adapter at bindView() time
422     *
423     * @param adapter the adapter that creates this view
424     */
425    public void bindViewInit(MessagesAdapter adapter) {
426        mAdapter = adapter;
427    }
428
429    /**
430     * Overriding this method allows us to "catch" clicks in the checkbox or star
431     * and process them accordingly.
432     */
433    @Override
434    public boolean onTouchEvent(MotionEvent event) {
435        boolean handled = false;
436        int touchX = (int) event.getX();
437        int checkRight = mCoordinates.checkmarkWidthIncludingMargins;
438        int starLeft = mViewWidth - mCoordinates.starWidthIncludingMargins;
439
440        switch (event.getAction()) {
441            case MotionEvent.ACTION_DOWN:
442                if (touchX < checkRight || touchX > starLeft) {
443                    mDownEvent = true;
444                    if ((touchX < checkRight) || (touchX > starLeft)) {
445                        handled = true;
446                    }
447                }
448                break;
449
450            case MotionEvent.ACTION_CANCEL:
451                mDownEvent = false;
452                break;
453
454            case MotionEvent.ACTION_UP:
455                if (mDownEvent) {
456                    if (touchX < checkRight) {
457                        mAdapter.toggleSelected(this);
458                        handled = true;
459                    } else if (touchX > starLeft) {
460                        mIsFavorite = !mIsFavorite;
461                        mAdapter.updateFavorite(this, mIsFavorite);
462                        handled = true;
463                    }
464                }
465                break;
466        }
467
468        if (handled) {
469            invalidate();
470        } else {
471            handled = super.onTouchEvent(event);
472        }
473
474        return handled;
475    }
476
477    @Override
478    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
479        CharSequence contentDescription = getContentDescription(mContext);
480        if (!TextUtils.isEmpty(contentDescription)) {
481            event.setClassName(getClass().getName());
482            event.setPackageName(getContext().getPackageName());
483            event.setEnabled(true);
484            event.setContentDescription(contentDescription);
485            return true;
486        }
487        return false;
488    }
489
490    /**
491     * Get message information to use for accessibility.
492     */
493    private CharSequence getContentDescription(Context context) {
494        if (mSubjectAndDescription == null) {
495            if (!TextUtils.isEmpty(mSubject)) {
496                mSubjectAndDescription = sSubjectDescription + mSubject;
497            } else {
498                mSubjectAndDescription = sSubjectEmptyDescription;
499            }
500        }
501        return mSubjectAndDescription;
502    }
503}
504