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