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