MessageListItem.java revision ca1f651ece39b2b415519ca49b9bb5275b6a64bf
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.Typeface;
32import android.graphics.Paint.FontMetricsInt;
33import android.graphics.drawable.Drawable;
34import android.net.Uri;
35import android.os.Handler;
36import android.os.Message;
37import android.provider.Browser;
38import android.provider.Telephony.Mms;
39import android.provider.Telephony.Sms;
40import android.telephony.PhoneNumberUtils;
41import android.text.Html;
42import android.text.SpannableStringBuilder;
43import android.text.TextUtils;
44import android.text.method.HideReturnsTransformationMethod;
45import android.text.style.ForegroundColorSpan;
46import android.text.style.LineHeightSpan;
47import android.text.style.StyleSpan;
48import android.text.style.TextAppearanceSpan;
49import android.text.style.URLSpan;
50import android.util.AttributeSet;
51import android.util.Log;
52import android.view.Gravity;
53import android.view.View;
54import android.view.ViewGroup;
55import android.view.View.OnClickListener;
56import android.widget.ArrayAdapter;
57import android.widget.Button;
58import android.widget.ImageButton;
59import android.widget.ImageView;
60import android.widget.LinearLayout;
61import android.widget.QuickContactBadge;
62import android.widget.RelativeLayout;
63import android.widget.TextView;
64
65import com.android.mms.MmsApp;
66import com.android.mms.R;
67import com.android.mms.data.Contact;
68import com.android.mms.data.WorkingMessage;
69import com.android.mms.transaction.Transaction;
70import com.android.mms.transaction.TransactionBundle;
71import com.android.mms.transaction.TransactionService;
72import com.android.mms.util.DownloadManager;
73import com.android.mms.util.SmileyParser;
74import com.google.android.mms.ContentType;
75import com.google.android.mms.pdu.PduHeaders;
76
77/**
78 * This class provides view of a message in the messages list.
79 */
80public class MessageListItem extends LinearLayout implements
81        SlideViewInterface, OnClickListener {
82    public static final String EXTRA_URLS = "com.android.mms.ExtraUrls";
83
84    private static final String TAG = "MessageListItem";
85    private static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
86
87    static final int MSG_LIST_EDIT_MMS   = 1;
88    static final int MSG_LIST_EDIT_SMS   = 2;
89
90    private View mMmsView;
91    private ImageView mImageView;
92    private ImageView mLockedIndicator;
93    private ImageView mDeliveredIndicator;
94    private ImageView mDetailsIndicator;
95    private ImageButton mSlideShowButton;
96    private TextView mBodyTextView;
97    private Button mDownloadButton;
98    private TextView mDownloadingLabel;
99    private QuickContactBadge mAvatar;
100    private Handler mHandler;
101    private MessageItem mMessageItem;
102    private String mDefaultCountryIso;
103    private TextView mDateView;
104    private ImageViewDivot mDivot;        // little triangle on the side of the avatar
105    static private Drawable sDefaultContactImage;
106
107    public MessageListItem(Context context) {
108        super(context);
109        mDefaultCountryIso = MmsApp.getApplication().getCurrentCountryIso();
110
111        if (sDefaultContactImage == null) {
112            sDefaultContactImage = context.getResources().getDrawable(R.drawable.ic_contact_picture);
113        }
114    }
115
116    public MessageListItem(Context context, AttributeSet attrs) {
117        super(context, attrs);
118
119        int color = mContext.getResources().getColor(R.color.timestamp_color);
120        mColorSpan = new ForegroundColorSpan(color);
121        mDefaultCountryIso = MmsApp.getApplication().getCurrentCountryIso();
122
123        if (sDefaultContactImage == null) {
124            sDefaultContactImage = context.getResources().getDrawable(R.drawable.ic_contact_picture);
125        }
126    }
127
128    @Override
129    protected void onFinishInflate() {
130        super.onFinishInflate();
131
132        mBodyTextView = (TextView) findViewById(R.id.text_view);
133        mDateView = (TextView) findViewById(R.id.date_view);
134        mLockedIndicator = (ImageView) findViewById(R.id.locked_indicator);
135        mDeliveredIndicator = (ImageView) findViewById(R.id.delivered_indicator);
136        mDetailsIndicator = (ImageView) findViewById(R.id.details_indicator);
137        mAvatar = (QuickContactBadge) findViewById(R.id.avatar);
138        mDivot = (ImageViewDivot) findViewById(R.id.divit);
139    }
140
141    public void bind(MessageItem msgItem) {
142        mMessageItem = msgItem;
143
144        setLongClickable(false);
145
146        switch (msgItem.mMessageType) {
147            case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
148                bindNotifInd(msgItem);
149                break;
150            default:
151                bindCommonMessage(msgItem);
152                break;
153        }
154    }
155
156    public MessageItem getMessageItem() {
157        return mMessageItem;
158    }
159
160    public void setMsgListItemHandler(Handler handler) {
161        mHandler = handler;
162    }
163
164    private void bindNotifInd(final MessageItem msgItem) {
165        hideMmsViewIfNeeded();
166
167        String msgSizeText = mContext.getString(R.string.message_size_label)
168                                + String.valueOf((msgItem.mMessageSize + 1023) / 1024)
169                                + mContext.getString(R.string.kilobyte);
170
171        mBodyTextView.setText(formatMessage(msgItem, msgItem.mContact, null, msgItem.mSubject,
172                                            msgItem.mHighlight, msgItem.mTextContentType));
173
174        mDateView.setText(msgSizeText + " " + msgItem.mTimestamp);
175
176        int state = DownloadManager.getInstance().getState(msgItem.mMessageUri);
177        switch (state) {
178            case DownloadManager.STATE_DOWNLOADING:
179                inflateDownloadControls();
180                mDownloadingLabel.setVisibility(View.VISIBLE);
181                mDownloadButton.setVisibility(View.GONE);
182                break;
183            case DownloadManager.STATE_UNSTARTED:
184            case DownloadManager.STATE_TRANSIENT_FAILURE:
185            case DownloadManager.STATE_PERMANENT_FAILURE:
186            default:
187                setLongClickable(true);
188                inflateDownloadControls();
189                mDownloadingLabel.setVisibility(View.GONE);
190                mDownloadButton.setVisibility(View.VISIBLE);
191                mDownloadButton.setOnClickListener(new OnClickListener() {
192                    public void onClick(View v) {
193                        mDownloadingLabel.setVisibility(View.VISIBLE);
194                        mDownloadButton.setVisibility(View.GONE);
195                        Intent intent = new Intent(mContext, TransactionService.class);
196                        intent.putExtra(TransactionBundle.URI, msgItem.mMessageUri.toString());
197                        intent.putExtra(TransactionBundle.TRANSACTION_TYPE,
198                                Transaction.RETRIEVE_TRANSACTION);
199                        mContext.startService(intent);
200                    }
201                });
202                break;
203        }
204
205        // Hide the indicators.
206        mLockedIndicator.setVisibility(View.GONE);
207        mDeliveredIndicator.setVisibility(View.GONE);
208        mDetailsIndicator.setVisibility(View.GONE);
209    }
210
211    private void updateAvatarView(String addr) {
212        Drawable avatarDrawable;
213        if (!TextUtils.isEmpty(addr)) {
214            Contact contact = Contact.get(addr, false);
215            avatarDrawable = contact.getAvatar(mContext, sDefaultContactImage);
216
217            if (contact.existsInDatabase()) {
218                mAvatar.assignContactUri(contact.getUri());
219            } else {
220                mAvatar.assignContactFromPhone(contact.getNumber(), true);
221            }
222        } else {
223            avatarDrawable = sDefaultContactImage;
224        }
225        mAvatar.setImageDrawable(avatarDrawable);
226    }
227
228    private void bindCommonMessage(final MessageItem msgItem) {
229        if (mDownloadButton != null) {
230            mDownloadButton.setVisibility(View.GONE);
231            mDownloadingLabel.setVisibility(View.GONE);
232        }
233        // Since the message text should be concatenated with the sender's
234        // address(or name), I have to display it here instead of
235        // displaying it by the Presenter.
236        mBodyTextView.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
237
238        String addr = null;
239        if (!Sms.isOutgoingFolder(msgItem.mBoxId)) {
240            addr = msgItem.mAddress;
241        } else {
242            addr = MmsApp.getApplication().getTelephonyManager().getLine1Number();
243        }
244        updateAvatarView(addr);
245
246        // Get and/or lazily set the formatted message from/on the
247        // MessageItem.  Because the MessageItem instances come from a
248        // cache (currently of size ~50), the hit rate on avoiding the
249        // expensive formatMessage() call is very high.
250        CharSequence formattedMessage = msgItem.getCachedFormattedMessage();
251        if (formattedMessage == null) {
252            formattedMessage = formatMessage(msgItem, msgItem.mContact, msgItem.mBody,
253                                             msgItem.mSubject,
254                                             msgItem.mHighlight, msgItem.mTextContentType);
255        }
256        mBodyTextView.setText(formattedMessage);
257
258        // If we're in the process of sending a message (i.e. pending), then we show a "SENDING..."
259        // string in place of the timestamp.
260        mDateView.setText(msgItem.isSending() ?
261                mContext.getResources().getString(R.string.sending_message) :
262                    msgItem.mTimestamp);
263
264        if (msgItem.isSms()) {
265            hideMmsViewIfNeeded();
266        } else {
267            Presenter presenter = PresenterFactory.getPresenter(
268                    "MmsThumbnailPresenter", mContext,
269                    this, msgItem.mSlideshow);
270            presenter.present();
271
272            if (msgItem.mAttachmentType != WorkingMessage.TEXT) {
273                inflateMmsView();
274                mMmsView.setVisibility(View.VISIBLE);
275                setOnClickListener(msgItem);
276                drawPlaybackButton(msgItem);
277            } else {
278                hideMmsViewIfNeeded();
279            }
280        }
281        drawRightStatusIndicator(msgItem);
282
283        requestLayout();
284    }
285
286    private void hideMmsViewIfNeeded() {
287        if (mMmsView != null) {
288            mMmsView.setVisibility(View.GONE);
289        }
290    }
291
292    public void startAudio() {
293        // TODO Auto-generated method stub
294    }
295
296    public void startVideo() {
297        // TODO Auto-generated method stub
298    }
299
300    public void setAudio(Uri audio, String name, Map<String, ?> extras) {
301        // TODO Auto-generated method stub
302    }
303
304    public void setImage(String name, Bitmap bitmap) {
305        inflateMmsView();
306
307        try {
308            if (null == bitmap) {
309                bitmap = BitmapFactory.decodeResource(getResources(),
310                        R.drawable.ic_missing_thumbnail_picture);
311            }
312            mImageView.setImageBitmap(bitmap);
313            mImageView.setVisibility(VISIBLE);
314        } catch (java.lang.OutOfMemoryError e) {
315            Log.e(TAG, "setImage: out of memory: ", e);
316        }
317    }
318
319    private void inflateMmsView() {
320        if (mMmsView == null) {
321            //inflate the surrounding view_stub
322            findViewById(R.id.mms_layout_view_stub).setVisibility(VISIBLE);
323
324            mMmsView = findViewById(R.id.mms_view);
325            mImageView = (ImageView) findViewById(R.id.image_view);
326            mSlideShowButton = (ImageButton) findViewById(R.id.play_slideshow_button);
327        }
328    }
329
330    private void inflateDownloadControls() {
331        if (mDownloadButton == null) {
332            //inflate the download controls
333            findViewById(R.id.mms_downloading_view_stub).setVisibility(VISIBLE);
334            mDownloadButton = (Button) findViewById(R.id.btn_download_msg);
335            mDownloadingLabel = (TextView) findViewById(R.id.label_downloading);
336        }
337    }
338
339
340    private LineHeightSpan mSpan = new LineHeightSpan() {
341        public void chooseHeight(CharSequence text, int start,
342                int end, int spanstartv, int v, FontMetricsInt fm) {
343            fm.ascent -= 10;
344        }
345    };
346
347    TextAppearanceSpan mTextSmallSpan =
348        new TextAppearanceSpan(mContext, android.R.style.TextAppearance_Small);
349
350    ForegroundColorSpan mColorSpan = null;  // set in ctor
351
352    private CharSequence formatMessage(MessageItem msgItem, String contact, String body,
353                                       String subject, Pattern highlight,
354                                       String contentType) {
355        SpannableStringBuilder buf = new SpannableStringBuilder();
356
357        boolean hasSubject = !TextUtils.isEmpty(subject);
358        if (hasSubject) {
359            buf.append(mContext.getResources().getString(R.string.inline_subject, subject));
360        }
361
362        if (!TextUtils.isEmpty(body)) {
363            // Converts html to spannable if ContentType is "text/html".
364            if (contentType != null && ContentType.TEXT_HTML.equals(contentType)) {
365                buf.append("\n");
366                buf.append(Html.fromHtml(body));
367            } else {
368                if (hasSubject) {
369                    buf.append(" - ");
370                }
371                SmileyParser parser = SmileyParser.getInstance();
372                buf.append(parser.addSmileySpans(body));
373            }
374        }
375
376        if (highlight != null) {
377            Matcher m = highlight.matcher(buf.toString());
378            while (m.find()) {
379                buf.setSpan(new StyleSpan(Typeface.BOLD), m.start(), m.end(), 0);
380            }
381        }
382        return buf;
383    }
384
385    private void drawPlaybackButton(MessageItem msgItem) {
386        switch (msgItem.mAttachmentType) {
387            case WorkingMessage.SLIDESHOW:
388            case WorkingMessage.AUDIO:
389            case WorkingMessage.VIDEO:
390                // Show the 'Play' button and bind message info on it.
391                mSlideShowButton.setTag(msgItem);
392                // Set call-back for the 'Play' button.
393                mSlideShowButton.setOnClickListener(this);
394                mSlideShowButton.setVisibility(View.VISIBLE);
395                setLongClickable(true);
396
397                // When we show the mSlideShowButton, this list item's onItemClickListener doesn't
398                // get called. (It gets set in ComposeMessageActivity:
399                // mMsgListView.setOnItemClickListener) Here we explicitly set the item's
400                // onClickListener. It allows the item to respond to embedded html links and at the
401                // same time, allows the slide show play button to work.
402                setOnClickListener(new OnClickListener() {
403                    public void onClick(View v) {
404                        onMessageListItemClick();
405                    }
406                });
407                break;
408            default:
409                mSlideShowButton.setVisibility(View.GONE);
410                break;
411        }
412    }
413
414    // OnClick Listener for the playback button
415    public void onClick(View v) {
416        MessageItem mi = (MessageItem) v.getTag();
417        switch (mi.mAttachmentType) {
418            case WorkingMessage.VIDEO:
419            case WorkingMessage.AUDIO:
420            case WorkingMessage.SLIDESHOW:
421                MessageUtils.viewMmsMessageAttachment(mContext, mi.mMessageUri, mi.mSlideshow);
422                break;
423        }
424    }
425
426    public void onMessageListItemClick() {
427        URLSpan[] spans = mBodyTextView.getUrls();
428
429        if (spans.length == 0) {
430            // Do nothing.
431        } else if (spans.length == 1) {
432            Uri uri = Uri.parse(spans[0].getURL());
433            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
434            intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
435            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
436            mContext.startActivity(intent);
437        } else {
438            final java.util.ArrayList<String> urls = MessageUtils.extractUris(spans);
439
440            ArrayAdapter<String> adapter =
441                new ArrayAdapter<String>(mContext, android.R.layout.select_dialog_item, urls) {
442                public View getView(int position, View convertView, ViewGroup parent) {
443                    View v = super.getView(position, convertView, parent);
444                    try {
445                        String url = getItem(position).toString();
446                        TextView tv = (TextView) v;
447                        Drawable d = mContext.getPackageManager().getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
448                        if (d != null) {
449                            d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight());
450                            tv.setCompoundDrawablePadding(10);
451                            tv.setCompoundDrawables(d, null, null, null);
452                        }
453                        final String telPrefix = "tel:";
454                        if (url.startsWith(telPrefix)) {
455                            url = PhoneNumberUtils.formatNumber(
456                                            url.substring(telPrefix.length()), mDefaultCountryIso);
457                        }
458                        tv.setText(url);
459                    } catch (android.content.pm.PackageManager.NameNotFoundException ex) {
460                        ;
461                    }
462                    return v;
463                }
464            };
465
466            AlertDialog.Builder b = new AlertDialog.Builder(mContext);
467
468            DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() {
469                public final void onClick(DialogInterface dialog, int which) {
470                    if (which >= 0) {
471                        Uri uri = Uri.parse(urls.get(which));
472                        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
473                        intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
474                        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
475                        mContext.startActivity(intent);
476                    }
477                    dialog.dismiss();
478                }
479            };
480
481            b.setTitle(R.string.select_link_title);
482            b.setCancelable(true);
483            b.setAdapter(adapter, click);
484
485            b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
486                public final void onClick(DialogInterface dialog, int which) {
487                    dialog.dismiss();
488                }
489            });
490
491            b.show();
492        }
493    }
494
495
496    private void setOnClickListener(final MessageItem msgItem) {
497        switch(msgItem.mAttachmentType) {
498        case WorkingMessage.IMAGE:
499        case WorkingMessage.VIDEO:
500            mImageView.setOnClickListener(new OnClickListener() {
501                public void onClick(View v) {
502                    MessageUtils.viewMmsMessageAttachment(mContext, null, msgItem.mSlideshow);
503                }
504            });
505            mImageView.setOnLongClickListener(new OnLongClickListener() {
506                public boolean onLongClick(View v) {
507                    return v.showContextMenu();
508                }
509            });
510            break;
511
512        default:
513            mImageView.setOnClickListener(null);
514            break;
515        }
516    }
517
518    private void setErrorIndicatorClickListener(final MessageItem msgItem) {
519        String type = msgItem.mType;
520        final int what;
521        if (type.equals("sms")) {
522            what = MSG_LIST_EDIT_SMS;
523        } else {
524            what = MSG_LIST_EDIT_MMS;
525        }
526        mDeliveredIndicator.setOnClickListener(new OnClickListener() {
527            public void onClick(View v) {
528                if (null != mHandler) {
529                    Message msg = Message.obtain(mHandler, what);
530                    msg.obj = new Long(msgItem.mMsgId);
531                    msg.sendToTarget();
532                }
533            }
534        });
535    }
536
537    private void drawRightStatusIndicator(MessageItem msgItem) {
538        // Locked icon
539        if (msgItem.mLocked) {
540            mLockedIndicator.setImageResource(R.drawable.ic_lock_message_sms);
541            mLockedIndicator.setVisibility(View.VISIBLE);
542        } else {
543            mLockedIndicator.setVisibility(View.GONE);
544        }
545
546        // Delivery icon
547        if (msgItem.isOutgoingMessage() && msgItem.isFailedMessage()) {
548            mDeliveredIndicator.setImageResource(R.drawable.ic_list_alert_sms_failed);
549            setErrorIndicatorClickListener(msgItem);
550            mDeliveredIndicator.setVisibility(View.VISIBLE);
551        } else if (msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.FAILED) {
552            mDeliveredIndicator.setImageResource(R.drawable.ic_list_alert_sms_failed);
553            mDeliveredIndicator.setVisibility(View.VISIBLE);
554        } else if (msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.RECEIVED) {
555            mDeliveredIndicator.setImageResource(R.drawable.ic_sms_mms_delivered);
556            mDeliveredIndicator.setVisibility(View.VISIBLE);
557        } else {
558            mDeliveredIndicator.setVisibility(View.GONE);
559        }
560
561        // Message details icon
562        if (msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.INFO || msgItem.mReadReport) {
563            mDetailsIndicator.setImageResource(R.drawable.ic_sms_mms_details);
564            mDetailsIndicator.setVisibility(View.VISIBLE);
565        } else {
566            mDetailsIndicator.setVisibility(View.GONE);
567        }
568    }
569
570    public void setImageRegionFit(String fit) {
571        // TODO Auto-generated method stub
572    }
573
574    public void setImageVisibility(boolean visible) {
575        // TODO Auto-generated method stub
576    }
577
578    public void setText(String name, String text) {
579        // TODO Auto-generated method stub
580    }
581
582    public void setTextVisibility(boolean visible) {
583        // TODO Auto-generated method stub
584    }
585
586    public void setVideo(String name, Uri video) {
587        inflateMmsView();
588
589        try {
590            Bitmap bitmap = VideoAttachmentView.createVideoThumbnail(mContext, video);
591            if (null == bitmap) {
592                bitmap = BitmapFactory.decodeResource(getResources(),
593                        R.drawable.ic_missing_thumbnail_video);
594            }
595            mImageView.setImageBitmap(bitmap);
596            mImageView.setVisibility(VISIBLE);
597        } catch (java.lang.OutOfMemoryError e) {
598            Log.e(TAG, "setVideo: out of memory: ", e);
599        }
600    }
601
602    public void setVideoVisibility(boolean visible) {
603        // TODO Auto-generated method stub
604    }
605
606    public void stopAudio() {
607        // TODO Auto-generated method stub
608    }
609
610    public void stopVideo() {
611        // TODO Auto-generated method stub
612    }
613
614    public void reset() {
615        if (mImageView != null) {
616            mImageView.setVisibility(GONE);
617        }
618    }
619
620    public void setVisibility(boolean visible) {
621        // TODO Auto-generated method stub
622    }
623
624    public void pauseAudio() {
625        // TODO Auto-generated method stub
626
627    }
628
629    public void pauseVideo() {
630        // TODO Auto-generated method stub
631
632    }
633
634    public void seekAudio(int seekTo) {
635        // TODO Auto-generated method stub
636
637    }
638
639    public void seekVideo(int seekTo) {
640        // TODO Auto-generated method stub
641
642    }
643}
644