• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.email.activity;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Typeface;
27 import android.graphics.drawable.Drawable;
28 import android.text.Layout.Alignment;
29 import android.text.Spannable;
30 import android.text.SpannableString;
31 import android.text.SpannableStringBuilder;
32 import android.text.StaticLayout;
33 import android.text.TextPaint;
34 import android.text.TextUtils;
35 import android.text.TextUtils.TruncateAt;
36 import android.text.format.DateUtils;
37 import android.text.style.ForegroundColorSpan;
38 import android.text.style.StyleSpan;
39 import android.util.AttributeSet;
40 import android.view.MotionEvent;
41 import android.view.View;
42 import android.view.accessibility.AccessibilityEvent;
43 
44 import com.android.email.R;
45 import com.android.emailcommon.utility.TextUtilities;
46 import com.google.common.base.Objects;
47 
48 /**
49  * This custom View is the list item for the MessageList activity, and serves two purposes:
50  * 1.  It's a container to store message metadata (e.g. the ids of the message, mailbox, & account)
51  * 2.  It handles internal clicks such as the checkbox or the favorite star
52  */
53 public class MessageListItem extends View {
54     // Note: messagesAdapter directly fiddles with these fields.
55     /* package */ long mMessageId;
56     /* package */ long mMailboxId;
57     /* package */ long mAccountId;
58 
59     private ThreePaneLayout mLayout;
60     private MessagesAdapter mAdapter;
61     private MessageListItemCoordinates mCoordinates;
62     private Context mContext;
63     private boolean mIsSearchResult = false;
64 
65     private boolean mDownEvent;
66 
67     public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL =
68         "com.android.email.MESSAGE_LIST_ITEMS";
69 
MessageListItem(Context context)70     public MessageListItem(Context context) {
71         super(context);
72         init(context);
73     }
74 
MessageListItem(Context context, AttributeSet attrs)75     public MessageListItem(Context context, AttributeSet attrs) {
76         super(context, attrs);
77         init(context);
78     }
79 
MessageListItem(Context context, AttributeSet attrs, int defStyle)80     public MessageListItem(Context context, AttributeSet attrs, int defStyle) {
81         super(context, attrs, defStyle);
82         init(context);
83     }
84 
85     // Wide mode shows sender, snippet, time, and favorite spread out across the screen
86     private static final int MODE_WIDE = MessageListItemCoordinates.WIDE_MODE;
87     // Sentinel indicating that the view needs layout
88     public static final int NEEDS_LAYOUT = -1;
89 
90     private static boolean sInit = false;
91     private static final TextPaint sDefaultPaint = new TextPaint();
92     private static final TextPaint sBoldPaint = new TextPaint();
93     private static final TextPaint sDatePaint = new TextPaint();
94     private static Bitmap sAttachmentIcon;
95     private static Bitmap sInviteIcon;
96     private static int sBadgeMargin;
97     private static Bitmap sFavoriteIconOff;
98     private static Bitmap sFavoriteIconOn;
99     private static Bitmap sSelectedIconOn;
100     private static Bitmap sSelectedIconOff;
101     private static Bitmap sStateReplied;
102     private static Bitmap sStateForwarded;
103     private static Bitmap sStateRepliedAndForwarded;
104     private static String sSubjectSnippetDivider;
105     private static String sSubjectDescription;
106     private static String sSubjectEmptyDescription;
107 
108     // Static colors.
109     private static int DEFAULT_TEXT_COLOR;
110     private static int ACTIVATED_TEXT_COLOR;
111     private static int LIGHT_TEXT_COLOR;
112     private static int DRAFT_TEXT_COLOR;
113     private static int SUBJECT_TEXT_COLOR_READ;
114     private static int SUBJECT_TEXT_COLOR_UNREAD;
115     private static int SNIPPET_TEXT_COLOR_READ;
116     private static int SNIPPET_TEXT_COLOR_UNREAD;
117     private static int SENDERS_TEXT_COLOR_READ;
118     private static int SENDERS_TEXT_COLOR_UNREAD;
119     private static int DATE_TEXT_COLOR_READ;
120     private static int DATE_TEXT_COLOR_UNREAD;
121 
122     public String mSender;
123     public SpannableStringBuilder mText;
124     public CharSequence mSnippet;
125     private String mSubject;
126     private StaticLayout mSubjectLayout;
127     public boolean mRead;
128     public boolean mHasAttachment = false;
129     public boolean mHasInvite = true;
130     public boolean mIsFavorite = false;
131     public boolean mHasBeenRepliedTo = false;
132     public boolean mHasBeenForwarded = false;
133     /** {@link Paint} for account color chips.  null if no chips should be drawn.  */
134     public Paint mColorChipPaint;
135 
136     private int mMode = -1;
137 
138     private int mViewWidth = 0;
139     private int mViewHeight = 0;
140 
141     private static int sItemHeightWide;
142     private static int sItemHeightNormal;
143 
144     // Note: these cannot be shared Drawables because they are selectors which have state.
145     private Drawable mReadSelector;
146     private Drawable mUnreadSelector;
147     private Drawable mWideReadSelector;
148     private Drawable mWideUnreadSelector;
149 
150     private CharSequence mFormattedSender;
151     // We must initialize this to something, in case the timestamp of the message is zero (which
152     // should be very rare); this is otherwise set in setTimestamp
153     private CharSequence mFormattedDate = "";
154 
init(Context context)155     private void init(Context context) {
156         mContext = context;
157         if (!sInit) {
158             Resources r = context.getResources();
159             sSubjectDescription = r.getString(R.string.message_subject_description).concat(", ");
160             sSubjectEmptyDescription = r.getString(R.string.message_is_empty_description);
161             sSubjectSnippetDivider = r.getString(R.string.message_list_subject_snippet_divider);
162             sItemHeightWide =
163                 r.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
164             sItemHeightNormal =
165                 r.getDimensionPixelSize(R.dimen.message_list_item_height_normal);
166 
167             sDefaultPaint.setTypeface(Typeface.DEFAULT);
168             sDefaultPaint.setAntiAlias(true);
169             sDatePaint.setTypeface(Typeface.DEFAULT);
170             sDatePaint.setAntiAlias(true);
171             sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
172             sBoldPaint.setAntiAlias(true);
173 
174             sAttachmentIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_attachment);
175             sInviteIcon = BitmapFactory.decodeResource(r, R.drawable.ic_badge_invite_holo_light);
176             sBadgeMargin = r.getDimensionPixelSize(R.dimen.message_list_badge_margin);
177             sFavoriteIconOff =
178                 BitmapFactory.decodeResource(r, R.drawable.btn_star_off_normal_email_holo_light);
179             sFavoriteIconOn =
180                 BitmapFactory.decodeResource(r, R.drawable.btn_star_on_normal_email_holo_light);
181             sSelectedIconOff =
182                 BitmapFactory.decodeResource(r, R.drawable.btn_check_off_normal_holo_light);
183             sSelectedIconOn =
184                 BitmapFactory.decodeResource(r, R.drawable.btn_check_on_normal_holo_light);
185 
186             sStateReplied =
187                 BitmapFactory.decodeResource(r, R.drawable.ic_badge_reply_holo_light);
188             sStateForwarded =
189                 BitmapFactory.decodeResource(r, R.drawable.ic_badge_forward_holo_light);
190             sStateRepliedAndForwarded =
191                 BitmapFactory.decodeResource(r, R.drawable.ic_badge_reply_forward_holo_light);
192 
193             DEFAULT_TEXT_COLOR = r.getColor(R.color.default_text_color);
194             ACTIVATED_TEXT_COLOR = r.getColor(android.R.color.white);
195             SUBJECT_TEXT_COLOR_READ = r.getColor(R.color.subject_text_color_read);
196             SUBJECT_TEXT_COLOR_UNREAD = r.getColor(R.color.subject_text_color_unread);
197             SNIPPET_TEXT_COLOR_READ = r.getColor(R.color.snippet_text_color_read);
198             SNIPPET_TEXT_COLOR_UNREAD = r.getColor(R.color.snippet_text_color_unread);
199             SENDERS_TEXT_COLOR_READ = r.getColor(R.color.senders_text_color_read);
200             SENDERS_TEXT_COLOR_UNREAD = r.getColor(R.color.senders_text_color_unread);
201             DATE_TEXT_COLOR_READ = r.getColor(R.color.date_text_color_read);
202             DATE_TEXT_COLOR_UNREAD = r.getColor(R.color.date_text_color_unread);
203 
204             sInit = true;
205         }
206     }
207 
208     /**
209      * Invalidate all drawing caches associated with drawing message list items.
210      * This is an expensive operation, and should be done rarely, such as when system font size
211      * changes occurs.
212      */
resetDrawingCaches()213     public static void resetDrawingCaches() {
214         MessageListItemCoordinates.resetCaches();
215         sInit = false;
216     }
217 
218     /**
219      * Sets message subject and snippet safely, ensuring the cache is invalidated.
220      */
setText(String subject, String snippet, boolean forceUpdate)221     public void setText(String subject, String snippet, boolean forceUpdate) {
222         boolean changed = false;
223         if (!Objects.equal(mSubject, subject)) {
224             mSubject = subject;
225             changed = true;
226             populateContentDescription();
227         }
228 
229         if (!Objects.equal(mSnippet, snippet)) {
230             mSnippet = snippet;
231             changed = true;
232         }
233 
234         if (forceUpdate || changed || (mSubject == null && mSnippet == null) /* first time */) {
235             SpannableStringBuilder ssb = new SpannableStringBuilder();
236             boolean hasSubject = false;
237             if (!TextUtils.isEmpty(mSubject)) {
238                 SpannableString ss = new SpannableString(mSubject);
239                 ss.setSpan(new StyleSpan(mRead ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
240                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
241                 ssb.append(ss);
242                 hasSubject = true;
243             }
244             if (!TextUtils.isEmpty(mSnippet)) {
245                 if (hasSubject) {
246                     ssb.append(sSubjectSnippetDivider);
247                 }
248                 ssb.append(mSnippet);
249             }
250             mText = ssb;
251             requestLayout();
252         }
253     }
254 
255     long mTimeFormatted = 0;
256 
setTimestamp(long timestamp)257     public void setTimestamp(long timestamp) {
258         if (mTimeFormatted != timestamp) {
259             mFormattedDate = DateUtils.getRelativeTimeSpanString(mContext, timestamp).toString();
260             mTimeFormatted = timestamp;
261         }
262     }
263 
264     /**
265      * Determine the mode of this view (WIDE or NORMAL)
266      *
267      * @param width The width of the view
268      * @return The mode of the view
269      */
getViewMode(int width)270     private int getViewMode(int width) {
271         return MessageListItemCoordinates.getMode(mContext, width, mIsSearchResult);
272     }
273 
274     private Drawable mCurentBackground = null; // Only used by updateBackground()
275 
updateBackground()276     private void updateBackground() {
277         final Drawable newBackground;
278         boolean isMultiPane = MessageListItemCoordinates.isMultiPane(mContext);
279         if (mRead) {
280             if (isMultiPane && mLayout.isLeftPaneVisible()) {
281                 if (mWideReadSelector == null) {
282                     mWideReadSelector = getContext().getResources()
283                             .getDrawable(R.drawable.conversation_wide_read_selector);
284                 }
285                 newBackground = mWideReadSelector;
286             } else {
287                 if (mReadSelector == null) {
288                     mReadSelector = getContext().getResources()
289                             .getDrawable(R.drawable.conversation_read_selector);
290                 }
291                 newBackground = mReadSelector;
292             }
293         } else {
294             if (isMultiPane && mLayout.isLeftPaneVisible()) {
295                 if (mWideUnreadSelector == null) {
296                     mWideUnreadSelector = getContext().getResources().getDrawable(
297                             R.drawable.conversation_wide_unread_selector);
298                 }
299                 newBackground = mWideUnreadSelector;
300             } else {
301                 if (mUnreadSelector == null) {
302                     mUnreadSelector = getContext().getResources()
303                             .getDrawable(R.drawable.conversation_unread_selector);
304                 }
305                 newBackground = mUnreadSelector;
306             }
307         }
308         if (newBackground != mCurentBackground) {
309             // setBackgroundDrawable is a heavy operation.  Only call it when really needed.
310             setBackgroundDrawable(newBackground);
311             mCurentBackground = newBackground;
312         }
313     }
314 
calculateSubjectText()315     private void calculateSubjectText() {
316         if (mText == null || mText.length() == 0) {
317             return;
318         }
319         boolean hasSubject = false;
320         int snippetStart = 0;
321         if (!TextUtils.isEmpty(mSubject)) {
322             int subjectColor = getFontColor(mRead ? SUBJECT_TEXT_COLOR_READ
323                     : SUBJECT_TEXT_COLOR_UNREAD);
324             mText.setSpan(new ForegroundColorSpan(subjectColor), 0, mSubject.length(),
325                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
326             snippetStart = mSubject.length() + 1;
327         }
328         if (!TextUtils.isEmpty(mSnippet)) {
329             int snippetColor = getFontColor(mRead ? SNIPPET_TEXT_COLOR_READ
330                     : SNIPPET_TEXT_COLOR_UNREAD);
331             mText.setSpan(new ForegroundColorSpan(snippetColor), snippetStart, mText.length(),
332                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
333         }
334     }
335 
calculateDrawingData()336     private void calculateDrawingData() {
337         sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
338         calculateSubjectText();
339         mSubjectLayout = new StaticLayout(mText, sDefaultPaint,
340                 mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, false /* includePad */);
341         if (mCoordinates.subjectLineCount < mSubjectLayout.getLineCount()) {
342             // TODO: ellipsize.
343             int end = mSubjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
344             mSubjectLayout = new StaticLayout(mText.subSequence(0, end),
345                     sDefaultPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
346         }
347 
348         // Now, format the sender for its width
349         TextPaint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
350         // And get the ellipsized string for the calculated width
351         if (TextUtils.isEmpty(mSender)) {
352             mFormattedSender = "";
353         } else {
354             int senderWidth = mCoordinates.sendersWidth;
355             senderPaint.setTextSize(mCoordinates.sendersFontSize);
356             senderPaint.setColor(getFontColor(mRead ? SENDERS_TEXT_COLOR_READ
357                     : SENDERS_TEXT_COLOR_UNREAD));
358             mFormattedSender = TextUtils.ellipsize(mSender, senderPaint, senderWidth,
359                     TruncateAt.END);
360         }
361     }
362     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)363     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
364         if (widthMeasureSpec != 0 || mViewWidth == 0) {
365             mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
366             int mode = getViewMode(mViewWidth);
367             if (mode != mMode) {
368                 mMode = mode;
369             }
370             mViewHeight = measureHeight(heightMeasureSpec, mMode);
371         }
372         setMeasuredDimension(mViewWidth, mViewHeight);
373     }
374 
375     /**
376      * Determine the height of this view
377      *
378      * @param measureSpec A measureSpec packed into an int
379      * @param mode The current mode of this view
380      * @return The height of the view, honoring constraints from measureSpec
381      */
measureHeight(int measureSpec, int mode)382     private int measureHeight(int measureSpec, int mode) {
383         int result = 0;
384         int specMode = MeasureSpec.getMode(measureSpec);
385         int specSize = MeasureSpec.getSize(measureSpec);
386 
387         if (specMode == MeasureSpec.EXACTLY) {
388             // We were told how big to be
389             result = specSize;
390         } else {
391             // Measure the text
392             if (mMode == MODE_WIDE) {
393                 result = sItemHeightWide;
394             } else {
395                 result = sItemHeightNormal;
396             }
397             if (specMode == MeasureSpec.AT_MOST) {
398                 // Respect AT_MOST value if that was what is called for by
399                 // measureSpec
400                 result = Math.min(result, specSize);
401             }
402         }
403         return result;
404     }
405 
406     @Override
draw(Canvas canvas)407     public void draw(Canvas canvas) {
408         // Update the background, before View.draw() draws it.
409         setSelected(mAdapter.isSelected(this));
410         updateBackground();
411         super.draw(canvas);
412     }
413 
414     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)415     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
416         super.onLayout(changed, left, top, right, bottom);
417 
418         mCoordinates = MessageListItemCoordinates.forWidth(mContext, mViewWidth, mIsSearchResult);
419         calculateDrawingData();
420     }
421 
getFontColor(int defaultColor)422     private int getFontColor(int defaultColor) {
423         return isActivated() && MessageListItemCoordinates.isMultiPane(mContext) ?
424                 ACTIVATED_TEXT_COLOR : defaultColor;
425     }
426 
427     @Override
onDraw(Canvas canvas)428     protected void onDraw(Canvas canvas) {
429         // Draw the color chip indicating the mailbox this belongs to
430         if (mColorChipPaint != null) {
431             canvas.drawRect(
432                     mCoordinates.chipX, mCoordinates.chipY,
433                     mCoordinates.chipX + mCoordinates.chipWidth,
434                     mCoordinates.chipY + mCoordinates.chipHeight,
435                     mColorChipPaint);
436         }
437 
438         // Draw the checkbox
439         canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn : sSelectedIconOff,
440                 mCoordinates.checkmarkX, mCoordinates.checkmarkY, null);
441 
442         // Draw the sender name
443         Paint senderPaint = mRead ? sDefaultPaint : sBoldPaint;
444         senderPaint.setColor(getFontColor(mRead ? SENDERS_TEXT_COLOR_READ
445                 : SENDERS_TEXT_COLOR_UNREAD));
446         senderPaint.setTextSize(mCoordinates.sendersFontSize);
447         canvas.drawText(mFormattedSender, 0, mFormattedSender.length(),
448                 mCoordinates.sendersX, mCoordinates.sendersY - mCoordinates.sendersAscent,
449                 senderPaint);
450 
451         // Draw the reply state. Draw nothing if neither replied nor forwarded.
452         if (mHasBeenRepliedTo && mHasBeenForwarded) {
453             canvas.drawBitmap(sStateRepliedAndForwarded,
454                     mCoordinates.stateX, mCoordinates.stateY, null);
455         } else if (mHasBeenRepliedTo) {
456             canvas.drawBitmap(sStateReplied,
457                     mCoordinates.stateX, mCoordinates.stateY, null);
458         } else if (mHasBeenForwarded) {
459             canvas.drawBitmap(sStateForwarded,
460                     mCoordinates.stateX, mCoordinates.stateY, null);
461         }
462 
463         // Subject and snippet.
464         sDefaultPaint.setTextSize(mCoordinates.subjectFontSize);
465         canvas.save();
466         canvas.translate(
467                 mCoordinates.subjectX,
468                 mCoordinates.subjectY);
469         mSubjectLayout.draw(canvas);
470         canvas.restore();
471 
472         // Draw the date
473         sDatePaint.setTextSize(mCoordinates.dateFontSize);
474         sDatePaint.setColor(mRead ? DATE_TEXT_COLOR_READ : DATE_TEXT_COLOR_UNREAD);
475         int dateX = mCoordinates.dateXEnd
476                 - (int) sDatePaint.measureText(mFormattedDate, 0, mFormattedDate.length());
477 
478         canvas.drawText(mFormattedDate, 0, mFormattedDate.length(),
479                 dateX, mCoordinates.dateY - mCoordinates.dateAscent, sDatePaint);
480 
481         // Draw the favorite icon
482         canvas.drawBitmap(mIsFavorite ? sFavoriteIconOn : sFavoriteIconOff,
483                 mCoordinates.starX, mCoordinates.starY, null);
484 
485         // TODO: deal with the icon layouts better from the coordinate class so that this logic
486         // doesn't have to exist.
487         // Draw the attachment and invite icons, if necessary.
488         int iconsLeft = dateX - sBadgeMargin;
489         if (mHasAttachment) {
490             iconsLeft = iconsLeft - sAttachmentIcon.getWidth();
491             canvas.drawBitmap(sAttachmentIcon, iconsLeft, mCoordinates.paperclipY, null);
492         }
493         if (mHasInvite) {
494             iconsLeft -= sInviteIcon.getWidth();
495             canvas.drawBitmap(sInviteIcon, iconsLeft, mCoordinates.paperclipY, null);
496         }
497 
498     }
499 
500     /**
501      * Called by the adapter at bindView() time
502      *
503      * @param adapter the adapter that creates this view
504      * @param layout If this is a three pane implementation, the
505      *            ThreePaneLayout. Otherwise, null.
506      */
bindViewInit(MessagesAdapter adapter, ThreePaneLayout layout, boolean isSearchResult)507     public void bindViewInit(MessagesAdapter adapter, ThreePaneLayout layout,
508             boolean isSearchResult) {
509         mLayout = layout;
510         mAdapter = adapter;
511         mIsSearchResult = isSearchResult;
512         requestLayout();
513     }
514 
515     private static final int TOUCH_SLOP = 24;
516     private static int sScaledTouchSlop = -1;
517 
initializeSlop(Context context)518     private void initializeSlop(Context context) {
519         if (sScaledTouchSlop == -1) {
520             final Resources res = context.getResources();
521             final Configuration config = res.getConfiguration();
522             final float density = res.getDisplayMetrics().density;
523             final float sizeAndDensity;
524             if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) {
525                 sizeAndDensity = density * 1.5f;
526             } else {
527                 sizeAndDensity = density;
528             }
529             sScaledTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f);
530         }
531     }
532 
533     /**
534      * Overriding this method allows us to "catch" clicks in the checkbox or star
535      * and process them accordingly.
536      */
537     @Override
onTouchEvent(MotionEvent event)538     public boolean onTouchEvent(MotionEvent event) {
539         initializeSlop(getContext());
540 
541         boolean handled = false;
542         int touchX = (int) event.getX();
543         int checkRight = mCoordinates.checkmarkX
544                 + mCoordinates.checkmarkWidthIncludingMargins + sScaledTouchSlop;
545         int starLeft = mCoordinates.starX - sScaledTouchSlop;
546 
547         switch (event.getAction()) {
548             case MotionEvent.ACTION_DOWN:
549                 if (touchX < checkRight || touchX > starLeft) {
550                     mDownEvent = true;
551                     if ((touchX < checkRight) || (touchX > starLeft)) {
552                         handled = true;
553                     }
554                 }
555                 break;
556 
557             case MotionEvent.ACTION_CANCEL:
558                 mDownEvent = false;
559                 break;
560 
561             case MotionEvent.ACTION_UP:
562                 if (mDownEvent) {
563                     if (touchX < checkRight) {
564                         mAdapter.toggleSelected(this);
565                         handled = true;
566                     } else if (touchX > starLeft) {
567                         mIsFavorite = !mIsFavorite;
568                         mAdapter.updateFavorite(this, mIsFavorite);
569                         handled = true;
570                     }
571                 }
572                 break;
573         }
574 
575         if (handled) {
576             invalidate();
577         } else {
578             handled = super.onTouchEvent(event);
579         }
580 
581         return handled;
582     }
583 
584     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)585     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
586         event.setClassName(getClass().getName());
587         event.setPackageName(getContext().getPackageName());
588         event.setEnabled(true);
589         event.setContentDescription(getContentDescription());
590         return true;
591     }
592 
593     /**
594      * Sets the content description for this item, used for accessibility.
595      */
populateContentDescription()596     private void populateContentDescription() {
597         if (!TextUtils.isEmpty(mSubject)) {
598             setContentDescription(sSubjectDescription + mSubject);
599         } else {
600             setContentDescription(sSubjectEmptyDescription);
601         }
602     }
603 }
604