blob: 638c7bce1b0d83ee1fcc5c10487cb180715fc1f0 [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 static android.content.res.Configuration.KEYBOARDHIDDEN_NO;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_ABORT;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_COMPLETE;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_START;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_STATUS_ACTION;
import static com.android.mms.ui.MessageListAdapter.COLUMN_ID;
import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE;
import static com.android.mms.ui.MessageListAdapter.PROJECTION;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.DialogInterface.OnClickListener;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SqliteWrapper;
import android.drm.DrmStore;
import android.graphics.drawable.Drawable;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
import android.os.SystemProperties;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts;
import android.provider.Settings;
import android.provider.ContactsContract.Intents;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsMessage;
import android.content.ClipboardManager;
import android.text.Editable;
import android.text.InputFilter;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.TextKeyListener;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewStub;
import android.view.WindowManager;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnCreateContextMenuListener;
import android.view.View.OnKeyListener;
import android.view.inputmethod.InputMethodManager;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.telephony.TelephonyIntents;
import com.android.internal.telephony.TelephonyProperties;
import com.android.mms.LogTag;
import com.android.mms.MmsApp;
import com.android.mms.MmsConfig;
import com.android.mms.R;
import com.android.mms.TempFileProvider;
import com.android.mms.data.Contact;
import com.android.mms.data.ContactList;
import com.android.mms.data.Conversation;
import com.android.mms.data.Conversation.ConversationQueryHandler;
import com.android.mms.data.WorkingMessage;
import com.android.mms.data.WorkingMessage.MessageStatusListener;
import com.android.mms.drm.DrmUtils;
import com.google.android.mms.ContentType;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.PduBody;
import com.google.android.mms.pdu.PduPart;
import com.google.android.mms.pdu.PduPersister;
import com.google.android.mms.pdu.SendReq;
import com.android.mms.model.SlideModel;
import com.android.mms.model.SlideshowModel;
import com.android.mms.transaction.MessagingNotification;
import com.android.mms.ui.MessageListView.OnSizeChangedListener;
import com.android.mms.ui.MessageUtils.ResizeImageResultCallback;
import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo;
import com.android.mms.util.AddressUtils;
import com.android.mms.util.PhoneNumberFormatter;
import com.android.mms.util.SendingProgressTokenManager;
import com.android.mms.util.SmileyParser;
import android.text.InputFilter.LengthFilter;
/**
* This is the main UI for:
* 1. Composing a new message;
* 2. Viewing/managing message history of a conversation.
*
* This activity can handle following parameters from the intent
* by which it's launched.
* thread_id long Identify the conversation to be viewed. When creating a
* new message, this parameter shouldn't be present.
* msg_uri Uri The message which should be opened for editing in the editor.
* address String The addresses of the recipients in current conversation.
* exit_on_sent boolean Exit this activity after the message is sent.
*/
public class ComposeMessageActivity extends Activity
implements View.OnClickListener, TextView.OnEditorActionListener,
MessageStatusListener, Contact.UpdateListener {
public static final int REQUEST_CODE_ATTACH_IMAGE = 100;
public static final int REQUEST_CODE_TAKE_PICTURE = 101;
public static final int REQUEST_CODE_ATTACH_VIDEO = 102;
public static final int REQUEST_CODE_TAKE_VIDEO = 103;
public static final int REQUEST_CODE_ATTACH_SOUND = 104;
public static final int REQUEST_CODE_RECORD_SOUND = 105;
public static final int REQUEST_CODE_CREATE_SLIDESHOW = 106;
public static final int REQUEST_CODE_ECM_EXIT_DIALOG = 107;
public static final int REQUEST_CODE_ADD_CONTACT = 108;
public static final int REQUEST_CODE_PICK = 109;
private static final String TAG = "Mms/compose";
private static final boolean DEBUG = false;
private static final boolean TRACE = false;
private static final boolean LOCAL_LOGV = false;
// Menu ID
private static final int MENU_ADD_SUBJECT = 0;
private static final int MENU_DELETE_THREAD = 1;
private static final int MENU_ADD_ATTACHMENT = 2;
private static final int MENU_DISCARD = 3;
private static final int MENU_SEND = 4;
private static final int MENU_CALL_RECIPIENT = 5;
private static final int MENU_CONVERSATION_LIST = 6;
private static final int MENU_DEBUG_DUMP = 7;
// Context menu ID
private static final int MENU_VIEW_CONTACT = 12;
private static final int MENU_ADD_TO_CONTACTS = 13;
private static final int MENU_EDIT_MESSAGE = 14;
private static final int MENU_VIEW_SLIDESHOW = 16;
private static final int MENU_VIEW_MESSAGE_DETAILS = 17;
private static final int MENU_DELETE_MESSAGE = 18;
private static final int MENU_SEARCH = 19;
private static final int MENU_DELIVERY_REPORT = 20;
private static final int MENU_FORWARD_MESSAGE = 21;
private static final int MENU_CALL_BACK = 22;
private static final int MENU_SEND_EMAIL = 23;
private static final int MENU_COPY_MESSAGE_TEXT = 24;
private static final int MENU_COPY_TO_SDCARD = 25;
private static final int MENU_INSERT_SMILEY = 26;
private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27;
private static final int MENU_LOCK_MESSAGE = 28;
private static final int MENU_UNLOCK_MESSAGE = 29;
private static final int MENU_SAVE_RINGTONE = 30;
private static final int MENU_PREFERENCES = 31;
private static final int RECIPIENTS_MAX_LENGTH = 312;
private static final int MESSAGE_LIST_QUERY_TOKEN = 9527;
private static final int MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN = 9528;
private static final int DELETE_MESSAGE_TOKEN = 9700;
private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
private static final long NO_DATE_FOR_DIALOG = -1L;
private static final String EXIT_ECM_RESULT = "exit_ecm_result";
// When the conversation has a lot of messages and a new message is sent, the list is scrolled
// so the user sees the just sent message. If we have to scroll the list more than 20 items,
// then a scroll shortcut is invoked to move the list near the end before scrolling.
private static final int MAX_ITEMS_TO_INVOKE_SCROLL_SHORTCUT = 20;
// Any change in height in the message list view greater than this threshold will not
// cause a smooth scroll. Instead, we jump the list directly to the desired position.
private static final int SMOOTH_SCROLL_THRESHOLD = 200;
private ContentResolver mContentResolver;
private BackgroundQueryHandler mBackgroundQueryHandler;
private Conversation mConversation; // Conversation we are working in
private boolean mExitOnSent; // Should we finish() after sending a message?
// TODO: mExitOnSent is obsolete -- remove
private View mTopPanel; // View containing the recipient and subject editors
private View mBottomPanel; // View containing the text editor, send button, ec.
private EditText mTextEditor; // Text editor to type your message into
private TextView mTextCounter; // Shows the number of characters used in text editor
private TextView mSendButtonMms; // Press to send mms
private ImageButton mSendButtonSms; // Press to send sms
private EditText mSubjectTextEditor; // Text editor for MMS subject
private AttachmentEditor mAttachmentEditor;
private View mAttachmentEditorScrollView;
private MessageListView mMsgListView; // ListView for messages in this conversation
public MessageListAdapter mMsgListAdapter; // and its corresponding ListAdapter
private RecipientsEditor mRecipientsEditor; // UI control for editing recipients
private ImageButton mRecipientsPicker; // UI control for recipients picker
private boolean mIsKeyboardOpen; // Whether the hardware keyboard is visible
private boolean mIsLandscape; // Whether we're in landscape mode
private boolean mPossiblePendingNotification; // If the message list has changed, we may have
// a pending notification to deal with.
private boolean mToastForDraftSave; // Whether to notify the user that a draft is being saved
private boolean mSentMessage; // true if the user has sent a message while in this
// activity. On a new compose message case, when the first
// message is sent is a MMS w/ attachment, the list blanks
// for a second before showing the sent message. But we'd
// think the message list is empty, thus show the recipients
// editor thinking it's a draft message. This flag should
// help clarify the situation.
private WorkingMessage mWorkingMessage; // The message currently being composed.
private AlertDialog mSmileyDialog;
private boolean mWaitingForSubActivity;
private int mLastRecipientCount; // Used for warning the user on too many recipients.
private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter;
private boolean mSendingMessage; // Indicates the current message is sending, and shouldn't send again.
private Intent mAddContactIntent; // Intent used to add a new contact
private Uri mTempMmsUri; // Only used as a temporary to hold a slideshow uri
private long mTempThreadId; // Only used as a temporary to hold a threadId
private AsyncDialog mAsyncDialog; // Used for background tasks.
private String mDebugRecipients;
private int mLastSmoothScrollPosition;
private boolean mScrollOnSend; // Flag that we need to scroll the list to the end.
private int mSavedScrollPosition = -1; // we save the ListView's scroll position in onPause(),
// so we can remember it after re-entering the activity.
// If the value >= 0, then we jump to that line. If the
// value is maxint, then we jump to the end.
/**
* Whether this activity is currently running (i.e. not paused)
*/
private boolean mIsRunning;
@SuppressWarnings("unused")
public static void log(String logMsg) {
Thread current = Thread.currentThread();
long tid = current.getId();
StackTraceElement[] stack = current.getStackTrace();
String methodName = stack[3].getMethodName();
// Prepend current thread ID and name of calling method to the message.
logMsg = "[" + tid + "] [" + methodName + "] " + logMsg;
Log.d(TAG, logMsg);
}
//==========================================================
// Inner classes
//==========================================================
private void editSlideshow() {
// The user wants to edit the slideshow. That requires us to persist the slideshow to
// disk as a PDU in saveAsMms. This code below does that persisting in a background
// task. If the task takes longer than a half second, a progress dialog is displayed.
// Once the PDU persisting is done, another runnable on the UI thread get executed to start
// the SlideshowEditActivity.
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
// This runnable gets run in a background thread.
mTempMmsUri = mWorkingMessage.saveAsMms(false);
}
}, new Runnable() {
@Override
public void run() {
// Once the above background thread is complete, this runnable is run
// on the UI thread.
if (mTempMmsUri == null) {
return;
}
Intent intent = new Intent(ComposeMessageActivity.this,
SlideshowEditActivity.class);
intent.setData(mTempMmsUri);
startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW);
}
}, R.string.building_slideshow_title);
}
private final Handler mAttachmentEditorHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case AttachmentEditor.MSG_EDIT_SLIDESHOW: {
editSlideshow();
break;
}
case AttachmentEditor.MSG_SEND_SLIDESHOW: {
if (isPreparedForSending()) {
ComposeMessageActivity.this.confirmSendMessageIfNeeded();
}
break;
}
case AttachmentEditor.MSG_VIEW_IMAGE:
case AttachmentEditor.MSG_PLAY_VIDEO:
case AttachmentEditor.MSG_PLAY_AUDIO:
case AttachmentEditor.MSG_PLAY_SLIDESHOW:
viewMmsMessageAttachment(msg.what);
break;
case AttachmentEditor.MSG_REPLACE_IMAGE:
case AttachmentEditor.MSG_REPLACE_VIDEO:
case AttachmentEditor.MSG_REPLACE_AUDIO:
showAddAttachmentDialog(true);
break;
case AttachmentEditor.MSG_REMOVE_ATTACHMENT:
mWorkingMessage.removeAttachment(true);
break;
default:
break;
}
}
};
private void viewMmsMessageAttachment(final int requestCode) {
SlideshowModel slideshow = mWorkingMessage.getSlideshow();
if (slideshow == null) {
throw new IllegalStateException("mWorkingMessage.getSlideshow() == null");
}
if (slideshow.isSimple()) {
MessageUtils.viewSimpleSlideshow(this, slideshow);
} else {
// The user wants to view the slideshow. That requires us to persist the slideshow to
// disk as a PDU in saveAsMms. This code below does that persisting in a background
// task. If the task takes longer than a half second, a progress dialog is displayed.
// Once the PDU persisting is done, another runnable on the UI thread get executed to
// start the SlideshowActivity.
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
// This runnable gets run in a background thread.
mTempMmsUri = mWorkingMessage.saveAsMms(false);
}
}, new Runnable() {
@Override
public void run() {
// Once the above background thread is complete, this runnable is run
// on the UI thread.
if (mTempMmsUri == null) {
return;
}
MessageUtils.launchSlideshowActivity(ComposeMessageActivity.this, mTempMmsUri,
requestCode);
}
}, R.string.building_slideshow_title);
}
}
private final Handler mMessageListItemHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
MessageItem msgItem = (MessageItem) msg.obj;
if (msgItem != null) {
switch (msg.what) {
case MessageListItem.MSG_LIST_DETAILS:
showMessageDetails(msgItem);
break;
case MessageListItem.MSG_LIST_EDIT:
editMessageItem(msgItem);
drawBottomPanel();
break;
case MessageListItem.MSG_LIST_PLAY:
switch (msgItem.mAttachmentType) {
case WorkingMessage.IMAGE:
case WorkingMessage.VIDEO:
case WorkingMessage.AUDIO:
case WorkingMessage.SLIDESHOW:
MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
msgItem.mMessageUri, msgItem.mSlideshow,
getAsyncDialog());
break;
}
break;
default:
Log.w(TAG, "Unknown message: " + msg.what);
return;
}
}
}
};
private boolean showMessageDetails(MessageItem msgItem) {
Cursor cursor = mMsgListAdapter.getCursorForItem(msgItem);
if (cursor == null) {
return false;
}
String messageDetails = MessageUtils.getMessageDetails(
ComposeMessageActivity.this, cursor, msgItem.mMessageSize);
new AlertDialog.Builder(ComposeMessageActivity.this)
.setTitle(R.string.message_details_title)
.setMessage(messageDetails)
.setCancelable(true)
.show();
return true;
}
private final OnKeyListener mSubjectKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
// When the subject editor is empty, press "DEL" to hide the input field.
if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) {
showSubjectEditor(false);
mWorkingMessage.setSubject(null, true);
return true;
}
return false;
}
};
/**
* Return the messageItem associated with the type ("mms" or "sms") and message id.
* @param type Type of the message: "mms" or "sms"
* @param msgId Message id of the message. This is the _id of the sms or pdu row and is
* stored in the MessageItem
* @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's
* cache and the code can create a new MessageItem based on the position of the current cursor.
* If false, the function returns null if the MessageItem isn't in the cache.
* @return MessageItem or null if not found and createFromCursorIfNotInCache is false
*/
private MessageItem getMessageItem(String type, long msgId,
boolean createFromCursorIfNotInCache) {
return mMsgListAdapter.getCachedMessageItem(type, msgId,
createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null);
}
private boolean isCursorValid() {
// Check whether the cursor is valid or not.
Cursor cursor = mMsgListAdapter.getCursor();
if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
Log.e(TAG, "Bad cursor.", new RuntimeException());
return false;
}
return true;
}
private void resetCounter() {
mTextCounter.setText("");
mTextCounter.setVisibility(View.GONE);
}
private void updateCounter(CharSequence text, int start, int before, int count) {
WorkingMessage workingMessage = mWorkingMessage;
if (workingMessage.requiresMms()) {
// If we're not removing text (i.e. no chance of converting back to SMS
// because of this change) and we're in MMS mode, just bail out since we
// then won't have to calculate the length unnecessarily.
final boolean textRemoved = (before > count);
if (!textRemoved) {
showSmsOrMmsSendButton(workingMessage.requiresMms());
return;
}
}
int[] params = SmsMessage.calculateLength(text, false);
/* SmsMessage.calculateLength returns an int[4] with:
* int[0] being the number of SMS's required,
* int[1] the number of code units used,
* int[2] is the number of code units remaining until the next message.
* int[3] is the encoding type that should be used for the message.
*/
int msgCount = params[0];
int remainingInCurrentMessage = params[2];
if (!MmsConfig.getMultipartSmsEnabled()) {
// The provider doesn't support multi-part sms's so as soon as the user types
// an sms longer than one segment, we have to turn the message into an mms.
mWorkingMessage.setLengthRequiresMms(msgCount > 1, true);
} else {
int threshold = MmsConfig.getSmsToMmsTextThreshold();
mWorkingMessage.setLengthRequiresMms(threshold > 0 && msgCount > threshold, true);
}
// Show the counter only if:
// - We are not in MMS mode
// - We are going to send more than one message OR we are getting close
boolean showCounter = false;
if (!workingMessage.requiresMms() &&
(msgCount > 1 ||
remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) {
showCounter = true;
}
showSmsOrMmsSendButton(workingMessage.requiresMms());
if (showCounter) {
// Update the remaining characters and number of messages required.
String counterText = msgCount > 1 ? remainingInCurrentMessage + " / " + msgCount
: String.valueOf(remainingInCurrentMessage);
mTextCounter.setText(counterText);
mTextCounter.setVisibility(View.VISIBLE);
} else {
mTextCounter.setVisibility(View.GONE);
}
}
@Override
public void startActivityForResult(Intent intent, int requestCode)
{
// requestCode >= 0 means the activity in question is a sub-activity.
if (requestCode >= 0) {
mWaitingForSubActivity = true;
}
if (mIsKeyboardOpen) {
hideKeyboard(); // camera and other activities take a long time to hide the keyboard
}
super.startActivityForResult(intent, requestCode);
}
private void toastConvertInfo(boolean toMms) {
final int resId = toMms ? R.string.converting_to_picture_message
: R.string.converting_to_text_message;
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
}
private class DeleteMessageListener implements OnClickListener {
private final MessageItem mMessageItem;
public DeleteMessageListener(MessageItem messageItem) {
mMessageItem = messageItem;
}
@Override
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... none) {
if (mMessageItem.isMms()) {
WorkingMessage.removeThumbnailsFromCache(mMessageItem.getSlideshow());
MmsApp.getApplication().getPduLoaderManager()
.removePdu(mMessageItem.mMessageUri);
// Delete the message *after* we've removed the thumbnails because we
// need the pdu and slideshow for removeThumbnailsFromCache to work.
}
mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN,
null, mMessageItem.mMessageUri,
mMessageItem.mLocked ? null : "locked=0", null);
return null;
}
}.execute();
}
}
private class DiscardDraftListener implements OnClickListener {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
mWorkingMessage.discard();
dialog.dismiss();
finish();
}
}
private class SendIgnoreInvalidRecipientListener implements OnClickListener {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
sendMessage(true);
dialog.dismiss();
}
}
private class CancelSendingListener implements OnClickListener {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
if (isRecipientsEditorVisible()) {
mRecipientsEditor.requestFocus();
}
dialog.dismiss();
}
}
private void confirmSendMessageIfNeeded() {
if (!isRecipientsEditorVisible()) {
sendMessage(true);
return;
}
boolean isMms = mWorkingMessage.requiresMms();
if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
if (mRecipientsEditor.hasValidRecipient(isMms)) {
String title = getResourcesString(R.string.has_invalid_recipient,
mRecipientsEditor.formatInvalidNumbers(isMms));
new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(R.string.invalid_recipient_message)
.setPositiveButton(R.string.try_to_send,
new SendIgnoreInvalidRecipientListener())
.setNegativeButton(R.string.no, new CancelSendingListener())
.show();
} else {
new AlertDialog.Builder(this)
.setTitle(R.string.cannot_send_message)
.setMessage(R.string.cannot_send_message_reason)
.setPositiveButton(R.string.yes, new CancelSendingListener())
.show();
}
} else {
sendMessage(true);
}
}
private final TextWatcher mRecipientsWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// This is a workaround for bug 1609057. Since onUserInteraction() is
// not called when the user touches the soft keyboard, we pretend it was
// called when textfields changes. This should be removed when the bug
// is fixed.
onUserInteraction();
}
@Override
public void afterTextChanged(Editable s) {
// Bug 1474782 describes a situation in which we send to
// the wrong recipient. We have been unable to reproduce this,
// but the best theory we have so far is that the contents of
// mRecipientList somehow become stale when entering
// ComposeMessageActivity via onNewIntent(). This assertion is
// meant to catch one possible path to that, of a non-visible
// mRecipientsEditor having its TextWatcher fire and refreshing
// mRecipientList with its stale contents.
if (!isRecipientsEditorVisible()) {
IllegalStateException e = new IllegalStateException(
"afterTextChanged called with invisible mRecipientsEditor");
// Make sure the crash is uploaded to the service so we
// can see if this is happening in the field.
Log.w(TAG,
"RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor");
return;
}
mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers());
mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true);
checkForTooManyRecipients();
// Walk backwards in the text box, skipping spaces. If the last
// character is a comma, update the title bar.
for (int pos = s.length() - 1; pos >= 0; pos--) {
char c = s.charAt(pos);
if (c == ' ')
continue;
if (c == ',') {
ContactList contacts = mRecipientsEditor.constructContactsFromInput(false);
updateTitle(contacts);
}
break;
}
// If we have gone to zero recipients, disable send button.
updateSendButtonState();
}
};
private void checkForTooManyRecipients() {
final int recipientLimit = MmsConfig.getRecipientLimit();
if (recipientLimit != Integer.MAX_VALUE) {
final int recipientCount = recipientCount();
boolean tooMany = recipientCount > recipientLimit;
if (recipientCount != mLastRecipientCount) {
// Don't warn the user on every character they type when they're over the limit,
// only when the actual # of recipients changes.
mLastRecipientCount = recipientCount;
if (tooMany) {
String tooManyMsg = getString(R.string.too_many_recipients, recipientCount,
recipientLimit);
Toast.makeText(ComposeMessageActivity.this,
tooManyMsg, Toast.LENGTH_LONG).show();
}
}
}
}
private final OnCreateContextMenuListener mRecipientsMenuCreateListener =
new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
if (menuInfo != null) {
Contact c = ((RecipientContextMenuInfo) menuInfo).recipient;
RecipientsMenuClickListener l = new RecipientsMenuClickListener(c);
menu.setHeaderTitle(c.getName());
if (c.existsInDatabase()) {
menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact)
.setOnMenuItemClickListener(l);
} else if (canAddToContacts(c)){
menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
.setOnMenuItemClickListener(l);
}
}
}
};
private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener {
private final Contact mRecipient;
RecipientsMenuClickListener(Contact recipient) {
mRecipient = recipient;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
// Context menu handlers for the recipients editor.
case MENU_VIEW_CONTACT: {
Uri contactUri = mRecipient.getUri();
Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(intent);
return true;
}
case MENU_ADD_TO_CONTACTS: {
mAddContactIntent = ConversationList.createAddContactIntent(
mRecipient.getNumber());
ComposeMessageActivity.this.startActivityForResult(mAddContactIntent,
REQUEST_CODE_ADD_CONTACT);
return true;
}
}
return false;
}
}
private boolean canAddToContacts(Contact contact) {
// There are some kind of automated messages, like STK messages, that we don't want
// to add to contacts. These names begin with special characters, like, "*Info".
final String name = contact.getName();
if (!TextUtils.isEmpty(contact.getNumber())) {
char c = contact.getNumber().charAt(0);
if (isSpecialChar(c)) {
return false;
}
}
if (!TextUtils.isEmpty(name)) {
char c = name.charAt(0);
if (isSpecialChar(c)) {
return false;
}
}
if (!(Mms.isEmailAddress(name) ||
AddressUtils.isPossiblePhoneNumber(name) ||
contact.isMe())) {
return false;
}
return true;
}
private boolean isSpecialChar(char c) {
return c == '*' || c == '%' || c == '$';
}
private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
AdapterView.AdapterContextMenuInfo info;
try {
info = (AdapterView.AdapterContextMenuInfo) menuInfo;
} catch (ClassCastException e) {
Log.e(TAG, "bad menuInfo");
return;
}
final int position = info.position;
addUriSpecificMenuItems(menu, v, position);
}
private Uri getSelectedUriFromMessageList(ListView listView, int position) {
// If the context menu was opened over a uri, get that uri.
MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position);
if (msglistItem == null) {
// FIXME: Should get the correct view. No such interface in ListView currently
// to get the view by position. The ListView.getChildAt(position) cannot
// get correct view since the list doesn't create one child for each item.
// And if setSelection(position) then getSelectedView(),
// cannot get corrent view when in touch mode.
return null;
}
TextView textView;
CharSequence text = null;
int selStart = -1;
int selEnd = -1;
//check if message sender is selected
textView = (TextView) msglistItem.findViewById(R.id.text_view);
if (textView != null) {
text = textView.getText();
selStart = textView.getSelectionStart();
selEnd = textView.getSelectionEnd();
}
// Check that some text is actually selected, rather than the cursor
// just being placed within the TextView.
if (selStart != selEnd) {
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
URLSpan[] urls = ((Spanned) text).getSpans(min, max,
URLSpan.class);
if (urls.length == 1) {
return Uri.parse(urls[0].getURL());
}
}
//no uri was selected
return null;
}
private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) {
Uri uri = getSelectedUriFromMessageList((ListView) v, position);
if (uri != null) {
Intent intent = new Intent(null, uri);
intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
menu.addIntentOptions(0, 0, 0,
new android.content.ComponentName(this, ComposeMessageActivity.class),
null, intent, 0, null);
}
}
private final void addCallAndContactMenuItems(
ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) {
if (TextUtils.isEmpty(msgItem.mBody)) {
return;
}
SpannableString msg = new SpannableString(msgItem.mBody);
Linkify.addLinks(msg, Linkify.ALL);
ArrayList<String> uris =
MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class));
// Remove any dupes so they don't get added to the menu multiple times
HashSet<String> collapsedUris = new HashSet<String>();
for (String uri : uris) {
collapsedUris.add(uri.toLowerCase());
}
for (String uriString : collapsedUris) {
String prefix = null;
int sep = uriString.indexOf(":");
if (sep >= 0) {
prefix = uriString.substring(0, sep);
uriString = uriString.substring(sep + 1);
}
Uri contactUri = null;
boolean knownPrefix = true;
if ("mailto".equalsIgnoreCase(prefix)) {
contactUri = getContactUriForEmail(uriString);
} else if ("tel".equalsIgnoreCase(prefix)) {
contactUri = getContactUriForPhoneNumber(uriString);
} else {
knownPrefix = false;
}
if (knownPrefix && contactUri == null) {
Intent intent = ConversationList.createAddContactIntent(uriString);
String addContactString = getString(R.string.menu_add_address_to_contacts,
uriString);
menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString)
.setOnMenuItemClickListener(l)
.setIntent(intent);
}
}
}
private Uri getContactUriForEmail(String emailAddress) {
Cursor cursor = SqliteWrapper.query(this, getContentResolver(),
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
new String[] { Email.CONTACT_ID, Contacts.DISPLAY_NAME }, null, null, null);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
String name = cursor.getString(1);
if (!TextUtils.isEmpty(name)) {
return ContentUris.withAppendedId(Contacts.CONTENT_URI, cursor.getLong(0));
}
}
} finally {
cursor.close();
}
}
return null;
}
private Uri getContactUriForPhoneNumber(String phoneNumber) {
Contact contact = Contact.get(phoneNumber, false);
if (contact.existsInDatabase()) {
return contact.getUri();
}
return null;
}
private final OnCreateContextMenuListener mMsgListMenuCreateListener =
new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (!isCursorValid()) {
return;
}
Cursor cursor = mMsgListAdapter.getCursor();
String type = cursor.getString(COLUMN_MSG_TYPE);
long msgId = cursor.getLong(COLUMN_ID);
addPositionBasedMenuItems(menu, v, menuInfo);
MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor);
if (msgItem == null) {
Log.e(TAG, "Cannot load message item for type = " + type
+ ", msgId = " + msgId);
return;
}
menu.setHeaderTitle(R.string.message_options);
MsgListMenuClickListener l = new MsgListMenuClickListener(msgItem);
// It is unclear what would make most sense for copying an MMS message
// to the clipboard, so we currently do SMS only.
if (msgItem.isSms()) {
// Message type is sms. Only allow "edit" if the message has a single recipient
if (getRecipients().size() == 1 &&
(msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX ||
msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) {
menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
.setOnMenuItemClickListener(l);
}
menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text)
.setOnMenuItemClickListener(l);
}
addCallAndContactMenuItems(menu, l, msgItem);
// Forward is not available for undownloaded messages.
if (msgItem.isDownloaded() && (msgItem.isSms() || isForwardable(msgId))) {
menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward)
.setOnMenuItemClickListener(l);
}
if (msgItem.isMms()) {
switch (msgItem.mBoxId) {
case Mms.MESSAGE_BOX_INBOX:
break;
case Mms.MESSAGE_BOX_OUTBOX:
// Since we currently break outgoing messages to multiple
// recipients into one message per recipient, only allow
// editing a message for single-recipient conversations.
if (getRecipients().size() == 1) {
menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
.setOnMenuItemClickListener(l);
}
break;
}
switch (msgItem.mAttachmentType) {
case WorkingMessage.TEXT:
break;
case WorkingMessage.VIDEO:
case WorkingMessage.IMAGE:
if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
.setOnMenuItemClickListener(l);
}
break;
case WorkingMessage.SLIDESHOW:
default:
menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow)
.setOnMenuItemClickListener(l);
if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
.setOnMenuItemClickListener(l);
}
if (isDrmRingtoneWithRights(msgItem.mMsgId)) {
menu.add(0, MENU_SAVE_RINGTONE, 0,
getDrmMimeMenuStringRsrc(msgItem.mMsgId))
.setOnMenuItemClickListener(l);
}
break;
}
}
if (msgItem.mLocked) {
menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock)
.setOnMenuItemClickListener(l);
} else {
menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock)
.setOnMenuItemClickListener(l);
}
menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details)
.setOnMenuItemClickListener(l);
if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || msgItem.mReadReport) {
menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report)
.setOnMenuItemClickListener(l);
}
menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message)
.setOnMenuItemClickListener(l);
}
};
private void editMessageItem(MessageItem msgItem) {
if ("sms".equals(msgItem.mType)) {
editSmsMessageItem(msgItem);
} else {
editMmsMessageItem(msgItem);
}
if (msgItem.isFailedMessage() && mMsgListAdapter.getCount() <= 1) {
// For messages with bad addresses, let the user re-edit the recipients.
initRecipientsEditor();
}
}
private void editSmsMessageItem(MessageItem msgItem) {
// When the message being edited is the only message in the conversation, the delete
// below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a
// thread contains no messages and silently deletes the thread. Meanwhile, the mConversation
// object still holds onto the old thread_id and code thinks there's a backing thread in
// the DB when it really has been deleted. Here we try and notice that situation and
// clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll
// create a new thread if necessary.
synchronized(mConversation) {
if (mConversation.getMessageCount() <= 1) {
mConversation.clearThreadId();
MessagingNotification.setCurrentlyDisplayedThreadId(
MessagingNotification.THREAD_NONE);
}
}
// Delete the old undelivered SMS and load its content.
Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId);
SqliteWrapper.delete(ComposeMessageActivity.this,
mContentResolver, uri, null, null);
mWorkingMessage.setText(msgItem.mBody);
}
private void editMmsMessageItem(MessageItem msgItem) {
// Load the selected message in as the working message.
WorkingMessage newWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri);
if (newWorkingMessage == null) {
return;
}
// Discard the current message in progress.
mWorkingMessage.discard();
mWorkingMessage = newWorkingMessage;
mWorkingMessage.setConversation(mConversation);
invalidateOptionsMenu();
drawTopPanel(false);
// WorkingMessage.load() above only loads the slideshow. Set the
// subject here because we already know what it is and avoid doing
// another DB lookup in load() just to get it.
mWorkingMessage.setSubject(msgItem.mSubject, false);
if (mWorkingMessage.hasSubject()) {
showSubjectEditor(true);
}
}
private void copyToClipboard(String str) {
ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(null, str));
}
private void forwardMessage(final MessageItem msgItem) {
mTempThreadId = 0;
// The user wants to forward the message. If the message is an mms message, we need to
// persist the pdu to disk. This is done in a background task.
// If the task takes longer than a half second, a progress dialog is displayed.
// Once the PDU persisting is done, another runnable on the UI thread get executed to start
// the ForwardMessageActivity.
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
// This runnable gets run in a background thread.
if (msgItem.mType.equals("mms")) {
SendReq sendReq = new SendReq();
String subject = getString(R.string.forward_prefix);
if (msgItem.mSubject != null) {
subject += msgItem.mSubject;
}
sendReq.setSubject(new EncodedStringValue(subject));
sendReq.setBody(msgItem.mSlideshow.makeCopy());
mTempMmsUri = null;
try {
PduPersister persister =
PduPersister.getPduPersister(ComposeMessageActivity.this);
// Copy the parts of the message here.
mTempMmsUri = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
mTempThreadId = MessagingNotification.getThreadId(
ComposeMessageActivity.this, mTempMmsUri);
} catch (MmsException e) {
Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri);
Toast.makeText(ComposeMessageActivity.this,
R.string.cannot_save_message, Toast.LENGTH_SHORT).show();
return;
}
}
}
}, new Runnable() {
@Override
public void run() {
// Once the above background thread is complete, this runnable is run
// on the UI thread.
Intent intent = createIntent(ComposeMessageActivity.this, 0);
intent.putExtra("exit_on_sent", true);
intent.putExtra("forwarded_message", true);
if (mTempThreadId > 0) {
intent.putExtra("thread_id", mTempThreadId);
}
if (msgItem.mType.equals("sms")) {
intent.putExtra("sms_body", msgItem.mBody);
} else {
intent.putExtra("msg_uri", mTempMmsUri);
String subject = getString(R.string.forward_prefix);
if (msgItem.mSubject != null) {
subject += msgItem.mSubject;
}
intent.putExtra("subject", subject);
}
// ForwardMessageActivity is simply an alias in the manifest for
// ComposeMessageActivity. We have to make an alias because ComposeMessageActivity
// launch flags specify singleTop. When we forward a message, we want to start a
// separate ComposeMessageActivity. The only way to do that is to override the
// singleTop flag, which is impossible to do in code. By creating an alias to the
// activity, without the singleTop flag, we can launch a separate
// ComposeMessageActivity to edit the forward message.
intent.setClassName(ComposeMessageActivity.this,
"com.android.mms.ui.ForwardMessageActivity");
startActivity(intent);
}
}, R.string.building_slideshow_title);
}
/**
* Context menu handlers for the message list view.
*/
private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener {
private MessageItem mMsgItem;
public MsgListMenuClickListener(MessageItem msgItem) {
mMsgItem = msgItem;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (mMsgItem == null) {
return false;
}
switch (item.getItemId()) {
case MENU_EDIT_MESSAGE:
editMessageItem(mMsgItem);
drawBottomPanel();
return true;
case MENU_COPY_MESSAGE_TEXT:
copyToClipboard(mMsgItem.mBody);
return true;
case MENU_FORWARD_MESSAGE:
forwardMessage(mMsgItem);
return true;
case MENU_VIEW_SLIDESHOW:
MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
ContentUris.withAppendedId(Mms.CONTENT_URI, mMsgItem.mMsgId), null,
getAsyncDialog());
return true;
case MENU_VIEW_MESSAGE_DETAILS:
return showMessageDetails(mMsgItem);
case MENU_DELETE_MESSAGE: {
DeleteMessageListener l = new DeleteMessageListener(mMsgItem);
confirmDeleteDialog(l, mMsgItem.mLocked);
return true;
}
case MENU_DELIVERY_REPORT:
showDeliveryReport(mMsgItem.mMsgId, mMsgItem.mType);
return true;
case MENU_COPY_TO_SDCARD: {
int resId = copyMedia(mMsgItem.mMsgId) ? R.string.copy_to_sdcard_success :
R.string.copy_to_sdcard_fail;
Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
return true;
}
case MENU_SAVE_RINGTONE: {
int resId = getDrmMimeSavedStringRsrc(mMsgItem.mMsgId,
saveRingtone(mMsgItem.mMsgId));
Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
return true;
}
case MENU_LOCK_MESSAGE: {
lockMessage(mMsgItem, true);
return true;
}
case MENU_UNLOCK_MESSAGE: {
lockMessage(mMsgItem, false);
return true;
}
default:
return false;
}
}
}
private void lockMessage(MessageItem msgItem, boolean locked) {
Uri uri;
if ("sms".equals(msgItem.mType)) {
uri = Sms.CONTENT_URI;
} else {
uri = Mms.CONTENT_URI;
}
final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);
final ContentValues values = new ContentValues(1);
values.put("locked", locked ? 1 : 0);
new Thread(new Runnable() {
@Override
public void run() {
getContentResolver().update(lockUri,
values, null, null);
}
}, "ComposeMessageActivity.lockMessage").start();
}
/**
* Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
* @param msgId
*/
private boolean haveSomethingToCopyToSDCard(long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "haveSomethingToCopyToSDCard can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
boolean result = false;
int partNum = body.getPartsNum();
for(int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
}
if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
ContentType.isAudioType(type) || DrmUtils.isDrmType(type)) {
result = true;
break;
}
}
return result;
}
/**
* Copies media from an Mms to the DrmProvider
* @param msgId
*/
private boolean saveRingtone(long msgId) {
boolean result = true;
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "copyToDrmProvider can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for(int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type)) {
// All parts (but there's probably only a single one) have to be successful
// for a valid result.
result &= copyPart(part, Long.toHexString(msgId));
}
}
return result;
}
/**
* Returns true if any part is drm'd audio with ringtone rights.
* @param msgId
* @return true if one of the parts is drm'd audio with rights to save as a ringtone.
*/
private boolean isDrmRingtoneWithRights(long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "isDrmRingtoneWithRights can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type)) {
String mimeType = MmsApp.getApplication().getDrmManagerClient()
.getOriginalMimeType(part.getDataUri());
if (ContentType.isAudioType(mimeType) && DrmUtils.haveRightsForAction(part.getDataUri(),
DrmStore.Action.RINGTONE)) {
return true;
}
}
}
return false;
}
/**
* Returns true if all drm'd parts are forwardable.
* @param msgId
* @return true if all drm'd parts are forwardable.
*/
private boolean isForwardable(long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "getDrmMimeType can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type) && !DrmUtils.haveRightsForAction(part.getDataUri(),
DrmStore.Action.TRANSFER)) {
return false;
}
}
return true;
}
private int getDrmMimeMenuStringRsrc(long msgId) {
if (isDrmRingtoneWithRights(msgId)) {
return R.string.save_ringtone;
}
return 0;
}
private int getDrmMimeSavedStringRsrc(long msgId, boolean success) {
if (isDrmRingtoneWithRights(msgId)) {
return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
}
return 0;
}
/**
* Copies media from an Mms to the "download" directory on the SD card. If any of the parts
* are audio types, drm'd or not, they're copied to the "Ringtones" directory.
* @param msgId
*/
private boolean copyMedia(long msgId) {
boolean result = true;
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "copyMedia can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for(int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
// all parts have to be successful for a valid result.
result &= copyPart(part, Long.toHexString(msgId));
}
return result;
}
private boolean copyPart(PduPart part, String fallback) {
Uri uri = part.getDataUri();
String type = new String(part.getContentType());
boolean isDrm = DrmUtils.isDrmType(type);
if (isDrm) {
type = MmsApp.getApplication().getDrmManagerClient()
.getOriginalMimeType(part.getDataUri());
}
if (!ContentType.isImageType(type) && !ContentType.isVideoType(type) &&
!ContentType.isAudioType(type)) {
return true; // we only save pictures, videos, and sounds. Skip the text parts,
// the app (smil) parts, and other type that we can't handle.
// Return true to pretend that we successfully saved the part so
// the whole save process will be counted a success.
}
InputStream input = null;
FileOutputStream fout = null;
try {
input = mContentResolver.openInputStream(uri);
if (input instanceof FileInputStream) {
FileInputStream fin = (FileInputStream) input;
byte[] location = part.getName();
if (location == null) {
location = part.getFilename();
}
if (location == null) {
location = part.getContentLocation();
}
String fileName;
if (location == null) {
// Use fallback name.
fileName = fallback;
} else {
// For locally captured videos, fileName can end up being something like this:
// /mnt/sdcard/Android/data/com.android.mms/cache/.temp1.3gp
fileName = new String(location);
}
File originalFile = new File(fileName);
fileName = originalFile.getName(); // Strip the full path of where the "part" is
// stored down to just the leaf filename.
// Depending on the location, there may be an
// extension already on the name or not. If we've got audio, put the attachment
// in the Ringtones directory.
String dir = Environment.getExternalStorageDirectory() + "/"
+ (ContentType.isAudioType(type) ? Environment.DIRECTORY_RINGTONES :
Environment.DIRECTORY_DOWNLOADS) + "/";
String extension;
int index;
if ((index = fileName.lastIndexOf('.')) == -1) {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
} else {
extension = fileName.substring(index + 1, fileName.length());
fileName = fileName.substring(0, index);
}
if (isDrm) {
extension += DrmUtils.getConvertExtension(type);
}
File file = getUniqueDestination(dir + fileName, extension);
// make sure the path is valid and directories created for this file.
File parentFile = file.getParentFile();
if (!parentFile.exists() && !parentFile.mkdirs()) {
Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
return false;
}
fout = new FileOutputStream(file);
byte[] buffer = new byte[8000];
int size = 0;
while ((size=fin.read(buffer)) != -1) {
fout.write(buffer, 0, size);
}
// Notify other applications listening to scanner events
// that a media file has been added to the sd card
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.fromFile(file)));
}
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while opening or reading stream", e);
return false;
} finally {
if (null != input) {
try {
input.close();
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while closing stream", e);
return false;
}
}
if (null != fout) {
try {
fout.close();
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while closing stream", e);
return false;
}
}
}
return true;
}
private File getUniqueDestination(String base, String extension) {
File file = new File(base + "." + extension);
for (int i = 2; file.exists(); i++) {
file = new File(base + "_" + i + "." + extension);
}
return file;
}
private void showDeliveryReport(long messageId, String type) {
Intent intent = new Intent(this, DeliveryReportActivity.class);
intent.putExtra("message_id", messageId);
intent.putExtra("message_type", type);
startActivity(intent);
}
private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION);
private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) {
long token = intent.getLongExtra("token",
SendingProgressTokenManager.NO_TOKEN);
if (token != mConversation.getThreadId()) {
return;
}
int progress = intent.getIntExtra("progress", 0);
switch (progress) {
case PROGRESS_START:
setProgressBarVisibility(true);
break;
case PROGRESS_ABORT:
case PROGRESS_COMPLETE:
setProgressBarVisibility(false);
break;
default:
setProgress(100 * progress);
}
}
}
};
private static ContactList sEmptyContactList;
private ContactList getRecipients() {
// If the recipients editor is visible, the conversation has
// not really officially 'started' yet. Recipients will be set
// on the conversation once it has been saved or sent. In the
// meantime, let anyone who needs the recipient list think it
// is empty rather than giving them a stale one.
if (isRecipientsEditorVisible()) {
if (sEmptyContactList == null) {
sEmptyContactList = new ContactList();
}
return sEmptyContactList;
}
return mConversation.getRecipients();
}
private void updateTitle(ContactList list) {
String title = null;
String subTitle = null;
int cnt = list.size();
switch (cnt) {
case 0: {
String recipient = null;
if (mRecipientsEditor != null) {
recipient = mRecipientsEditor.getText().toString();
}
title = TextUtils.isEmpty(recipient) ? getString(R.string.new_message) : recipient;
break;
}
case 1: {
title = list.get(0).getName(); // get name returns the number if there's no
// name available.
String number = list.get(0).getNumber();
if (!title.equals(number)) {
subTitle = PhoneNumberUtils.formatNumber(number, number,
MmsApp.getApplication().getCurrentCountryIso());
}
break;
}
default: {
// Handle multiple recipients
title = list.formatNames(", ");
subTitle = getResources().getQuantityString(R.plurals.recipient_count, cnt, cnt);
break;
}
}
mDebugRecipients = list.serialize();
ActionBar actionBar = getActionBar();
actionBar.setTitle(title);
actionBar.setSubtitle(subTitle);
}
// Get the recipients editor ready to be displayed onscreen.
private void initRecipientsEditor() {
if (isRecipientsEditorVisible()) {
return;
}
// Must grab the recipients before the view is made visible because getRecipients()
// returns empty recipients when the editor is visible.
ContactList recipients = getRecipients();
ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub);
if (stub != null) {
View stubView = stub.inflate();
mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor);
mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker);
} else {
mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
mRecipientsEditor.setVisibility(View.VISIBLE);
mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker);
}
mRecipientsPicker.setOnClickListener(this);
mRecipientsEditor.setAdapter(new ChipsRecipientAdapter(this));
mRecipientsEditor.populate(recipients);
mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener);
mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
// TODO : Remove the max length limitation due to the multiple phone picker is added and the
// user is able to select a large number of recipients from the Contacts. The coming
// potential issue is that it is hard for user to edit a recipient from hundred of
// recipients in the editor box. We may redesign the editor box UI for this use case.
// mRecipientsEditor.setFilters(new InputFilter[] {
// new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) });
mRecipientsEditor.setOnSelectChipRunnable(new Runnable() {
@Override
public void run() {
// After the user selects an item in the pop-up contacts list, move the
// focus to the text editor if there is only one recipient. This helps
// the common case of selecting one recipient and then typing a message,
// but avoids annoying a user who is trying to add five recipients and
// keeps having focus stolen away.
if (mRecipientsEditor.getRecipientCount() == 1) {
// if we're in extract mode then don't request focus
final InputMethodManager inputManager = (InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputManager == null || !inputManager.isFullscreenMode()) {
mTextEditor.requestFocus();
}
}
}
});
mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
RecipientsEditor editor = (RecipientsEditor) v;
ContactList contacts = editor.constructContactsFromInput(false);
updateTitle(contacts);
}
}
});
PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(this, mRecipientsEditor);
mTopPanel.setVisibility(View.VISIBLE);
}
//==========================================================
// Activity methods
//==========================================================
public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) {
if (MessagingNotification.isFailedToDeliver(intent)) {
// Cancel any failed message notifications
MessagingNotification.cancelNotification(context,
MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID);
return true;
}
return false;
}
public static boolean cancelFailedDownloadNotification(Intent intent, Context context) {
if (MessagingNotification.isFailedToDownload(intent)) {
// Cancel any failed download notifications
MessagingNotification.cancelNotification(context,
MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID);
return true;
}
return false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
resetConfiguration(getResources().getConfiguration());
setContentView(R.layout.compose_message_activity);
setProgressBarVisibility(false);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
// Initialize members for UI elements.
initResourceRefs();
mContentResolver = getContentResolver();
mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
initialize(savedInstanceState, 0);
if (TRACE) {
android.os.Debug.startMethodTracing("compose");
}
}
private void showSubjectEditor(boolean show) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("" + show);
}
if (mSubjectTextEditor == null) {
// Don't bother to initialize the subject editor if
// we're just going to hide it.
if (show == false) {
return;
}
mSubjectTextEditor = (EditText)findViewById(R.id.subject);
mSubjectTextEditor.setFilters(new InputFilter[] {
new LengthFilter(MmsConfig.getMaxSubjectLength())});
}
mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null);
if (show) {
mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher);
} else {
mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher);
}
mSubjectTextEditor.setText(mWorkingMessage.getSubject());
mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE);
hideOrShowTopPanel();
}
private void hideOrShowTopPanel() {
boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible());
mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE);
}
public void initialize(Bundle savedInstanceState, long originalThreadId) {
// Create a new empty working message.
mWorkingMessage = WorkingMessage.createEmpty(this);
// Read parameters or previously saved state of this activity. This will load a new
// mConversation
initActivityState(savedInstanceState);
if (LogTag.SEVERE_WARNING && originalThreadId != 0 &&
originalThreadId == mConversation.getThreadId()) {
LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " +
" threadId didn't change from: " + originalThreadId, this);
}
log("savedInstanceState = " + savedInstanceState +
" intent = " + getIntent() +
" mConversation = " + mConversation);
if (cancelFailedToDeliverNotification(getIntent(), this)) {
// Show a pop-up dialog to inform user the message was
// failed to deliver.
undeliveredMessageDialog(getMessageDate(null));
}
cancelFailedDownloadNotification(getIntent(), this);
// Set up the message history ListAdapter
initMessageList();
// Load the draft for this thread, if we aren't already handling
// existing data, such as a shared picture or forwarded message.
boolean isForwardedMessage = false;
// We don't attempt to handle the Intent.ACTION_SEND when saveInstanceState is non-null.
// saveInstanceState is non-null when this activity is killed. In that case, we already
// handled the attachment or the send, so we don't try and parse the intent again.
boolean intentHandled = savedInstanceState == null &&
(handleSendIntent() || handleForwardedMessage());
if (!intentHandled) {
loadDraft();
}
// Let the working message know what conversation it belongs to
mWorkingMessage.setConversation(mConversation);
// Show the recipients editor if we don't have a valid thread. Hide it otherwise.
if (mConversation.getThreadId() <= 0) {
// Hide the recipients editor so the call to initRecipientsEditor won't get
// short-circuited.
hideRecipientEditor();
initRecipientsEditor();
// Bring up the softkeyboard so the user can immediately enter recipients. This
// call won't do anything on devices with a hard keyboard.
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
hideRecipientEditor();
}
invalidateOptionsMenu(); // do after show/hide of recipients editor because the options
// menu depends on the recipients, which depending upon the
// visibility of the recipients editor, returns a different
// value (see getRecipients()).
updateSendButtonState();
drawTopPanel(false);
if (intentHandled) {
// We're not loading a draft, so we can draw the bottom panel immediately.
drawBottomPanel();
}
onKeyboardStateChanged(mIsKeyboardOpen);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("update title, mConversation=" + mConversation.toString());
}
updateTitle(mConversation.getRecipients());
if (isForwardedMessage && isRecipientsEditorVisible()) {
// The user is forwarding the message to someone. Put the focus on the
// recipient editor rather than in the message editor.
mRecipientsEditor.requestFocus();
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
Conversation conversation = null;
mSentMessage = false;
// If we have been passed a thread_id, use that to find our
// conversation.
// Note that originalThreadId might be zero but if this is a draft and we save the
// draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage
// the thread will get a threadId behind the UI thread's back.
long originalThreadId = mConversation.getThreadId();
long threadId = intent.getLongExtra("thread_id", 0);
Uri intentUri = intent.getData();
boolean sameThread = false;
if (threadId > 0) {
conversation = Conversation.get(this, threadId, false);
} else {
if (mConversation.getThreadId() == 0) {
// We've got a draft. Make sure the working recipients are synched
// to the conversation so when we compare conversations later in this function,
// the compare will work.
mWorkingMessage.syncWorkingRecipients();
}
// Get the "real" conversation based on the intentUri. The intentUri might specify
// the conversation by a phone number or by a thread id. We'll typically get a threadId
// based uri when the user pulls down a notification while in ComposeMessageActivity and
// we end up here in onNewIntent. mConversation can have a threadId of zero when we're
// working on a draft. When a new message comes in for that same recipient, a
// conversation will get created behind CMA's back when the message is inserted into
// the database and the corresponding entry made in the threads table. The code should
// use the real conversation as soon as it can rather than finding out the threadId
// when sending with "ensureThreadId".
conversation = Conversation.get(this, intentUri, false);
}
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId +
", new conversation=" + conversation + ", mConversation=" + mConversation);
}
// this is probably paranoid to compare both thread_ids and recipient lists,
// but we want to make double sure because this is a last minute fix for Froyo
// and the previous code checked thread ids only.
// (we cannot just compare thread ids because there is a case where mConversation
// has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1),
// even though the recipient lists are different)
sameThread = ((conversation.getThreadId() == mConversation.getThreadId() ||
mConversation.getThreadId() == 0) &&
conversation.equals(mConversation));
if (sameThread) {
log("onNewIntent: same conversation");
if (mConversation.getThreadId() == 0) {
mConversation = conversation;
mWorkingMessage.setConversation(mConversation);
updateThreadIdIfRunning();
invalidateOptionsMenu();
}
} else {
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("onNewIntent: different conversation");
}
saveDraft(false); // if we've got a draft, save it first
initialize(null, originalThreadId);
}
loadMessageContent();
}
private void sanityCheckConversation() {
if (mWorkingMessage.getConversation() != mConversation) {
LogTag.warnPossibleRecipientMismatch(
"ComposeMessageActivity: mWorkingMessage.mConversation=" +
mWorkingMessage.getConversation() + ", mConversation=" +
mConversation + ", MISMATCH!", this);
}
}
@Override
protected void onRestart() {
super.onRestart();
if (mWorkingMessage.isDiscarded()) {
// If the message isn't worth saving, don't resurrect it. Doing so can lead to
// a situation where a new incoming message gets the old thread id of the discarded
// draft. This activity can end up displaying the recipients of the old message with
// the contents of the new message. Recognize that dangerous situation and bail out
// to the ConversationList where the user can enter this in a clean manner.
if (mWorkingMessage.isWorthSaving()) {
if (LogTag.VERBOSE) {
log("onRestart: mWorkingMessage.unDiscard()");
}
mWorkingMessage.unDiscard(); // it was discarded in onStop().
sanityCheckConversation();
} else if (isRecipientsEditorVisible()) {
if (LogTag.VERBOSE) {
log("onRestart: goToConversationList");
}
goToConversationList();
} else {
if (LogTag.VERBOSE) {
log("onRestart: loadDraft");
}
loadDraft();
mWorkingMessage.setConversation(mConversation);
mAttachmentEditor.update(mWorkingMessage);
invalidateOptionsMenu();
}
}
}
@Override
protected void onStart() {
super.onStart();
initFocus();
// Register a BroadcastReceiver to listen on HTTP I/O process.
registerReceiver(mHttpProgressReceiver, mHttpProgressFilter);
loadMessageContent();
// Update the fasttrack info in case any of the recipients' contact info changed
// while we were paused. This can happen, for example, if a user changes or adds
// an avatar associated with a contact.
mWorkingMessage.syncWorkingRecipients();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("update title, mConversation=" + mConversation.toString());
}
updateTitle(mConversation.getRecipients());
ActionBar actionBar = getActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
}
public void loadMessageContent() {
// Don't let any markAsRead DB updates occur before we've loaded the messages for
// the thread. Unblocking occurs when we're done querying for the conversation
// items.
mConversation.blockMarkAsRead(true);
mConversation.markAsRead(); // dismiss any notifications for this convo
startMsgListQuery();
updateSendFailedNotification();
drawBottomPanel();
}
private void updateSendFailedNotification() {
final long threadId = mConversation.getThreadId();
if (threadId <= 0)
return;
// updateSendFailedNotificationForThread makes a database call, so do the work off
// of the ui thread.
new Thread(new Runnable() {
@Override
public void run() {
MessagingNotification.updateSendFailedNotificationForThread(
ComposeMessageActivity.this, threadId);
}
}, "ComposeMessageActivity.updateSendFailedNotification").start();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("recipients", getRecipients().serialize());
mWorkingMessage.writeStateToBundle(outState);
if (mExitOnSent) {
outState.putBoolean("exit_on_sent", mExitOnSent);
}
}
@Override
protected void onResume() {
super.onResume();
// OLD: get notified of presence updates to update the titlebar.
// NEW: we are using ContactHeaderWidget which displays presence, but updating presence
// there is out of our control.
//Contact.startPresenceObserver();
addRecipientsListeners();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("update title, mConversation=" + mConversation.toString());
}
// There seems to be a bug in the framework such that setting the title
// here gets overwritten to the original title. Do this delayed as a
// workaround.
mMessageListItemHandler.postDelayed(new Runnable() {
@Override
public void run() {
ContactList recipients = isRecipientsEditorVisible() ?
mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
updateTitle(recipients);
}
}, 100);
mIsRunning = true;
updateThreadIdIfRunning();
}
@Override
protected void onPause() {
super.onPause();
// OLD: stop getting notified of presence updates to update the titlebar.
// NEW: we are using ContactHeaderWidget which displays presence, but updating presence
// there is out of our control.
//Contact.stopPresenceObserver();
removeRecipientsListeners();
// remove any callback to display a progress spinner
if (mAsyncDialog != null) {
mAsyncDialog.clearPendingProgressDialog();
}
MessagingNotification.setCurrentlyDisplayedThreadId(MessagingNotification.THREAD_NONE);
// Remember whether the list is scrolled to the end when we're paused so we can rescroll
// to the end when resumed.
if (mMsgListAdapter != null &&
mMsgListView.getLastVisiblePosition() >= mMsgListAdapter.getCount() - 1) {
mSavedScrollPosition = Integer.MAX_VALUE;
} else {
mSavedScrollPosition = mMsgListView.getFirstVisiblePosition();
}
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "onPause: mSavedScrollPosition=" + mSavedScrollPosition);
}
mIsRunning = false;
}
@Override
protected void onStop() {
super.onStop();
// Allow any blocked calls to update the thread's read status.
mConversation.blockMarkAsRead(false);
if (mMsgListAdapter != null) {
mMsgListAdapter.changeCursor(null);
mMsgListAdapter.cancelBackgroundLoading();
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("save draft");
}
saveDraft(true);
// Cleanup the BroadcastReceiver.
unregisterReceiver(mHttpProgressReceiver);
}
@Override
protected void onDestroy() {
if (TRACE) {
android.os.Debug.stopMethodTracing();
}
super.onDestroy();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (LOCAL_LOGV) {
Log.v(TAG, "onConfigurationChanged: " + newConfig);
}
if (resetConfiguration(newConfig)) {
// Have to re-layout the attachment editor because we have different layouts
// depending on whether we're portrait or landscape.
drawTopPanel(isSubjectEditorVisible());
}
onKeyboardStateChanged(mIsKeyboardOpen);
}
// returns true if landscape/portrait configuration has changed
private boolean resetConfiguration(Configuration config) {
mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO;
boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE;
if (mIsLandscape != isLandscape) {
mIsLandscape = isLandscape;
return true;
}
return false;
}
private void onKeyboardStateChanged(boolean isKeyboardOpen) {
// If the keyboard is hidden, don't show focus highlights for
// things that cannot receive input.
if (isKeyboardOpen) {
if (mRecipientsEditor != null) {
mRecipientsEditor.setFocusableInTouchMode(true);
}
if (mSubjectTextEditor != null) {
mSubjectTextEditor.setFocusableInTouchMode(true);
}
mTextEditor.setFocusableInTouchMode(true);
mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
} else {
if (mRecipientsEditor != null) {
mRecipientsEditor.setFocusable(false);
}
if (mSubjectTextEditor != null) {
mSubjectTextEditor.setFocusable(false);
}
mTextEditor.setFocusable(false);
mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
}
}
@Override
public void onUserInteraction() {
checkPendingNotification();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
Cursor cursor;
try {
cursor = (Cursor) mMsgListView.getSelectedItem();
} catch (ClassCastException e) {
Log.e(TAG, "Unexpected ClassCastException.", e);
return super.onKeyDown(keyCode, event);
}
if (cursor != null) {
String type = cursor.getString(COLUMN_MSG_TYPE);
long msgId = cursor.getLong(COLUMN_ID);
MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId,
cursor);
if (msgItem != null) {
DeleteMessageListener l = new DeleteMessageListener(msgItem);
confirmDeleteDialog(l, msgItem.mLocked);
}
return true;
}
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (isPreparedForSending()) {
confirmSendMessageIfNeeded();
return true;
}
break;
case KeyEvent.KEYCODE_BACK:
exitComposeMessageActivity(new Runnable() {
@Override
public void run() {
finish();
}
});
return true;
}
return super.onKeyDown(keyCode, event);
}
private void exitComposeMessageActivity(final Runnable exit) {
// If the message is empty, just quit -- finishing the
// activity will cause an empty draft to be deleted.
if (!mWorkingMessage.isWorthSaving()) {
exit.run();
return;
}
if (isRecipientsEditorVisible() &&
!mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) {
MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener());
return;
}
mToastForDraftSave = true;
exit.run();
}
private void goToConversationList() {
finish();
startActivity(new Intent(this, ConversationList.class));
}
private void hideRecipientEditor() {
if (mRecipientsEditor != null) {
mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
mRecipientsEditor.setVisibility(View.GONE);
hideOrShowTopPanel();
}
}
private boolean isRecipientsEditorVisible() {
return (null != mRecipientsEditor)
&& (View.VISIBLE == mRecipientsEditor.getVisibility());
}
private boolean isSubjectEditorVisible() {
return (null != mSubjectTextEditor)
&& (View.VISIBLE == mSubjectTextEditor.getVisibility());
}
@Override
public void onAttachmentChanged() {
// Have to make sure we're on the UI thread. This function can be called off of the UI
// thread when we're adding multi-attachments
runOnUiThread(new Runnable() {
@Override
public void run() {
drawBottomPanel();
updateSendButtonState();
drawTopPanel(isSubjectEditorVisible());
}
});
}
@Override
public void onProtocolChanged(final boolean mms) {
// Have to make sure we're on the UI thread. This function can be called off of the UI
// thread when we're adding multi-attachments
runOnUiThread(new Runnable() {
@Override
public void run() {
toastConvertInfo(mms);
showSmsOrMmsSendButton(mms);
if (mms) {
// In the case we went from a long sms with a counter to an mms because
// the user added an attachment or a subject, hide the counter --
// it doesn't apply to mms.
mTextCounter.setVisibility(View.GONE);
}
}
});
}
// Show or hide the Sms or Mms button as appropriate. Return the view so that the caller
// can adjust the enableness and focusability.
private View showSmsOrMmsSendButton(boolean isMms) {
View showButton;
View hideButton;
if (isMms) {
showButton = mSendButtonMms;
hideButton = mSendButtonSms;
} else {
showButton = mSendButtonSms;
hideButton = mSendButtonMms;
}
showButton.setVisibility(View.VISIBLE);
hideButton.setVisibility(View.GONE);
return showButton;
}
Runnable mResetMessageRunnable = new Runnable() {
@Override
public void run() {
resetMessage();
}
};
@Override
public void onPreMessageSent() {
runOnUiThread(mResetMessageRunnable);
}
@Override
public void onMessageSent() {
// This callback can come in on any thread; put it on the main thread to avoid
// concurrency problems
runOnUiThread(new Runnable() {
@Override
public void run() {
// If we already have messages in the list adapter, it
// will be auto-requerying; don't thrash another query in.
// TODO: relying on auto-requerying seems unreliable when priming an MMS into the
// outbox. Need to investigate.
// if (mMsgListAdapter.getCount() == 0) {
if (LogTag.VERBOSE) {
log("onMessageSent");
}
startMsgListQuery();
// }
// The thread ID could have changed if this is a new message that we just inserted
// into the database (and looked up or created a thread for it)
updateThreadIdIfRunning();
}
});
}
@Override
public void onMaxPendingMessagesReached() {
saveDraft(false);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms,
Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onAttachmentError(final int error) {
runOnUiThread(new Runnable() {
@Override
public void run() {
handleAddAttachmentError(error, R.string.type_picture);
onMessageSent(); // now requery the list of messages
}
});
}
// We don't want to show the "call" option unless there is only one
// recipient and it's a phone number.
private boolean isRecipientCallable() {
ContactList recipients = getRecipients();
return (recipients.size() == 1 && !recipients.containsEmail());
}
private void dialRecipient() {
if (isRecipientCallable()) {
String number = getRecipients().get(0).getNumber();
Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number));
startActivity(dialIntent);
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu) ;
menu.clear();
if (isRecipientCallable()) {
MenuItem item = menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call)
.setIcon(R.drawable.ic_menu_call)
.setTitle(R.string.menu_call);
if (!isRecipientsEditorVisible()) {
// If we're not composing a new message, show the call icon in the actionbar
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
}
if (MmsConfig.getMmsEnabled()) {
if (!isSubjectEditorVisible()) {
menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon(
R.drawable.ic_menu_edit);
}
menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment)
.setIcon(R.drawable.ic_menu_attachment)
.setTitle(R.string.add_attachment)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); // add to actionbar
}
if (isPreparedForSending()) {
menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
}
if (!mWorkingMessage.hasSlideshow()) {
menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon(
R.drawable.ic_menu_emoticons);
}
if (mMsgListAdapter.getCount() > 0) {
// Removed search as part of b/1205708
//menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
// R.drawable.ic_menu_search);
Cursor cursor = mMsgListAdapter.getCursor();
if ((null != cursor) && (cursor.getCount() > 0)) {
menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
android.R.drawable.ic_menu_delete);
}
} else {
menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
}
buildAddAddressToContactMenuItem(menu);
menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon(
android.R.drawable.ic_menu_preferences);
if (LogTag.DEBUG_DUMP) {