MessageListItem.java revision e82e62f94d4ba4ac139faf055f40fbbc2b99b551
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 com.android.mms.R;
21import com.android.mms.transaction.Transaction;
22import com.android.mms.transaction.TransactionBundle;
23import com.android.mms.transaction.TransactionService;
24import com.android.mms.util.DownloadManager;
25import com.google.android.mms.pdu.PduHeaders;
26
27import android.app.AlertDialog;
28import android.content.Context;
29import android.content.DialogInterface;
30import android.content.Intent;
31import android.graphics.Bitmap;
32import android.graphics.Canvas;
33import android.graphics.Paint;
34import android.graphics.Typeface;
35import android.graphics.drawable.Drawable;
36import android.media.MediaPlayer;
37import android.net.Uri;
38import android.os.Handler;
39import android.os.Message;
40import android.provider.Telephony.Mms;
41import android.provider.Telephony.MmsSms;
42import android.provider.Telephony.Sms;
43import android.telephony.PhoneNumberUtils;
44import android.text.Spannable;
45import android.text.SpannableStringBuilder;
46import android.text.TextUtils;
47import android.text.method.HideReturnsTransformationMethod;
48import android.text.style.AbsoluteSizeSpan;
49import android.text.style.BackgroundColorSpan;
50import android.text.style.ForegroundColorSpan;
51import android.text.style.LineBackgroundSpan;
52import android.text.style.StyleSpan;
53import android.text.style.URLSpan;
54import android.util.AttributeSet;
55import android.util.Log;
56import android.view.View;
57import android.view.ViewGroup;
58import android.view.View.OnClickListener;
59import android.widget.ArrayAdapter;
60import android.widget.Button;
61import android.widget.ImageButton;
62import android.widget.ImageView;
63import android.widget.LinearLayout;
64import android.widget.TextView;
65
66import java.io.IOException;
67import java.util.Map;
68
69import com.google.android.util.SmileyParser;
70import com.google.android.util.SmileyResources;
71
72/**
73 * This class provides view of a message in the messages list.
74 */
75public class MessageListItem extends LinearLayout implements
76        SlideViewInterface, OnClickListener {
77    public static final String EXTRA_URLS = "com.android.mms.ExtraUrls";
78
79    private static final String TAG = "MessageListItem";
80    private static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
81
82    static final int MSG_LIST_EDIT_MMS   = 1;
83    static final int MSG_LIST_EDIT_SMS   = 2;
84
85    private View mMsgListItem;
86    private View mMmsView;
87    private ImageView mImageView;
88    private ImageView mRightStatusIndicator;
89    private ImageButton mSlideShowButton;
90    private TextView mBodyTextView;
91    private Button mDownloadButton;
92    private TextView mDownloadingLabel;
93    private Handler mHandler;
94    private MessageItem mMessageItem;
95
96    public static final int[] DEFAULT_SMILEY_RES_IDS = {
97        R.drawable.emo_im_happy,
98        R.drawable.emo_im_sad,
99        R.drawable.emo_im_winking,
100        R.drawable.emo_im_tongue_sticking_out,
101        R.drawable.emo_im_surprised,
102        R.drawable.emo_im_kissing,
103        R.drawable.emo_im_yelling,
104        R.drawable.emo_im_cool,
105        R.drawable.emo_im_cool,
106        R.drawable.emo_im_money_mouth,
107        R.drawable.emo_im_foot_in_mouth,
108        R.drawable.emo_im_embarrased,
109        R.drawable.emo_im_angel,
110        R.drawable.emo_im_undecided,
111        R.drawable.emo_im_crying,
112        R.drawable.emo_im_lips_are_sealed,
113        R.drawable.emo_im_laughing,
114        R.drawable.emo_im_wtf
115    };
116
117    public static final int DEFAULT_SMILEY_TEXTS = R.array.default_smiley_texts;
118    public static final int DEFAULT_SMILEY_NAMES = R.array.default_smiley_names;
119
120    public MessageListItem(Context context) {
121        super(context);
122    }
123
124    public MessageListItem(Context context, AttributeSet attrs) {
125        super(context, attrs);
126    }
127
128    @Override
129    protected void onFinishInflate() {
130        super.onFinishInflate();
131
132        mMsgListItem = findViewById(R.id.msg_list_item);
133        mBodyTextView = (TextView) findViewById(R.id.text_view);
134        mRightStatusIndicator = (ImageView) findViewById(R.id.right_status_indicator);
135    }
136
137    public void bind(MessageItem msgItem) {
138        mMessageItem = msgItem;
139
140        setLongClickable(false);
141
142        switch (msgItem.mMessageType) {
143            case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
144                bindNotifInd(msgItem);
145                break;
146            default:
147                bindCommonMessage(msgItem);
148                break;
149        }
150    }
151
152    public MessageItem getMessageItem() {
153        return mMessageItem;
154    }
155
156    public void setMsgListItemHandler(Handler handler) {
157        mHandler = handler;
158    }
159
160    private void bindNotifInd(final MessageItem msgItem) {
161        hideMmsViewIfNeeded();
162
163        String msgSizeText = mContext.getString(R.string.message_size_label)
164                                + String.valueOf((msgItem.mMessageSize + 1023) / 1024)
165                                + mContext.getString(R.string.kilobyte);
166
167        boolean drawWithBackground = msgItem.mBoxId == Mms.MESSAGE_BOX_INBOX;
168
169        mBodyTextView.setText(formatMessage(msgItem.mContact, null, msgItem.mSubject,
170                                            msgSizeText + "\n" + msgItem.mTimestamp,
171                                            drawWithBackground));
172
173        int state = DownloadManager.getInstance().getState(msgItem.mMessageUri);
174        switch (state) {
175            case DownloadManager.STATE_DOWNLOADING:
176                inflateDownloadControls();
177                mDownloadingLabel.setVisibility(View.VISIBLE);
178                mDownloadButton.setVisibility(View.GONE);
179                break;
180            case DownloadManager.STATE_UNSTARTED:
181            case DownloadManager.STATE_TRANSIENT_FAILURE:
182            case DownloadManager.STATE_PERMANENT_FAILURE:
183            default:
184                setLongClickable(true);
185                inflateDownloadControls();
186                mDownloadingLabel.setVisibility(View.GONE);
187                mDownloadButton.setVisibility(View.VISIBLE);
188                mDownloadButton.setOnClickListener(new OnClickListener() {
189                    public void onClick(View v) {
190                        mDownloadingLabel.setVisibility(View.VISIBLE);
191                        mDownloadButton.setVisibility(View.GONE);
192                        Intent intent = new Intent(mContext, TransactionService.class);
193                        intent.putExtra(TransactionBundle.URI, msgItem.mMessageUri.toString());
194                        intent.putExtra(TransactionBundle.TRANSACTION_TYPE,
195                                Transaction.RETRIEVE_TRANSACTION);
196                        mContext.startService(intent);
197                    }
198                });
199                break;
200        }
201
202        // Hide the error indicator.
203        mRightStatusIndicator.setVisibility(View.GONE);
204
205        drawLeftStatusIndicator(msgItem.mBoxId);
206    }
207
208    private void bindCommonMessage(final MessageItem msgItem) {
209        if (mDownloadButton != null) {
210            mDownloadButton.setVisibility(View.GONE);
211            mDownloadingLabel.setVisibility(View.GONE);
212        }
213        // Since the message text should be concatenated with the sender's
214        // address(or name), I have to display it here instead of
215        // displaying it by the Presenter.
216        mBodyTextView.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
217
218        boolean drawWithBackground = msgItem.mBoxId == Mms.MESSAGE_BOX_INBOX;
219
220        mBodyTextView.setText(formatMessage(msgItem.mContact, msgItem.mBody,
221                                            msgItem.mSubject, msgItem.mTimestamp,
222                                            drawWithBackground));
223        // TODO part of changing contact names to links
224        //mBodyTextView.setText(formatMessage(msgItem.mAddress, msgItem.mBody));
225
226        if (msgItem.isSms()) {
227            hideMmsViewIfNeeded();
228        } else {
229            Presenter presenter = PresenterFactory.getPresenter(
230                    "MmsThumbnailPresenter", mContext,
231                    this, msgItem.mSlideshow);
232            presenter.present();
233
234            if (msgItem.mAttachmentType != AttachmentEditor.TEXT_ONLY) {
235                inflateMmsView();
236                mMmsView.setVisibility(View.VISIBLE);
237                setOnClickListener(msgItem);
238                drawPlaybackButton(msgItem);
239            } else {
240                hideMmsViewIfNeeded();
241            }
242        }
243
244        drawLeftStatusIndicator(msgItem.mBoxId);
245        drawRightStatusIndicator(msgItem);
246    }
247
248    private void hideMmsViewIfNeeded() {
249        if (mMmsView != null) {
250            mMmsView.setVisibility(View.GONE);
251        }
252    }
253
254    public void startAudio() {
255        // TODO Auto-generated method stub
256    }
257
258    public void startVideo() {
259        // TODO Auto-generated method stub
260    }
261
262    public void setAudio(Uri audio, String name, Map<String, ?> extras) {
263        // TODO Auto-generated method stub
264    }
265
266    public void setImage(String name, Bitmap bitmap) {
267        inflateMmsView();
268
269        mImageView.setImageBitmap(bitmap);
270        mImageView.setVisibility(VISIBLE);
271    }
272
273    private void inflateMmsView() {
274        if (mMmsView == null) {
275            //inflate the surrounding view_stub
276            findViewById(R.id.mms_layout_view_stub).setVisibility(VISIBLE);
277
278            mMmsView = findViewById(R.id.mms_view);
279            mImageView = (ImageView) findViewById(R.id.image_view);
280            mSlideShowButton = (ImageButton) findViewById(R.id.play_slideshow_button);
281        }
282    }
283
284    private void inflateDownloadControls() {
285        if (mDownloadButton == null) {
286            //inflate the download controls
287            findViewById(R.id.mms_downloading_view_stub).setVisibility(VISIBLE);
288            mDownloadButton = (Button) findViewById(R.id.btn_download_msg);
289            mDownloadingLabel = (TextView) findViewById(R.id.label_downloading);
290        }
291    }
292
293    private CharSequence formatMessage(String contact, String body, String subject,
294                                       String timestamp, boolean drawBackground) {
295        SpannableStringBuilder buf = new SpannableStringBuilder(contact);
296        buf.append(": ");
297        buf.setSpan(STYLE_BOLD, 0, buf.length(),
298                Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
299
300        boolean hasSubject = !TextUtils.isEmpty(subject);
301        if (hasSubject) {
302            buf.append(mContext.getResources().getString(R.string.inline_subject, subject));
303        }
304
305        if (!TextUtils.isEmpty(body)) {
306            if (hasSubject) {
307                buf.append(" - ");
308            }
309            SmileyResources smileyResources = new SmileyResources(
310                    getResources().getStringArray(DEFAULT_SMILEY_TEXTS), DEFAULT_SMILEY_RES_IDS);
311            SmileyParser smileyParser = new SmileyParser(body, smileyResources);
312            smileyParser.parse();
313            buf.append(smileyParser.getSpannableString(mContext));
314        }
315
316        buf.append("\n");
317        int startOffset = buf.length();
318
319        // put a one pixel high spacer line between the message and the time stamp as requested
320        // by the spec.
321        buf.append("\n");
322        buf.setSpan(new AbsoluteSizeSpan(3), startOffset, buf.length(),
323                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
324
325        startOffset = buf.length();
326        buf.append(timestamp);
327        buf.setSpan(new AbsoluteSizeSpan(12), startOffset, buf.length(),
328                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
329        // Make the timestamp text not as dark
330        int color = mContext.getResources().getColor(R.color.timestamp_color);
331        buf.setSpan(new ForegroundColorSpan(color), startOffset, buf.length(),
332                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
333
334        // For now, they've decided not to draw a darker background behind the timestamp.
335        // Keep the code for now.
336//        if (drawBackground) {
337//            int color = mContext.getResources().getColor(R.color.timestamp_color);
338//            buf.setSpan(new Background(color), startOffset, buf.length(),
339//                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
340//        }
341        return buf;
342    }
343
344    private static class Background implements LineBackgroundSpan {
345        private int mColor;
346
347        public Background(int color) {
348            mColor = color;
349        }
350
351        public void drawBackground(Canvas c, Paint p,
352                int left, int right,
353                int top, int baseline, int bottom,
354                CharSequence text, int start, int end,
355                int lnum) {
356            int col = p.getColor();
357            Paint.Style s = p.getStyle();
358
359            p.setColor(mColor);
360            p.setStyle(Paint.Style.FILL);
361            c.drawRect(left, top, right, bottom, p);
362
363            p.setColor(col);
364            p.setStyle(s);
365        }
366    }
367
368    private void drawPlaybackButton(MessageItem msgItem) {
369        switch (msgItem.mAttachmentType) {
370            case AttachmentEditor.SLIDESHOW_ATTACHMENT:
371            case AttachmentEditor.AUDIO_ATTACHMENT:
372            case AttachmentEditor.VIDEO_ATTACHMENT:
373                // Show the 'Play' button and bind message info on it.
374                mSlideShowButton.setTag(msgItem);
375                // Set call-back for the 'Play' button.
376                mSlideShowButton.setOnClickListener(this);
377                mSlideShowButton.setVisibility(View.VISIBLE);
378                setLongClickable(true);
379                break;
380            default:
381                mSlideShowButton.setVisibility(View.GONE);
382                break;
383        }
384    }
385
386    // OnClick Listener for the playback button
387    public void onClick(View v) {
388        MessageItem mi = (MessageItem) v.getTag();
389        switch (mi.mAttachmentType) {
390            case AttachmentEditor.AUDIO_ATTACHMENT:
391            case AttachmentEditor.VIDEO_ATTACHMENT:
392            case AttachmentEditor.SLIDESHOW_ATTACHMENT:
393                Intent intent = new Intent(
394                        mContext, SlideshowActivity.class);
395                intent.setData(mi.mMessageUri);
396                mContext.startActivity(intent);
397                break;
398        }
399    }
400
401    public void onMessageListItemClick() {
402        URLSpan[] spans = mBodyTextView.getUrls();
403
404        if (spans.length == 0) {
405            // Do nothing.
406        } else if (spans.length == 1) {
407            Uri uri = Uri.parse(spans[0].getURL());
408            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
409
410            mContext.startActivity(intent);
411        } else {
412            final java.util.ArrayList<String> urls = MessageUtils.extractUris(spans);
413
414            ArrayAdapter<String> adapter =
415                new ArrayAdapter<String>(mContext, android.R.layout.select_dialog_item, urls) {
416                public View getView(int position, View convertView, ViewGroup parent) {
417                    View v = super.getView(position, convertView, parent);
418                    try {
419                        String url = getItem(position).toString();
420                        TextView tv = (TextView) v;
421                        Drawable d = mContext.getPackageManager().getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
422                        if (d != null) {
423                            d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight());
424                            tv.setCompoundDrawablePadding(10);
425                            tv.setCompoundDrawables(d, null, null, null);
426                        }
427                        final String telPrefix = "tel:";
428                        if (url.startsWith(telPrefix)) {
429                            url = PhoneNumberUtils.formatNumber(url.substring(telPrefix.length()));
430                        }
431                        tv.setText(url);
432                    } catch (android.content.pm.PackageManager.NameNotFoundException ex) {
433                        ;
434                    }
435                    return v;
436                }
437            };
438
439            AlertDialog.Builder b = new AlertDialog.Builder(mContext);
440
441            DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() {
442                public final void onClick(DialogInterface dialog, int which) {
443                    if (which >= 0) {
444                        Uri uri = Uri.parse(urls.get(which));
445                        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
446                        intent.addCategory(Intent.CATEGORY_BROWSABLE);
447                        mContext.startActivity(intent);
448                    }
449                }
450            };
451
452            b.setTitle(R.string.select_link_title);
453            b.setCancelable(true);
454            b.setAdapter(adapter, click);
455
456            b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
457                public final void onClick(DialogInterface dialog, int which) {
458                    dialog.dismiss();
459                }
460            });
461
462            b.show();
463        }
464    }
465
466
467    private void setOnClickListener(final MessageItem msgItem) {
468        if (msgItem.mAttachmentType == AttachmentEditor.IMAGE_ATTACHMENT) {
469            mImageView.setOnClickListener(new OnClickListener() {
470                public void onClick(View v) {
471                    // FIXME: Use SlideshowActivity to view image for the time being.
472                    // As described in UI spec, pressing an inline attachment will
473                    // open up the full view of the attachment in its associated app
474                    // (here should the pictures app).
475                    // But the <ViewImage> would only show images in MediaStore.
476                    // Should we save a copy to MediaStore temporarily for displaying?
477                    Intent intent = new Intent(mContext, SlideshowActivity.class);
478                    intent.setData(msgItem.mMessageUri);
479                    mContext.startActivity(intent);
480                }
481            });
482        } else {
483            mImageView.setOnClickListener(null);
484        }
485    }
486
487    private void drawLeftStatusIndicator(int msgBoxId) {
488        switch (msgBoxId) {
489            case Mms.MESSAGE_BOX_INBOX:
490                mMsgListItem.setBackgroundResource(R.drawable.listitem_background_lightblue);
491                break;
492
493            case Mms.MESSAGE_BOX_DRAFTS:
494            case Sms.MESSAGE_TYPE_FAILED:
495            case Sms.MESSAGE_TYPE_QUEUED:
496            case Mms.MESSAGE_BOX_OUTBOX:
497                mMsgListItem.setBackgroundResource(R.drawable.listitem_background);
498                break;
499
500            default:
501                mMsgListItem.setBackgroundResource(R.drawable.listitem_background);
502                break;
503        }
504    }
505
506    private boolean isFailedMessage(MessageItem msgItem) {
507        boolean isFailedMms = msgItem.isMms()
508                            && (msgItem.mErrorType >= MmsSms.ERR_TYPE_GENERIC_PERMANENT);
509        boolean isFailedSms = msgItem.isSms()
510                            && (msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED);
511        return isFailedMms || isFailedSms;
512    }
513
514    private void setErrorIndicatorClickListener(final MessageItem msgItem) {
515        String type = msgItem.mType;
516        final int what;
517        if (type.equals("sms")) {
518            what = MSG_LIST_EDIT_SMS;
519        } else {
520            what = MSG_LIST_EDIT_MMS;
521        }
522        mRightStatusIndicator.setOnClickListener(new OnClickListener() {
523            public void onClick(View v) {
524                if (null != mHandler) {
525                    Message msg = Message.obtain(mHandler, what);
526                    msg.obj = new Long(msgItem.mMsgId);
527                    msg.sendToTarget();
528                }
529            }
530        });
531    }
532
533    private void drawRightStatusIndicator(MessageItem msgItem) {
534        if (msgItem.isOutgoingMessage()) {
535            if (isFailedMessage(msgItem)) {
536                mRightStatusIndicator.setImageResource(R.drawable.ic_sms_error);
537                setErrorIndicatorClickListener(msgItem);
538            } else {
539                mRightStatusIndicator.setImageResource(R.drawable.ic_email_pending);
540            }
541            mRightStatusIndicator.setVisibility(View.VISIBLE);
542        } else if (msgItem.mDeliveryReport || msgItem.mReadReport) {
543            mRightStatusIndicator.setImageResource(R.drawable.ic_mms_message_details);
544            mRightStatusIndicator.setVisibility(View.VISIBLE);
545        } else {
546            mRightStatusIndicator.setVisibility(View.GONE);
547        }
548    }
549
550    public void setImageRegionFit(String fit) {
551        // TODO Auto-generated method stub
552    }
553
554    public void setImageVisibility(boolean visible) {
555        // TODO Auto-generated method stub
556    }
557
558    public void setText(String name, String text) {
559        // TODO Auto-generated method stub
560    }
561
562    public void setTextVisibility(boolean visible) {
563        // TODO Auto-generated method stub
564    }
565
566    public void setVideo(String name, Uri video) {
567        inflateMmsView();
568
569        MediaPlayer mp = new MediaPlayer();
570
571        try {
572            mp.setDataSource(mContext, video);
573            mImageView.setImageBitmap(mp.getFrameAt(1000));
574            mImageView.setVisibility(VISIBLE);
575        } catch (IOException e) {
576            Log.e(TAG, "Unexpected IOException.", e);
577        } finally {
578            mp.release();
579        }
580    }
581
582    public void setVideoVisibility(boolean visible) {
583        // TODO Auto-generated method stub
584    }
585
586    public void stopAudio() {
587        // TODO Auto-generated method stub
588    }
589
590    public void stopVideo() {
591        // TODO Auto-generated method stub
592    }
593
594    public void reset() {
595        if (mImageView != null) {
596            mImageView.setVisibility(GONE);
597        }
598    }
599
600    public void setVisibility(boolean visible) {
601        // TODO Auto-generated method stub
602    }
603
604    public void pauseAudio() {
605        // TODO Auto-generated method stub
606
607    }
608
609    public void pauseVideo() {
610        // TODO Auto-generated method stub
611
612    }
613
614    public void seekAudio(int seekTo) {
615        // TODO Auto-generated method stub
616
617    }
618
619    public void seekVideo(int seekTo) {
620        // TODO Auto-generated method stub
621
622    }
623}
624