blob: ac8cdf31b784bc83c2cb535a2b4dcd5ae6686620 [file] [log] [blame]
/*
* Copyright (C) 2008 Esmertec AG.
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mms.ui;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Typeface;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.provider.Browser;
import android.provider.ContactsContract.Profile;
import android.provider.Telephony.Sms;
import android.telephony.PhoneNumberUtils;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.method.HideReturnsTransformationMethod;
import android.text.style.ForegroundColorSpan;
import android.text.style.LineHeightSpan;
import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.mms.MmsApp;
import com.android.mms.R;
import com.android.mms.data.Contact;
import com.android.mms.data.WorkingMessage;
import com.android.mms.transaction.Transaction;
import com.android.mms.transaction.TransactionBundle;
import com.android.mms.transaction.TransactionService;
import com.android.mms.util.DownloadManager;
import com.android.mms.util.SmileyParser;
import com.google.android.mms.ContentType;
import com.google.android.mms.pdu.PduHeaders;
/**
* This class provides view of a message in the messages list.
*/
public class MessageListItem extends LinearLayout implements
SlideViewInterface, OnClickListener {
public static final String EXTRA_URLS = "com.android.mms.ExtraUrls";
private static final String TAG = "MessageListItem";
private static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD);
static final int MSG_LIST_EDIT_MMS = 1;
static final int MSG_LIST_EDIT_SMS = 2;
private View mMmsView;
private ImageView mImageView;
private ImageView mLockedIndicator;
private ImageView mDeliveredIndicator;
private ImageView mDetailsIndicator;
private ImageButton mSlideShowButton;
private TextView mBodyTextView;
private Button mDownloadButton;
private TextView mDownloadingLabel;
private Handler mHandler;
private MessageItem mMessageItem;
private String mDefaultCountryIso;
private TextView mDateView;
public View mMessageBlock;
private Path mPath = new Path();
private Paint mPaint = new Paint();
private QuickContactDivot mAvatar;
private boolean mIsLastItemInList;
static private Drawable sDefaultContactImage;
public MessageListItem(Context context) {
super(context);
mDefaultCountryIso = MmsApp.getApplication().getCurrentCountryIso();
if (sDefaultContactImage == null) {
sDefaultContactImage = context.getResources().getDrawable(R.drawable.ic_contact_picture);
}
}
public MessageListItem(Context context, AttributeSet attrs) {
super(context, attrs);
int color = mContext.getResources().getColor(R.color.timestamp_color);
mColorSpan = new ForegroundColorSpan(color);
mDefaultCountryIso = MmsApp.getApplication().getCurrentCountryIso();
if (sDefaultContactImage == null) {
sDefaultContactImage = context.getResources().getDrawable(R.drawable.ic_contact_picture);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBodyTextView = (TextView) findViewById(R.id.text_view);
mDateView = (TextView) findViewById(R.id.date_view);
mLockedIndicator = (ImageView) findViewById(R.id.locked_indicator);
mDeliveredIndicator = (ImageView) findViewById(R.id.delivered_indicator);
mDetailsIndicator = (ImageView) findViewById(R.id.details_indicator);
mAvatar = (QuickContactDivot) findViewById(R.id.avatar);
mMessageBlock = findViewById(R.id.message_block);
}
public void bind(MessageItem msgItem, boolean isLastItem) {
mMessageItem = msgItem;
mIsLastItemInList = isLastItem;
setLongClickable(false);
setClickable(false); // let the list view handle clicks on the item normally. When
// clickable is true, clicks bypass the listview and go straight
// to this listitem. We always want the listview to handle the
// clicks first.
switch (msgItem.mMessageType) {
case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
bindNotifInd(msgItem);
break;
default:
bindCommonMessage(msgItem);
break;
}
}
public void unbind() {
// Clear all references to the message item, which can contain attachments and other
// memory-intensive objects
mMessageItem = null;
if (mImageView != null) {
// Because #setOnClickListener may have set the listener to an object that has the
// message item in its closure.
mImageView.setOnClickListener(null);
}
if (mSlideShowButton != null) {
// Because #drawPlaybackButton sets the tag to mMessageItem
mSlideShowButton.setTag(null);
}
}
public MessageItem getMessageItem() {
return mMessageItem;
}
public void setMsgListItemHandler(Handler handler) {
mHandler = handler;
}
private void bindNotifInd(final MessageItem msgItem) {
hideMmsViewIfNeeded();
String msgSizeText = mContext.getString(R.string.message_size_label)
+ String.valueOf((msgItem.mMessageSize + 1023) / 1024)
+ mContext.getString(R.string.kilobyte);
mBodyTextView.setText(formatMessage(msgItem, msgItem.mContact, null, msgItem.mSubject,
msgItem.mHighlight, msgItem.mTextContentType));
mDateView.setText(msgSizeText + " " + msgItem.mTimestamp);
int state = DownloadManager.getInstance().getState(msgItem.mMessageUri);
switch (state) {
case DownloadManager.STATE_DOWNLOADING:
inflateDownloadControls();
mDownloadingLabel.setVisibility(View.VISIBLE);
mDownloadButton.setVisibility(View.GONE);
break;
case DownloadManager.STATE_UNSTARTED:
case DownloadManager.STATE_TRANSIENT_FAILURE:
case DownloadManager.STATE_PERMANENT_FAILURE:
default:
setLongClickable(true);
inflateDownloadControls();
mDownloadingLabel.setVisibility(View.GONE);
mDownloadButton.setVisibility(View.VISIBLE);
mDownloadButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mDownloadingLabel.setVisibility(View.VISIBLE);
mDownloadButton.setVisibility(View.GONE);
Intent intent = new Intent(mContext, TransactionService.class);
intent.putExtra(TransactionBundle.URI, msgItem.mMessageUri.toString());
intent.putExtra(TransactionBundle.TRANSACTION_TYPE,
Transaction.RETRIEVE_TRANSACTION);
mContext.startService(intent);
}
});
break;
}
// Hide the indicators.
mLockedIndicator.setVisibility(View.GONE);
mDeliveredIndicator.setVisibility(View.GONE);
mDetailsIndicator.setVisibility(View.GONE);
updateAvatarView(msgItem.mAddress, false);
}
private void updateAvatarView(String addr, boolean isSelf) {
Drawable avatarDrawable;
if (isSelf || !TextUtils.isEmpty(addr)) {
Contact contact = isSelf ? Contact.getMe(false) : Contact.get(addr, false);
avatarDrawable = contact.getAvatar(mContext, sDefaultContactImage);
if (isSelf) {
mAvatar.assignContactUri(Profile.CONTENT_URI);
} else {
if (contact.existsInDatabase()) {
mAvatar.assignContactUri(contact.getUri());
} else {
mAvatar.assignContactFromPhone(contact.getNumber(), true);
}
}
} else {
avatarDrawable = sDefaultContactImage;
}
mAvatar.setImageDrawable(avatarDrawable);
}
private void bindCommonMessage(final MessageItem msgItem) {
if (mDownloadButton != null) {
mDownloadButton.setVisibility(View.GONE);
mDownloadingLabel.setVisibility(View.GONE);
}
// Since the message text should be concatenated with the sender's
// address(or name), I have to display it here instead of
// displaying it by the Presenter.
mBodyTextView.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
boolean isSelf = Sms.isOutgoingFolder(msgItem.mBoxId);
String addr = isSelf ? null : msgItem.mAddress;
updateAvatarView(addr, isSelf);
// Get and/or lazily set the formatted message from/on the
// MessageItem. Because the MessageItem instances come from a
// cache (currently of size ~50), the hit rate on avoiding the
// expensive formatMessage() call is very high.
CharSequence formattedMessage = msgItem.getCachedFormattedMessage();
if (formattedMessage == null) {
formattedMessage = formatMessage(msgItem, msgItem.mContact, msgItem.mBody,
msgItem.mSubject,
msgItem.mHighlight, msgItem.mTextContentType);
}
mBodyTextView.setText(formattedMessage);
// If we're in the process of sending a message (i.e. pending), then we show a "SENDING..."
// string in place of the timestamp.
mDateView.setText(msgItem.isSending() ?
mContext.getResources().getString(R.string.sending_message) :
msgItem.mTimestamp);
if (msgItem.isSms()) {
hideMmsViewIfNeeded();
} else {
Presenter presenter = PresenterFactory.getPresenter(
"MmsThumbnailPresenter", mContext,
this, msgItem.mSlideshow);
presenter.present();
if (msgItem.mAttachmentType != WorkingMessage.TEXT) {
inflateMmsView();
mMmsView.setVisibility(View.VISIBLE);
setOnClickListener(msgItem);
drawPlaybackButton(msgItem);
} else {
hideMmsViewIfNeeded();
}
}
drawRightStatusIndicator(msgItem);
requestLayout();
}
private void hideMmsViewIfNeeded() {
if (mMmsView != null) {
mMmsView.setVisibility(View.GONE);
}
}
@Override
public void startAudio() {
// TODO Auto-generated method stub
}
@Override
public void startVideo() {
// TODO Auto-generated method stub
}
@Override
public void setAudio(Uri audio, String name, Map<String, ?> extras) {
// TODO Auto-generated method stub
}
@Override
public void setImage(String name, Bitmap bitmap) {
inflateMmsView();
try {
if (null == bitmap) {
bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_missing_thumbnail_picture);
}
mImageView.setImageBitmap(bitmap);
mImageView.setVisibility(VISIBLE);
} catch (java.lang.OutOfMemoryError e) {
Log.e(TAG, "setImage: out of memory: ", e);
}
}
private void inflateMmsView() {
if (mMmsView == null) {
//inflate the surrounding view_stub
findViewById(R.id.mms_layout_view_stub).setVisibility(VISIBLE);
mMmsView = findViewById(R.id.mms_view);
mImageView = (ImageView) findViewById(R.id.image_view);
mSlideShowButton = (ImageButton) findViewById(R.id.play_slideshow_button);
}
}
private void inflateDownloadControls() {
if (mDownloadButton == null) {
//inflate the download controls
findViewById(R.id.mms_downloading_view_stub).setVisibility(VISIBLE);
mDownloadButton = (Button) findViewById(R.id.btn_download_msg);
mDownloadingLabel = (TextView) findViewById(R.id.label_downloading);
}
}
private LineHeightSpan mSpan = new LineHeightSpan() {
@Override
public void chooseHeight(CharSequence text, int start,
int end, int spanstartv, int v, FontMetricsInt fm) {
fm.ascent -= 10;
}
};
TextAppearanceSpan mTextSmallSpan =
new TextAppearanceSpan(mContext, android.R.style.TextAppearance_Small);
ForegroundColorSpan mColorSpan = null; // set in ctor
private CharSequence formatMessage(MessageItem msgItem, String contact, String body,
String subject, Pattern highlight,
String contentType) {
SpannableStringBuilder buf = new SpannableStringBuilder();
boolean hasSubject = !TextUtils.isEmpty(subject);
SmileyParser parser = SmileyParser.getInstance();
if (hasSubject) {
CharSequence smilizedSubject = parser.addSmileySpans(subject);
// Can't use the normal getString() with extra arguments for string replacement
// because it doesn't preserve the SpannableText returned by addSmileySpans.
// We have to manually replace the %s with our text.
buf.append(TextUtils.replace(mContext.getResources().getString(R.string.inline_subject),
new String[] { "%s" }, new CharSequence[] { smilizedSubject }));
}
if (!TextUtils.isEmpty(body)) {
// Converts html to spannable if ContentType is "text/html".
if (contentType != null && ContentType.TEXT_HTML.equals(contentType)) {
buf.append("\n");
buf.append(Html.fromHtml(body));
} else {
if (hasSubject) {
buf.append(" - ");
}
buf.append(parser.addSmileySpans(body));
}
}
if (highlight != null) {
Matcher m = highlight.matcher(buf.toString());
while (m.find()) {
buf.setSpan(new StyleSpan(Typeface.BOLD), m.start(), m.end(), 0);
}
}
return buf;
}
private void drawPlaybackButton(MessageItem msgItem) {
switch (msgItem.mAttachmentType) {
case WorkingMessage.SLIDESHOW:
case WorkingMessage.AUDIO:
case WorkingMessage.VIDEO:
// Show the 'Play' button and bind message info on it.
mSlideShowButton.setTag(msgItem);
// Set call-back for the 'Play' button.
mSlideShowButton.setOnClickListener(this);
mSlideShowButton.setVisibility(View.VISIBLE);
setLongClickable(true);
// When we show the mSlideShowButton, this list item's onItemClickListener doesn't
// get called. (It gets set in ComposeMessageActivity:
// mMsgListView.setOnItemClickListener) Here we explicitly set the item's
// onClickListener. It allows the item to respond to embedded html links and at the
// same time, allows the slide show play button to work.
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onMessageListItemClick();
}
});
break;
default:
mSlideShowButton.setVisibility(View.GONE);
break;
}
}
// OnClick Listener for the playback button
@Override
public void onClick(View v) {
MessageItem mi = (MessageItem) v.getTag();
switch (mi.mAttachmentType) {
case WorkingMessage.VIDEO:
case WorkingMessage.AUDIO:
case WorkingMessage.SLIDESHOW:
MessageUtils.viewMmsMessageAttachment(mContext, mi.mMessageUri, mi.mSlideshow);
break;
}
}
public void onMessageListItemClick() {
// If the message is a failed one, clicking it should reload it in the compose view,
// regardless of whether it has links in it
if (mMessageItem != null &&
mMessageItem.isOutgoingMessage() &&
mMessageItem.isFailedMessage() ) {
recomposeFailedMessage();
return;
}
// Check for links. If none, do nothing; if 1, open it; if >1, ask user to pick one
URLSpan[] spans = mBodyTextView.getUrls();
if (spans.length == 0) {
// Do nothing.
} else if (spans.length == 1) {
Uri uri = Uri.parse(spans[0].getURL());
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
mContext.startActivity(intent);
} else {
final java.util.ArrayList<String> urls = MessageUtils.extractUris(spans);
ArrayAdapter<String> adapter =
new ArrayAdapter<String>(mContext, android.R.layout.select_dialog_item, urls) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
try {
String url = getItem(position).toString();
TextView tv = (TextView) v;
Drawable d = mContext.getPackageManager().getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
if (d != null) {
d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight());
tv.setCompoundDrawablePadding(10);
tv.setCompoundDrawables(d, null, null, null);
}
final String telPrefix = "tel:";
if (url.startsWith(telPrefix)) {
url = PhoneNumberUtils.formatNumber(
url.substring(telPrefix.length()), mDefaultCountryIso);
}
tv.setText(url);
} catch (android.content.pm.PackageManager.NameNotFoundException ex) {
// it's ok if we're unable to set the drawable for this view - the user
// can still use it
}
return v;
}
};
AlertDialog.Builder b = new AlertDialog.Builder(mContext);
DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() {
@Override
public final void onClick(DialogInterface dialog, int which) {
if (which >= 0) {
Uri uri = Uri.parse(urls.get(which));
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
mContext.startActivity(intent);
}
dialog.dismiss();
}
};
b.setTitle(R.string.select_link_title);
b.setCancelable(true);
b.setAdapter(adapter, click);
b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public final void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
b.show();
}
}
private void setOnClickListener(final MessageItem msgItem) {
switch(msgItem.mAttachmentType) {
case WorkingMessage.IMAGE:
case WorkingMessage.VIDEO:
mImageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
MessageUtils.viewMmsMessageAttachment(mContext, null, msgItem.mSlideshow);
}
});
mImageView.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return v.showContextMenu();
}
});
break;
default:
mImageView.setOnClickListener(null);
break;
}
}
/**
* Assuming the current message is a failed one, reload it into the compose view so that the
* user can resend it.
*/
private void recomposeFailedMessage() {
String type = mMessageItem.mType;
final int what;
if (type.equals("sms")) {
what = MSG_LIST_EDIT_SMS;
} else {
what = MSG_LIST_EDIT_MMS;
}
if (null != mHandler) {
Message msg = Message.obtain(mHandler, what);
msg.obj = new Long(mMessageItem.mMsgId);
msg.sendToTarget();
}
}
private void drawRightStatusIndicator(MessageItem msgItem) {
// Locked icon
if (msgItem.mLocked) {
mLockedIndicator.setImageResource(R.drawable.ic_lock_message_sms);
mLockedIndicator.setVisibility(View.VISIBLE);
} else {
mLockedIndicator.setVisibility(View.GONE);
}
// Delivery icon - we can show a failed icon for both sms and mms, but for an actual
// delivery, we only show the icon for sms. We don't have the information here in mms to
// know whether the message has been delivered. For mms, msgItem.mDeliveryStatus set
// to MessageItem.DeliveryStatus.RECEIVED simply means the setting requesting a
// delivery report was turned on when the message was sent. Yes, it's confusing!
if ((msgItem.isOutgoingMessage() && msgItem.isFailedMessage()) ||
msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.FAILED) {
mDeliveredIndicator.setImageResource(R.drawable.ic_list_alert_sms_failed);
mDeliveredIndicator.setVisibility(View.VISIBLE);
} else if (msgItem.isSms() &&
msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.RECEIVED) {
mDeliveredIndicator.setImageResource(R.drawable.ic_sms_mms_delivered);
mDeliveredIndicator.setVisibility(View.VISIBLE);
} else {
mDeliveredIndicator.setVisibility(View.GONE);
}
// Message details icon - this icon is shown both for sms and mms messages. For mms,
// we show the icon if the read report or delivery report setting was set when the
// message was sent. Showing the icon tells the user there's more information
// by selecting the "View report" menu.
if (msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.INFO || msgItem.mReadReport
|| (msgItem.isMms() &&
msgItem.mDeliveryStatus == MessageItem.DeliveryStatus.RECEIVED)) {
mDetailsIndicator.setImageResource(R.drawable.ic_sms_mms_details);
mDetailsIndicator.setVisibility(View.VISIBLE);
} else {
mDetailsIndicator.setVisibility(View.GONE);
}
}
@Override
public void setImageRegionFit(String fit) {
// TODO Auto-generated method stub
}
@Override
public void setImageVisibility(boolean visible) {
// TODO Auto-generated method stub
}
@Override
public void setText(String name, String text) {
// TODO Auto-generated method stub
}
@Override
public void setTextVisibility(boolean visible) {
// TODO Auto-generated method stub
}
@Override
public void setVideo(String name, Uri video) {
inflateMmsView();
try {
Bitmap bitmap = VideoAttachmentView.createVideoThumbnail(mContext, video);
if (null == bitmap) {
bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_missing_thumbnail_video);
}
mImageView.setImageBitmap(bitmap);
mImageView.setVisibility(VISIBLE);
} catch (java.lang.OutOfMemoryError e) {
Log.e(TAG, "setVideo: out of memory: ", e);
}
}
@Override
public void setVideoVisibility(boolean visible) {
// TODO Auto-generated method stub
}
@Override
public void stopAudio() {
// TODO Auto-generated method stub
}
@Override
public void stopVideo() {
// TODO Auto-generated method stub
}
@Override
public void reset() {
if (mImageView != null) {
mImageView.setVisibility(GONE);
}
}
@Override
public void setVisibility(boolean visible) {
// TODO Auto-generated method stub
}
@Override
public void pauseAudio() {
// TODO Auto-generated method stub
}
@Override
public void pauseVideo() {
// TODO Auto-generated method stub
}
@Override
public void seekAudio(int seekTo) {
// TODO Auto-generated method stub
}
@Override
public void seekVideo(int seekTo) {
// TODO Auto-generated method stub
}
/**
* Override dispatchDraw so that we can put our own background and border in.
* This is all complexity to support a shared border from one item to the next.
*/
@Override
public void dispatchDraw(Canvas c) {
View v = mMessageBlock;
if (v != null) {
float l = v.getX();
float t = v.getY();
float r = v.getX() + v.getWidth();
float b = v.getY() + v.getHeight();
Path path = mPath;
path.reset();
super.dispatchDraw(c);
path.reset();
r -= 1;
// This block of code draws the border around the "message block" section
// of the layout. This would normally be a simple rectangle but we omit
// the border at the point of the avatar's divot. Also, the bottom is drawn
// 1 pixel below our own bounds to get it to line up with the border of
// the next item.
//
// But for the last item we draw the bottom in our own bounds -- so it will
// show up.
if (mIsLastItemInList) {
b -= 1;
}
if (mAvatar.getPosition() == Divot.RIGHT_UPPER) {
path.moveTo(l, t + mAvatar.getCloseOffset());
path.lineTo(l, t);
path.lineTo(r, t);
path.lineTo(r, b);
path.lineTo(l, b);
path.lineTo(l, t + mAvatar.getFarOffset());
} else if (mAvatar.getPosition() == Divot.LEFT_UPPER) {
path.moveTo(r, t + mAvatar.getCloseOffset());
path.lineTo(r, t);
path.lineTo(l, t);
path.lineTo(l, b);
path.lineTo(r, b);
path.lineTo(r, t + mAvatar.getFarOffset());
}
Paint paint = mPaint;
// paint.setColor(0xff00ff00);
paint.setColor(0xffcccccc);
paint.setStrokeWidth(1F);
paint.setStyle(Paint.Style.STROKE);
c.drawPath(path, paint);
} else {
super.dispatchDraw(c);
}
}
}