MessageListItem.java revision 9a0457146d9caa476ca8c44c950dce741ede8143
1/*
2 * Copyright (C) 2008 Esmertec AG.
3 * Copyright (C) 2008 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mms.ui;
19
20import java.util.Map;
21import java.util.regex.Matcher;
22import java.util.regex.Pattern;
23
24import android.app.AlertDialog;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.content.res.Resources;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.graphics.Canvas;
32import android.graphics.Paint;
33import android.graphics.Path;
34import android.graphics.Typeface;
35import android.graphics.Paint.FontMetricsInt;
36import android.graphics.drawable.Drawable;
37import android.net.Uri;
38import android.os.Handler;
39import android.os.Message;
40import android.provider.Browser;
41import android.provider.ContactsContract.Profile;
42import android.provider.Telephony.Mms;
43import android.provider.Telephony.Sms;
44import android.telephony.PhoneNumberUtils;
45import android.text.Html;
46import android.text.SpannableStringBuilder;
47import android.text.TextUtils;
48import android.text.method.HideReturnsTransformationMethod;
49import android.text.style.ForegroundColorSpan;
50import android.text.style.LineHeightSpan;
51import android.text.style.StyleSpan;
52import android.text.style.TextAppearanceSpan;
53import android.text.style.URLSpan;
54import android.util.AttributeSet;
55import android.util.Log;
56import android.view.Gravity;
57import android.view.View;
58import android.view.ViewGroup;
59import android.view.View.OnClickListener;
60import android.widget.ArrayAdapter;
61import android.widget.Button;
62import android.widget.ImageButton;
63import android.widget.ImageView;
64import android.widget.LinearLayout;
65import android.widget.QuickContactBadge;
66import android.widget.RelativeLayout;
67import android.widget.TextView;
68
69import com.android.mms.MmsApp;
70import com.android.mms.R;
71import com.android.mms.data.Contact;
72import com.android.mms.data.WorkingMessage;
73import com.android.mms.transaction.Transaction;
74import com.android.mms.transaction.TransactionBundle;
75import com.android.mms.transaction.TransactionService;
76import com.android.mms.util.DownloadManager;
77import com.android.mms.util.SmileyParser;
78import com.google.android.mms.ContentType;
79import com.google.android.mms.pdu.PduHeaders;
80
81/**
82 * This class provides view of a message in the messages list.
83 */
84public class MessageListItem extends LinearLayout implements
85        SlideViewInterface, OnClickListener {
86    public static final String EXTRA_URLS = "com.android.mms.ExtraUrls";
87
88    private static final String TAG = "MessageListItem";
89    private static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
90
91    static final int MSG_LIST_EDIT_MMS   = 1;
92    static final int MSG_LIST_EDIT_SMS   = 2;
93
94    private View mMmsView;
95    private ImageView mImageView;
96    private ImageView mLockedIndicator;
97    private ImageView mDeliveredIndicator;
98    private ImageView mDetailsIndicator;
99    private ImageButton mSlideShowButton;
100    private TextView mBodyTextView;
101    private Button mDownloadButton;
102    private TextView mDownloadingLabel;
103    private Handler mHandler;
104    private MessageItem mMessageItem;
105    private String mDefaultCountryIso;
106    private TextView mDateView;
107    public View mMessageBlock;
108    private Path mPath = new Path();
109    private Paint mPaint = new Paint();
110    private QuickContactDivot mAvatar;
111    private boolean mIsLastItemInList;
112    static private Drawable sDefaultContactImage;
113
114    public MessageListItem(Context context) {
115        super(context);
116        mDefaultCountryIso = MmsApp.getApplication().getCurrentCountryIso();
117
118        if (sDefaultContactImage == null) {
119            sDefaultContactImage = context.getResources().getDrawable(R.drawable.ic_contact_picture);
120        }
121    }
122
123    public MessageListItem(Context context, AttributeSet attrs) {
124        super(context, attrs);
125
126        int color = mContext.getResources().getColor(R.color.timestamp_color);
127        mColorSpan = new ForegroundColorSpan(color);
128        mDefaultCountryIso = MmsApp.getApplication().getCurrentCountryIso();
129
130        if (sDefaultContactImage == null) {
131            sDefaultContactImage = context.getResources().getDrawable(R.drawable.ic_contact_picture);
132        }
133    }
134
135    @Override
136    protected void onFinishInflate() {
137        super.onFinishInflate();
138
139        mBodyTextView = (TextView) findViewById(R.id.text_view);
140        mDateView = (TextView) findViewById(R.id.date_view);
141        mLockedIndicator = (ImageView) findViewById(R.id.locked_indicator);
142        mDeliveredIndicator = (ImageView) findViewById(R.id.delivered_indicator);
143        mDetailsIndicator = (ImageView) findViewById(R.id.details_indicator);
144        mAvatar = (QuickContactDivot) findViewById(R.id.avatar);
145        mMessageBlock = findViewById(R.id.message_block);
146    }
147
148    public void bind(MessageItem msgItem, boolean isLastItem) {
149        mMessageItem = msgItem;
150        mIsLastItemInList = isLastItem;
151
152        setLongClickable(false);
153
154        switch (msgItem.mMessageType) {
155            case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
156                bindNotifInd(msgItem);
157                break;
158            default:
159                bindCommonMessage(msgItem);
160                break;
161        }
162    }
163
164    public void unbind() {
165        // Clear all references to the message item, which can contain attachments and other
166        // memory-intensive objects
167        mMessageItem = null;
168        if (mImageView != null) {
169            // Because #setOnClickListener may have set the listener to an object that has the
170            // message item in its closure.
171            mImageView.setOnClickListener(null);
172        }
173        if (mSlideShowButton != null) {
174            // Because #drawPlaybackButton sets the tag to mMessageItem
175            mSlideShowButton.setTag(null);
176        }
177    }
178
179    public MessageItem getMessageItem() {
180        return mMessageItem;
181    }
182
183    public void setMsgListItemHandler(Handler handler) {
184        mHandler = handler;
185    }
186
187    private void bindNotifInd(final MessageItem msgItem) {
188        hideMmsViewIfNeeded();
189
190        String msgSizeText = mContext.getString(R.string.message_size_label)
191                                + String.valueOf((msgItem.mMessageSize + 1023) / 1024)
192                                + mContext.getString(R.string.kilobyte);
193
194        mBodyTextView.setText(formatMessage(msgItem, msgItem.mContact, null, msgItem.mSubject,
195                                            msgItem.mHighlight, msgItem.mTextContentType));
196
197        mDateView.setText(msgSizeText + " " + msgItem.mTimestamp);
198
199        int state = DownloadManager.getInstance().getState(msgItem.mMessageUri);
200        switch (state) {
201            case DownloadManager.STATE_DOWNLOADING:
202                inflateDownloadControls();
203                mDownloadingLabel.setVisibility(View.VISIBLE);
204                mDownloadButton.setVisibility(View.GONE);
205                break;
206            case DownloadManager.STATE_UNSTARTED:
207            case DownloadManager.STATE_TRANSIENT_FAILURE:
208            case DownloadManager.STATE_PERMANENT_FAILURE:
209            default:
210                setLongClickable(true);
211                inflateDownloadControls();
212                mDownloadingLabel.setVisibility(View.GONE);
213                mDownloadButton.setVisibility(View.VISIBLE);
214                mDownloadButton.setOnClickListener(new OnClickListener() {
215                    public void onClick(View v) {
216                        mDownloadingLabel.setVisibility(View.VISIBLE);
217                        mDownloadButton.setVisibility(View.GONE);
218                        Intent intent = new Intent(mContext, TransactionService.class);
219                        intent.putExtra(TransactionBundle.URI, msgItem.mMessageUri.toString());
220                        intent.putExtra(TransactionBundle.TRANSACTION_TYPE,
221                                Transaction.RETRIEVE_TRANSACTION);
222                        mContext.startService(intent);
223                    }
224                });
225                break;
226        }
227
228        // Hide the indicators.
229        mLockedIndicator.setVisibility(View.GONE);
230        mDeliveredIndicator.setVisibility(View.GONE);
231        mDetailsIndicator.setVisibility(View.GONE);
232    }
233
234    private void updateAvatarView(String addr, boolean isSelf) {
235        Drawable avatarDrawable;
236        if (isSelf || !TextUtils.isEmpty(addr)) {
237            Contact contact = isSelf ? Contact.getMe(false) : Contact.get(addr, false);
238            avatarDrawable = contact.getAvatar(mContext, sDefaultContactImage);
239
240            if (isSelf) {
241                mAvatar.assignContactUri(Profile.CONTENT_URI);
242            } else {
243                if (contact.existsInDatabase()) {
244                    mAvatar.assignContactUri(contact.getUri());
245                } else {
246                    mAvatar.assignContactFromPhone(contact.getNumber(), true);
247                }
248            }
249        } else {
250            avatarDrawable = sDefaultContactImage;
251        }
252        mAvatar.setImageDrawable(avatarDrawable);
253    }
254
255    private void bindCommonMessage(final MessageItem msgItem) {
256        if (mDownloadButton != null) {
257            mDownloadButton.setVisibility(View.GONE);
258            mDownloadingLabel.setVisibility(View.GONE);
259        }
260        // Since the message text should be concatenated with the sender's
261        // address(or name), I have to display it here instead of
262        // displaying it by the Presenter.
263        mBodyTextView.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
264
265        boolean isSelf = Sms.isOutgoingFolder(msgItem.mBoxId);
266        String addr = isSelf ? null : msgItem.mAddress;
267        updateAvatarView(addr, isSelf);
268
269        // Get and/or lazily set the formatted message from/on the
270        // MessageItem.  Because the MessageItem instances come from a
271        // cache (currently of size ~50), the hit rate on avoiding the
272        // expensive formatMessage() call is very high.
273        CharSequence formattedMessage = msgItem.getCachedFormattedMessage();
274        if (formattedMessage == null) {
275            formattedMessage = formatMessage(msgItem, msgItem.mContact, msgItem.mBody,
276                                             msgItem.mSubject,
277                                             msgItem.mHighlight, msgItem.mTextContentType);
278        }
279        mBodyTextView.setText(formattedMessage);
280
281        // If we're in the process of sending a message (i.e. pending), then we show a "SENDING..."
282        // string in place of the timestamp.
283        mDateView.setText(msgItem.isSending() ?
284                mContext.getResources().getString(R.string.sending_message) :
285                    msgItem.mTimestamp);
286
287        if (msgItem.isSms()) {
288            hideMmsViewIfNeeded();
289        } else {
290            Presenter presenter = PresenterFactory.getPresenter(
291                    "MmsThumbnailPresenter", mContext,
292                    this, msgItem.mSlideshow);
293            presenter.present();
294
295            if (msgItem.mAttachmentType != WorkingMessage.TEXT) {
296                inflateMmsView();
297                mMmsView.setVisibility(View.VISIBLE);
298                setOnClickListener(msgItem);
299                drawPlaybackButton(msgItem);
300            } else {
301                hideMmsViewIfNeeded();
302            }
303        }
304        drawRightStatusIndicator(msgItem);
305
306        requestLayout();
307    }
308
309    private void hideMmsViewIfNeeded() {
310        if (mMmsView != null) {
311            mMmsView.setVisibility(View.GONE);
312        }
313    }
314
315    public void startAudio() {
316        // TODO Auto-generated method stub
317    }
318
319    public void startVideo() {
320        // TODO Auto-generated method stub
321    }
322
323    public void setAudio(Uri audio, String name, Map<String, ?> extras) {
324        // TODO Auto-generated method stub
325    }
326
327    public void setImage(String name, Bitmap bitmap) {
328        inflateMmsView();
329
330        try {
331            if (null == bitmap) {
332                bitmap = BitmapFactory.decodeResource(getResources(),
333                        R.drawable.ic_missing_thumbnail_picture);
334            }
335            mImageView.setImageBitmap(bitmap);
336            mImageView.setVisibility(VISIBLE);
337        } catch (java.lang.OutOfMemoryError e) {
338            Log.e(TAG, "setImage: out of memory: ", e);
339        }
340    }
341
342    private void inflateMmsView() {
343        if (mMmsView == null) {
344            //inflate the surrounding view_stub
345            findViewById(R.id.mms_layout_view_stub).setVisibility(VISIBLE);
346
347            mMmsView = findViewById(R.id.mms_view);
348            mImageView = (ImageView) findViewById(R.id.image_view);
349            mSlideShowButton = (ImageButton) findViewById(R.id.play_slideshow_button);
350        }
351    }
352
353    private void inflateDownloadControls() {
354        if (mDownloadButton == null) {
355            //inflate the download controls
356            findViewById(R.id.mms_downloading_view_stub).setVisibility(VISIBLE);
357            mDownloadButton = (Button) findViewById(R.id.btn_download_msg);
358            mDownloadingLabel = (TextView) findViewById(R.id.label_downloading);
359        }
360    }
361
362
363    private LineHeightSpan mSpan = new LineHeightSpan() {
364        public void chooseHeight(CharSequence text, int start,
365                int end, int spanstartv, int v, FontMetricsInt fm) {
366            fm.ascent -= 10;
367        }
368    };
369
370    TextAppearanceSpan mTextSmallSpan =
371        new TextAppearanceSpan(mContext, android.R.style.TextAppearance_Small);
372
373    ForegroundColorSpan mColorSpan = null;  // set in ctor
374
375    private CharSequence formatMessage(MessageItem msgItem, String contact, String body,
376                                       String subject, Pattern highlight,
377                                       String contentType) {
378        SpannableStringBuilder buf = new SpannableStringBuilder();
379
380        boolean hasSubject = !TextUtils.isEmpty(subject);
381        if (hasSubject) {
382            buf.append(mContext.getResources().getString(R.string.inline_subject, subject));
383        }
384
385        if (!TextUtils.isEmpty(body)) {
386            // Converts html to spannable if ContentType is "text/html".
387            if (contentType != null && ContentType.TEXT_HTML.equals(contentType)) {
388                buf.append("\n");
389                buf.append(Html.fromHtml(body));
390            } else {
391                if (hasSubject) {
392                    buf.append(" - ");
393                }
394                SmileyParser parser = SmileyParser.getInstance();
395                buf.append(parser.addSmileySpans(body));
396            }
397        }
398
399        if (highlight != null) {
400            Matcher m = highlight.matcher(buf.toString());
401            while (m.find()) {
402                buf.setSpan(new StyleSpan(Typeface.BOLD), m.start(), m.end(), 0);
403            }
404        }
405        return buf;
406    }
407
408    private void drawPlaybackButton(MessageItem msgItem) {
409        switch (msgItem.mAttachmentType) {
410            case WorkingMessage.SLIDESHOW:
411            case WorkingMessage.AUDIO:
412            case WorkingMessage.VIDEO:
413                // Show the 'Play' button and bind message info on it.
414                mSlideShowButton.setTag(msgItem);
415                // Set call-back for the 'Play' button.
416                mSlideShowButton.setOnClickListener(this);
417                mSlideShowButton.setVisibility(View.VISIBLE);
418                setLongClickable(true);
419
420                // When we show the mSlideShowButton, this list item's onItemClickListener doesn't
421                // get called. (It gets set in ComposeMessageActivity:
422                // mMsgListView.setOnItemClickListener) Here we explicitly set the item's
423                // onClickListener. It allows the item to respond to embedded html links and at the
424                // same time, allows the slide show play button to work.
425                setOnClickListener(new OnClickListener() {
426                    public void onClick(View v) {
427                        onMessageListItemClick();
428                    }
429                });
430                break;
431            default:
432                mSlideShowButton.setVisibility(View.GONE);
433                break;
434        }
435    }
436
437    // OnClick Listener for the playback button
438    public void onClick(View v) {
439        MessageItem mi = (MessageItem) v.getTag();
440        switch (mi.mAttachmentType) {
441            case WorkingMessage.VIDEO:
442            case WorkingMessage.AUDIO:
443            case WorkingMessage.SLIDESHOW:
444                MessageUtils.viewMmsMessageAttachment(mContext, mi.mMessageUri, mi.mSlideshow);
445                break;
446        }
447    }
448
449    public void onMessageListItemClick() {
450        URLSpan[] spans = mBodyTextView.getUrls();
451
452        if (spans.length == 0) {
453            // Do nothing.
454        } else if (spans.length == 1) {
455            Uri uri = Uri.parse(spans[0].getURL());
456            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
457            intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
458            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
459            mContext.startActivity(intent);
460        } else {
461            final java.util.ArrayList<String> urls = MessageUtils.extractUris(spans);
462
463            ArrayAdapter<String> adapter =
464                new ArrayAdapter<String>(mContext, android.R.layout.select_dialog_item, urls) {
465                public View getView(int position, View convertView, ViewGroup parent) {
466                    View v = super.getView(position, convertView, parent);
467                    try {
468                        String url = getItem(position).toString();
469                        TextView tv = (TextView) v;
470                        Drawable d = mContext.getPackageManager().getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
471                        if (d != null) {
472                            d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight());
473                            tv.setCompoundDrawablePadding(10);
474                            tv.setCompoundDrawables(d, null, null, null);
475                        }
476                        final String telPrefix = "tel:";
477                        if (url.startsWith(telPrefix)) {
478                            url = PhoneNumberUtils.formatNumber(
479                                            url.substring(telPrefix.length()), mDefaultCountryIso);
480                        }
481                        tv.setText(url);
482                    } catch (android.content.pm.PackageManager.NameNotFoundException ex) {
483                        ;
484                    }
485                    return v;
486                }
487            };
488
489            AlertDialog.Builder b = new AlertDialog.Builder(mContext);
490
491            DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() {
492                public final void onClick(DialogInterface dialog, int which) {
493                    if (which >= 0) {
494                        Uri uri = Uri.parse(urls.get(which));
495                        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
496                        intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
497                        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
498                        mContext.startActivity(intent);
499                    }
500                    dialog.dismiss();
501                }
502            };
503
504            b.setTitle(R.string.select_link_title);
505            b.setCancelable(true);
506            b.setAdapter(adapter, click);
507
508            b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
509                public final void onClick(DialogInterface dialog, int which) {
510                    dialog.dismiss();
511                }
512            });
513
514            b.show();
515        }
516    }
517
518
519    private void setOnClickListener(final MessageItem msgItem) {
520        switch(msgItem.mAttachmentType) {
521        case WorkingMessage.IMAGE:
522        case WorkingMessage.VIDEO:
523            mImageView.setOnClickListener(new OnClickListener() {
524                public void onClick(View v) {
525                    MessageUtils.viewMmsMessageAttachment(mContext, null, msgItem.mSlideshow);
526                }
527            });
528            mImageView.setOnLongClickListener(new OnLongClickListener() {
529                public boolean onLongClick(View v) {
530                    return v.showContextMenu();
531                }
532            });
533            break;
534
535        default:
536            mImageView.setOnClickListener(null);
537            break;
538        }
539    }
540
541    private void setErrorIndicatorClickListener(final MessageItem msgItem) {
542        String type = msgItem.mType;
543        final int what;
544        if (type.equals("sms")) {
545            what = MSG_LIST_EDIT_SMS;
546        } else {
547            what = MSG_LIST_EDIT_MMS;
548        }
549        mDeliveredIndicator.setOnClickListener(new OnClickListener() {
550            public void onClick(View v) {
551                if (null != mHandler) {
552                    Message msg = Message.obtain(mHandler, what);
553                    msg.obj = new Long(msgItem.mMsgId);
554                    msg.sendToTarget();
555                }
556            }
557        });
558    }
559
560    private void drawRightStatusIndicator(MessageItem msgItem) {
561        // Locked icon
562        if (msgItem.mLocked) {
563            mLockedIndicator.setImageResource(R.drawable.ic_lock_message_sms);
564            mLockedIndicator.setVisibility(View.VISIBLE);
565        } else {
566            mLockedIndicator.setVisibility(View.GONE);
567        }
568
569        // Delivery icon
570        if (msgItem.isOutgoingMessage() && msgItem.isFailedMessage()) {
571            mDeliveredIndicator.setImageResource(R.drawable.ic_list_alert_sms_failed);
572            setErrorIndicatorClickListener(msgItem);
573            mDeliveredIndicator.setVisibility(View.VISIBLE);
574        } else if (msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.FAILED) {
575            mDeliveredIndicator.setImageResource(R.drawable.ic_list_alert_sms_failed);
576            mDeliveredIndicator.setVisibility(View.VISIBLE);
577        } else if (msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.RECEIVED) {
578            mDeliveredIndicator.setImageResource(R.drawable.ic_sms_mms_delivered);
579            mDeliveredIndicator.setVisibility(View.VISIBLE);
580        } else {
581            mDeliveredIndicator.setVisibility(View.GONE);
582        }
583
584        // Message details icon
585        if (msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.INFO || msgItem.mReadReport) {
586            mDetailsIndicator.setImageResource(R.drawable.ic_sms_mms_details);
587            mDetailsIndicator.setVisibility(View.VISIBLE);
588        } else {
589            mDetailsIndicator.setVisibility(View.GONE);
590        }
591    }
592
593    public void setImageRegionFit(String fit) {
594        // TODO Auto-generated method stub
595    }
596
597    public void setImageVisibility(boolean visible) {
598        // TODO Auto-generated method stub
599    }
600
601    public void setText(String name, String text) {
602        // TODO Auto-generated method stub
603    }
604
605    public void setTextVisibility(boolean visible) {
606        // TODO Auto-generated method stub
607    }
608
609    public void setVideo(String name, Uri video) {
610        inflateMmsView();
611
612        try {
613            Bitmap bitmap = VideoAttachmentView.createVideoThumbnail(mContext, video);
614            if (null == bitmap) {
615                bitmap = BitmapFactory.decodeResource(getResources(),
616                        R.drawable.ic_missing_thumbnail_video);
617            }
618            mImageView.setImageBitmap(bitmap);
619            mImageView.setVisibility(VISIBLE);
620        } catch (java.lang.OutOfMemoryError e) {
621            Log.e(TAG, "setVideo: out of memory: ", e);
622        }
623    }
624
625    public void setVideoVisibility(boolean visible) {
626        // TODO Auto-generated method stub
627    }
628
629    public void stopAudio() {
630        // TODO Auto-generated method stub
631    }
632
633    public void stopVideo() {
634        // TODO Auto-generated method stub
635    }
636
637    public void reset() {
638        if (mImageView != null) {
639            mImageView.setVisibility(GONE);
640        }
641    }
642
643    public void setVisibility(boolean visible) {
644        // TODO Auto-generated method stub
645    }
646
647    public void pauseAudio() {
648        // TODO Auto-generated method stub
649
650    }
651
652    public void pauseVideo() {
653        // TODO Auto-generated method stub
654
655    }
656
657    public void seekAudio(int seekTo) {
658        // TODO Auto-generated method stub
659
660    }
661
662    public void seekVideo(int seekTo) {
663        // TODO Auto-generated method stub
664
665    }
666
667    /**
668     * Override dispatchDraw so that we can put our own background and border in.
669     * This is all complexity to support a shared border from one item to the next.
670     */
671    @Override
672    public void dispatchDraw(Canvas c) {
673        View v = mMessageBlock;
674        if (v != null) {
675            float l = v.getX();
676            float t = v.getY();
677            float r = v.getX() + v.getWidth();
678            float b = v.getY() + v.getHeight();
679
680            Path path = mPath;
681            path.reset();
682
683            // This block of code draws our own background but omits the top pixel so that
684            // if the previous item draws it's border there we don't overwrite it.
685            path.moveTo(l, t + 1);
686            path.lineTo(r, t + 1);
687            path.lineTo(r, b);
688            path.lineTo(l, b);
689            path.close();
690
691            Paint paint = mPaint;
692            paint.setStyle(Paint.Style.FILL);
693            paint.setColor(0xffffffff);
694            c.drawPath(path, paint);
695
696            super.dispatchDraw(c);
697
698            path.reset();
699
700            r -= 1;
701
702
703            // This block of code draws the border around the "message block" section
704            // of the layout.  This would normally be a simple rectangle but we omit
705            // the border at the point of the avatar's divot.  Also, the bottom is drawn
706            // 1 pixel below our own bounds to get it to line up with the border of
707            // the next item.
708            //
709            // But for the last item we draw the bottom in our own bounds -- so it will
710            // show up.
711            if (mIsLastItemInList) {
712                b -= 1;
713            }
714            if (mAvatar.getPosition() == Divot.RIGHT_UPPER) {
715                path.moveTo(l, t + mAvatar.getCloseOffset());
716                path.lineTo(l, t);
717                path.lineTo(r, t);
718                path.lineTo(r, b);
719                path.lineTo(l, b);
720                path.lineTo(l, t + mAvatar.getFarOffset());
721            } else if (mAvatar.getPosition() == Divot.LEFT_UPPER) {
722                path.moveTo(r, t + mAvatar.getCloseOffset());
723                path.lineTo(r, t);
724                path.lineTo(l, t);
725                path.lineTo(l, b);
726                path.lineTo(r, b);
727                path.lineTo(r, t + mAvatar.getFarOffset());
728            }
729
730//            paint.setColor(0xff00ff00);
731            paint.setColor(0xffcccccc);
732            paint.setStrokeWidth(1F);
733            paint.setStyle(Paint.Style.STROKE);
734            c.drawPath(path, paint);
735        } else {
736            super.dispatchDraw(c);
737        }
738    }
739}
740