MessageListItem.java revision bc878528ed57d19bec31553cd75efaac1e0d3a5b
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
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                break;
341            default:
342                mSlideShowButton.setVisibility(View.GONE);
343                break;
344        }
345    }
346
347    // OnClick Listener for the playback button
348    public void onClick(View v) {
349        MessageItem mi = (MessageItem) v.getTag();
350        switch (mi.mAttachmentType) {
351            case WorkingMessage.VIDEO:
352            case WorkingMessage.AUDIO:
353            case WorkingMessage.SLIDESHOW:
354                MessageUtils.viewMmsMessageAttachment(mContext, mi.mMessageUri, mi.mSlideshow);
355                break;
356        }
357    }
358
359    public void onMessageListItemClick() {
360        URLSpan[] spans = mBodyTextView.getUrls();
361
362        if (spans.length == 0) {
363            // Do nothing.
364        } else if (spans.length == 1) {
365            Uri uri = Uri.parse(spans[0].getURL());
366            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
367            intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
368
369            mContext.startActivity(intent);
370        } else {
371            final java.util.ArrayList<String> urls = MessageUtils.extractUris(spans);
372
373            ArrayAdapter<String> adapter =
374                new ArrayAdapter<String>(mContext, android.R.layout.select_dialog_item, urls) {
375                public View getView(int position, View convertView, ViewGroup parent) {
376                    View v = super.getView(position, convertView, parent);
377                    try {
378                        String url = getItem(position).toString();
379                        TextView tv = (TextView) v;
380                        Drawable d = mContext.getPackageManager().getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
381                        if (d != null) {
382                            d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight());
383                            tv.setCompoundDrawablePadding(10);
384                            tv.setCompoundDrawables(d, null, null, null);
385                        }
386                        final String telPrefix = "tel:";
387                        if (url.startsWith(telPrefix)) {
388                            url = PhoneNumberUtils.formatNumber(url.substring(telPrefix.length()));
389                        }
390                        tv.setText(url);
391                    } catch (android.content.pm.PackageManager.NameNotFoundException ex) {
392                        ;
393                    }
394                    return v;
395                }
396            };
397
398            AlertDialog.Builder b = new AlertDialog.Builder(mContext);
399
400            DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() {
401                public final void onClick(DialogInterface dialog, int which) {
402                    if (which >= 0) {
403                        Uri uri = Uri.parse(urls.get(which));
404                        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
405                        intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
406                        mContext.startActivity(intent);
407                    }
408                }
409            };
410
411            b.setTitle(R.string.select_link_title);
412            b.setCancelable(true);
413            b.setAdapter(adapter, click);
414
415            b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
416                public final void onClick(DialogInterface dialog, int which) {
417                    dialog.dismiss();
418                }
419            });
420
421            b.show();
422        }
423    }
424
425
426    private void setOnClickListener(final MessageItem msgItem) {
427        switch(msgItem.mAttachmentType) {
428        case WorkingMessage.IMAGE:
429        case WorkingMessage.VIDEO:
430            mImageView.setOnClickListener(new OnClickListener() {
431                public void onClick(View v) {
432                    MessageUtils.viewMmsMessageAttachment(mContext, null, msgItem.mSlideshow);
433                }
434            });
435            mImageView.setOnLongClickListener(new OnLongClickListener() {
436                public boolean onLongClick(View v) {
437                    return v.showContextMenu();
438                }
439            });
440            break;
441
442        default:
443            mImageView.setOnClickListener(null);
444            break;
445        }
446    }
447
448    private void drawLeftStatusIndicator(int msgBoxId) {
449        switch (msgBoxId) {
450            case Mms.MESSAGE_BOX_INBOX:
451                mMsgListItem.setBackgroundResource(R.drawable.listitem_background_lightblue);
452                break;
453
454            case Mms.MESSAGE_BOX_DRAFTS:
455            case Sms.MESSAGE_TYPE_FAILED:
456            case Sms.MESSAGE_TYPE_QUEUED:
457            case Mms.MESSAGE_BOX_OUTBOX:
458                mMsgListItem.setBackgroundResource(R.drawable.listitem_background);
459                break;
460
461            default:
462                mMsgListItem.setBackgroundResource(R.drawable.listitem_background);
463                break;
464        }
465    }
466
467    public static boolean isFailedMessage(MessageItem msgItem) {
468        boolean isFailedMms = msgItem.isMms()
469                            && (msgItem.mErrorType >= MmsSms.ERR_TYPE_GENERIC_PERMANENT);
470        boolean isFailedSms = msgItem.isSms()
471                            && (msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED);
472        return isFailedMms || isFailedSms;
473    }
474
475    private void setErrorIndicatorClickListener(final MessageItem msgItem) {
476        String type = msgItem.mType;
477        final int what;
478        if (type.equals("sms")) {
479            what = MSG_LIST_EDIT_SMS;
480        } else {
481            what = MSG_LIST_EDIT_MMS;
482        }
483        mRightStatusIndicator.setOnClickListener(new OnClickListener() {
484            public void onClick(View v) {
485                if (null != mHandler) {
486                    Message msg = Message.obtain(mHandler, what);
487                    msg.obj = new Long(msgItem.mMsgId);
488                    msg.sendToTarget();
489                }
490            }
491        });
492    }
493
494    private void drawRightStatusIndicator(MessageItem msgItem) {
495        if (msgItem.isOutgoingMessage()) {
496            if (isFailedMessage(msgItem)) {
497                mRightStatusIndicator.setImageResource(R.drawable.ic_sms_mms_not_delivered);
498                setErrorIndicatorClickListener(msgItem);
499            } else {
500                mRightStatusIndicator.setImageResource(R.drawable.ic_email_pending);
501            }
502            mRightStatusIndicator.setVisibility(View.VISIBLE);
503        } else if (msgItem.mDeliveryReport || msgItem.mReadReport) {
504            mRightStatusIndicator.setImageResource(R.drawable.ic_mms_message_details);
505            mRightStatusIndicator.setVisibility(View.VISIBLE);
506        } else if (msgItem.mLocked) {
507            mRightStatusIndicator.setImageResource(R.drawable.lock);
508            mRightStatusIndicator.setVisibility(View.VISIBLE);
509        } else {
510            mRightStatusIndicator.setVisibility(View.GONE);
511        }
512    }
513
514    public void setImageRegionFit(String fit) {
515        // TODO Auto-generated method stub
516    }
517
518    public void setImageVisibility(boolean visible) {
519        // TODO Auto-generated method stub
520    }
521
522    public void setText(String name, String text) {
523        // TODO Auto-generated method stub
524    }
525
526    public void setTextVisibility(boolean visible) {
527        // TODO Auto-generated method stub
528    }
529
530    public void setVideo(String name, Uri video) {
531        inflateMmsView();
532        Bitmap bitmap = VideoAttachmentView.createVideoThumbnail(mContext, video);
533        if (null == bitmap) {
534            bitmap = BitmapFactory.decodeResource(getResources(),
535                    R.drawable.ic_missing_thumbnail_video);
536        }
537        mImageView.setImageBitmap(bitmap);
538        mImageView.setVisibility(VISIBLE);
539    }
540
541    public void setVideoVisibility(boolean visible) {
542        // TODO Auto-generated method stub
543    }
544
545    public void stopAudio() {
546        // TODO Auto-generated method stub
547    }
548
549    public void stopVideo() {
550        // TODO Auto-generated method stub
551    }
552
553    public void reset() {
554        if (mImageView != null) {
555            mImageView.setVisibility(GONE);
556        }
557    }
558
559    public void setVisibility(boolean visible) {
560        // TODO Auto-generated method stub
561    }
562
563    public void pauseAudio() {
564        // TODO Auto-generated method stub
565
566    }
567
568    public void pauseVideo() {
569        // TODO Auto-generated method stub
570
571    }
572
573    public void seekAudio(int seekTo) {
574        // TODO Auto-generated method stub
575
576    }
577
578    public void seekVideo(int seekTo) {
579        // TODO Auto-generated method stub
580
581    }
582}
583