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