Home | History | Annotate | Download | only in activity
      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 
     70     public MessageListItem(Context context) {
     71         super(context);
     72         init(context);
     73     }
     74 
     75     public MessageListItem(Context context, AttributeSet attrs) {
     76         super(context, attrs);
     77         init(context);
     78     }
     79 
     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 
    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      */
    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      */
    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 
    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      */
    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 
    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 
    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 
    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
    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      */
    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
    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
    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 
    422     private int getFontColor(int defaultColor) {
    423         return isActivated() && MessageListItemCoordinates.isMultiPane(mContext) ?
    424                 ACTIVATED_TEXT_COLOR : defaultColor;
    425     }
    426 
    427     @Override
    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      */
    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 
    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
    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
    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      */
    596     private void populateContentDescription() {
    597         if (!TextUtils.isEmpty(mSubject)) {
    598             setContentDescription(sSubjectDescription + mSubject);
    599         } else {
    600             setContentDescription(sSubjectEmptyDescription);
    601         }
    602     }
    603 }
    604