• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 Esmertec AG.
3  * Copyright (C) 2008 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.mms.ui;
19 
20 import static android.content.res.Configuration.KEYBOARDHIDDEN_NO;
21 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_ABORT;
22 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_COMPLETE;
23 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_START;
24 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_STATUS_ACTION;
25 import static com.android.mms.ui.MessageListAdapter.COLUMN_ID;
26 import static com.android.mms.ui.MessageListAdapter.COLUMN_MMS_LOCKED;
27 import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE;
28 import static com.android.mms.ui.MessageListAdapter.PROJECTION;
29 
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.UnsupportedEncodingException;
36 import java.net.URLDecoder;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.regex.Pattern;
42 
43 import android.app.Activity;
44 import android.app.AlertDialog;
45 import android.content.ActivityNotFoundException;
46 import android.content.AsyncQueryHandler;
47 import android.content.BroadcastReceiver;
48 import android.content.ContentResolver;
49 import android.content.ContentUris;
50 import android.content.ContentValues;
51 import android.content.Context;
52 import android.content.DialogInterface;
53 import android.content.Intent;
54 import android.content.IntentFilter;
55 import android.content.DialogInterface.OnClickListener;
56 import android.content.res.Configuration;
57 import android.content.res.Resources;
58 import android.database.Cursor;
59 import android.database.sqlite.SQLiteException;
60 import android.database.sqlite.SqliteWrapper;
61 import android.drm.mobile1.DrmException;
62 import android.drm.mobile1.DrmRawContent;
63 import android.graphics.drawable.Drawable;
64 import android.media.CamcorderProfile;
65 import android.media.RingtoneManager;
66 import android.net.Uri;
67 import android.os.Bundle;
68 import android.os.Environment;
69 import android.os.Handler;
70 import android.os.Message;
71 import android.os.Parcelable;
72 import android.os.SystemProperties;
73 import android.provider.ContactsContract;
74 import android.provider.DrmStore;
75 import android.provider.MediaStore;
76 import android.provider.Settings;
77 import android.provider.ContactsContract.Contacts;
78 import android.provider.ContactsContract.CommonDataKinds.Email;
79 import android.provider.MediaStore.Images;
80 import android.provider.MediaStore.Video;
81 import android.provider.Telephony.Mms;
82 import android.provider.Telephony.Sms;
83 import android.telephony.SmsMessage;
84 import android.text.ClipboardManager;
85 import android.text.Editable;
86 import android.text.InputFilter;
87 import android.text.SpannableString;
88 import android.text.Spanned;
89 import android.text.TextUtils;
90 import android.text.TextWatcher;
91 import android.text.method.TextKeyListener;
92 import android.text.style.AbsoluteSizeSpan;
93 import android.text.style.URLSpan;
94 import android.text.util.Linkify;
95 import android.util.Config;
96 import android.util.Log;
97 import android.view.ContextMenu;
98 import android.view.KeyEvent;
99 import android.view.LayoutInflater;
100 import android.view.Menu;
101 import android.view.MenuItem;
102 import android.view.View;
103 import android.view.ViewStub;
104 import android.view.WindowManager;
105 import android.view.ContextMenu.ContextMenuInfo;
106 import android.view.View.OnCreateContextMenuListener;
107 import android.view.View.OnKeyListener;
108 import android.view.inputmethod.InputMethodManager;
109 import android.webkit.MimeTypeMap;
110 import android.widget.AdapterView;
111 import android.widget.Button;
112 import android.widget.EditText;
113 import android.widget.ImageView;
114 import android.widget.LinearLayout;
115 import android.widget.ListView;
116 import android.widget.SimpleAdapter;
117 import android.widget.TextView;
118 import android.widget.Toast;
119 
120 import com.android.internal.telephony.TelephonyIntents;
121 import com.android.internal.telephony.TelephonyProperties;
122 import com.android.mms.LogTag;
123 import com.android.mms.MmsConfig;
124 import com.android.mms.R;
125 import com.android.mms.data.Contact;
126 import com.android.mms.data.ContactList;
127 import com.android.mms.data.Conversation;
128 import com.android.mms.data.WorkingMessage;
129 import com.android.mms.data.WorkingMessage.MessageStatusListener;
130 import com.google.android.mms.ContentType;
131 import com.google.android.mms.pdu.EncodedStringValue;
132 import com.google.android.mms.MmsException;
133 import com.google.android.mms.pdu.PduBody;
134 import com.google.android.mms.pdu.PduPart;
135 import com.google.android.mms.pdu.PduPersister;
136 import com.google.android.mms.pdu.SendReq;
137 import com.android.mms.model.SlideModel;
138 import com.android.mms.model.SlideshowModel;
139 import com.android.mms.transaction.MessagingNotification;
140 import com.android.mms.ui.MessageUtils.ResizeImageResultCallback;
141 import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo;
142 import com.android.mms.util.SendingProgressTokenManager;
143 import com.android.mms.util.SmileyParser;
144 
145 import android.text.InputFilter.LengthFilter;
146 
147 /**
148  * This is the main UI for:
149  * 1. Composing a new message;
150  * 2. Viewing/managing message history of a conversation.
151  *
152  * This activity can handle following parameters from the intent
153  * by which it's launched.
154  * thread_id long Identify the conversation to be viewed. When creating a
155  *         new message, this parameter shouldn't be present.
156  * msg_uri Uri The message which should be opened for editing in the editor.
157  * address String The addresses of the recipients in current conversation.
158  * exit_on_sent boolean Exit this activity after the message is sent.
159  */
160 public class ComposeMessageActivity extends Activity
161         implements View.OnClickListener, TextView.OnEditorActionListener,
162         MessageStatusListener, Contact.UpdateListener {
163     public static final int REQUEST_CODE_ATTACH_IMAGE     = 10;
164     public static final int REQUEST_CODE_TAKE_PICTURE     = 11;
165     public static final int REQUEST_CODE_ATTACH_VIDEO     = 12;
166     public static final int REQUEST_CODE_TAKE_VIDEO       = 13;
167     public static final int REQUEST_CODE_ATTACH_SOUND     = 14;
168     public static final int REQUEST_CODE_RECORD_SOUND     = 15;
169     public static final int REQUEST_CODE_CREATE_SLIDESHOW = 16;
170     public static final int REQUEST_CODE_ECM_EXIT_DIALOG  = 17;
171     public static final int REQUEST_CODE_ADD_CONTACT      = 18;
172 
173     private static final String TAG = "Mms/compose";
174 
175     private static final boolean DEBUG = false;
176     private static final boolean TRACE = false;
177     private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
178 
179     // Menu ID
180     private static final int MENU_ADD_SUBJECT           = 0;
181     private static final int MENU_DELETE_THREAD         = 1;
182     private static final int MENU_ADD_ATTACHMENT        = 2;
183     private static final int MENU_DISCARD               = 3;
184     private static final int MENU_SEND                  = 4;
185     private static final int MENU_CALL_RECIPIENT        = 5;
186     private static final int MENU_CONVERSATION_LIST     = 6;
187 
188     // Context menu ID
189     private static final int MENU_VIEW_CONTACT          = 12;
190     private static final int MENU_ADD_TO_CONTACTS       = 13;
191 
192     private static final int MENU_EDIT_MESSAGE          = 14;
193     private static final int MENU_VIEW_SLIDESHOW        = 16;
194     private static final int MENU_VIEW_MESSAGE_DETAILS  = 17;
195     private static final int MENU_DELETE_MESSAGE        = 18;
196     private static final int MENU_SEARCH                = 19;
197     private static final int MENU_DELIVERY_REPORT       = 20;
198     private static final int MENU_FORWARD_MESSAGE       = 21;
199     private static final int MENU_CALL_BACK             = 22;
200     private static final int MENU_SEND_EMAIL            = 23;
201     private static final int MENU_COPY_MESSAGE_TEXT     = 24;
202     private static final int MENU_COPY_TO_SDCARD        = 25;
203     private static final int MENU_INSERT_SMILEY         = 26;
204     private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27;
205     private static final int MENU_LOCK_MESSAGE          = 28;
206     private static final int MENU_UNLOCK_MESSAGE        = 29;
207     private static final int MENU_COPY_TO_DRM_PROVIDER  = 30;
208 
209     private static final int RECIPIENTS_MAX_LENGTH = 312;
210 
211     private static final int MESSAGE_LIST_QUERY_TOKEN = 9527;
212 
213     private static final int DELETE_MESSAGE_TOKEN  = 9700;
214 
215     private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
216 
217     private static final long NO_DATE_FOR_DIALOG = -1L;
218 
219     private static final String EXIT_ECM_RESULT = "exit_ecm_result";
220 
221     private ContentResolver mContentResolver;
222 
223     private BackgroundQueryHandler mBackgroundQueryHandler;
224 
225     private Conversation mConversation;     // Conversation we are working in
226 
227     private boolean mExitOnSent;            // Should we finish() after sending a message?
228 
229     private View mTopPanel;                 // View containing the recipient and subject editors
230     private View mBottomPanel;              // View containing the text editor, send button, ec.
231     private EditText mTextEditor;           // Text editor to type your message into
232     private TextView mTextCounter;          // Shows the number of characters used in text editor
233     private Button mSendButton;             // Press to detonate
234     private EditText mSubjectTextEditor;    // Text editor for MMS subject
235 
236     private AttachmentEditor mAttachmentEditor;
237 
238     private MessageListView mMsgListView;        // ListView for messages in this conversation
239     public MessageListAdapter mMsgListAdapter;  // and its corresponding ListAdapter
240 
241     private RecipientsEditor mRecipientsEditor;  // UI control for editing recipients
242 
243     private boolean mIsKeyboardOpen;             // Whether the hardware keyboard is visible
244     private boolean mIsLandscape;                // Whether we're in landscape mode
245 
246     private boolean mPossiblePendingNotification;   // If the message list has changed, we may have
247                                                     // a pending notification to deal with.
248 
249     private boolean mToastForDraftSave;   // Whether to notify the user that a draft is being saved
250 
251     private boolean mSentMessage;       // true if the user has sent a message while in this
252                                         // activity. On a new compose message case, when the first
253                                         // message is sent is a MMS w/ attachment, the list blanks
254                                         // for a second before showing the sent message. But we'd
255                                         // think the message list is empty, thus show the recipients
256                                         // editor thinking it's a draft message. This flag should
257                                         // help clarify the situation.
258 
259     private WorkingMessage mWorkingMessage;         // The message currently being composed.
260 
261     private AlertDialog mSmileyDialog;
262 
263     private boolean mWaitingForSubActivity;
264     private int mLastRecipientCount;            // Used for warning the user on too many recipients.
265     private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter;
266 
267     private boolean mSendingMessage;    // Indicates the current message is sending, and shouldn't send again.
268 
269     private Intent mAddContactIntent;   // Intent used to add a new contact
270 
271     private String mDebugRecipients;
272 
273     @SuppressWarnings("unused")
log(String logMsg)274     private static void log(String logMsg) {
275         Thread current = Thread.currentThread();
276         long tid = current.getId();
277         StackTraceElement[] stack = current.getStackTrace();
278         String methodName = stack[3].getMethodName();
279         // Prepend current thread ID and name of calling method to the message.
280         logMsg = "[" + tid + "] [" + methodName + "] " + logMsg;
281         Log.d(TAG, logMsg);
282     }
283 
284     //==========================================================
285     // Inner classes
286     //==========================================================
287 
editSlideshow()288     private void editSlideshow() {
289         Uri dataUri = mWorkingMessage.saveAsMms(false);
290         Intent intent = new Intent(this, SlideshowEditActivity.class);
291         intent.setData(dataUri);
292         startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW);
293     }
294 
295     private final Handler mAttachmentEditorHandler = new Handler() {
296         @Override
297         public void handleMessage(Message msg) {
298             switch (msg.what) {
299                 case AttachmentEditor.MSG_EDIT_SLIDESHOW: {
300                     editSlideshow();
301                     break;
302                 }
303                 case AttachmentEditor.MSG_SEND_SLIDESHOW: {
304                     if (isPreparedForSending()) {
305                         ComposeMessageActivity.this.confirmSendMessageIfNeeded();
306                     }
307                     break;
308                 }
309                 case AttachmentEditor.MSG_VIEW_IMAGE:
310                 case AttachmentEditor.MSG_PLAY_VIDEO:
311                 case AttachmentEditor.MSG_PLAY_AUDIO:
312                 case AttachmentEditor.MSG_PLAY_SLIDESHOW:
313                     MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
314                             mWorkingMessage);
315                     break;
316 
317                 case AttachmentEditor.MSG_REPLACE_IMAGE:
318                 case AttachmentEditor.MSG_REPLACE_VIDEO:
319                 case AttachmentEditor.MSG_REPLACE_AUDIO:
320                     showAddAttachmentDialog(true);
321                     break;
322 
323                 case AttachmentEditor.MSG_REMOVE_ATTACHMENT:
324                     mWorkingMessage.setAttachment(WorkingMessage.TEXT, null, false);
325                     break;
326 
327                 default:
328                     break;
329             }
330         }
331     };
332 
333     private final Handler mMessageListItemHandler = new Handler() {
334         @Override
335         public void handleMessage(Message msg) {
336             String type;
337             switch (msg.what) {
338                 case MessageListItem.MSG_LIST_EDIT_MMS:
339                     type = "mms";
340                     break;
341                 case MessageListItem.MSG_LIST_EDIT_SMS:
342                     type = "sms";
343                     break;
344                 default:
345                     Log.w(TAG, "Unknown message: " + msg.what);
346                     return;
347             }
348 
349             MessageItem msgItem = getMessageItem(type, (Long) msg.obj, false);
350             if (msgItem != null) {
351                 editMessageItem(msgItem);
352                 drawBottomPanel();
353             }
354         }
355     };
356 
357     private final OnKeyListener mSubjectKeyListener = new OnKeyListener() {
358         public boolean onKey(View v, int keyCode, KeyEvent event) {
359             if (event.getAction() != KeyEvent.ACTION_DOWN) {
360                 return false;
361             }
362 
363             // When the subject editor is empty, press "DEL" to hide the input field.
364             if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) {
365                 showSubjectEditor(false);
366                 mWorkingMessage.setSubject(null, true);
367                 return true;
368             }
369 
370             return false;
371         }
372     };
373 
374     /**
375      * Return the messageItem associated with the type ("mms" or "sms") and message id.
376      * @param type Type of the message: "mms" or "sms"
377      * @param msgId Message id of the message. This is the _id of the sms or pdu row and is
378      * stored in the MessageItem
379      * @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's
380      * cache and the code can create a new MessageItem based on the position of the current cursor.
381      * If false, the function returns null if the MessageItem isn't in the cache.
382      * @return MessageItem or null if not found and createFromCursorIfNotInCache is false
383      */
getMessageItem(String type, long msgId, boolean createFromCursorIfNotInCache)384     private MessageItem getMessageItem(String type, long msgId,
385             boolean createFromCursorIfNotInCache) {
386         return mMsgListAdapter.getCachedMessageItem(type, msgId,
387                 createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null);
388     }
389 
isCursorValid()390     private boolean isCursorValid() {
391         // Check whether the cursor is valid or not.
392         Cursor cursor = mMsgListAdapter.getCursor();
393         if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
394             Log.e(TAG, "Bad cursor.", new RuntimeException());
395             return false;
396         }
397         return true;
398     }
399 
resetCounter()400     private void resetCounter() {
401         mTextCounter.setText("");
402         mTextCounter.setVisibility(View.GONE);
403     }
404 
updateCounter(CharSequence text, int start, int before, int count)405     private void updateCounter(CharSequence text, int start, int before, int count) {
406         WorkingMessage workingMessage = mWorkingMessage;
407         if (workingMessage.requiresMms()) {
408             // If we're not removing text (i.e. no chance of converting back to SMS
409             // because of this change) and we're in MMS mode, just bail out since we
410             // then won't have to calculate the length unnecessarily.
411             final boolean textRemoved = (before > count);
412             if (!textRemoved) {
413                 setSendButtonText(workingMessage.requiresMms());
414                 return;
415             }
416         }
417 
418         int[] params = SmsMessage.calculateLength(text, false);
419             /* SmsMessage.calculateLength returns an int[4] with:
420              *   int[0] being the number of SMS's required,
421              *   int[1] the number of code units used,
422              *   int[2] is the number of code units remaining until the next message.
423              *   int[3] is the encoding type that should be used for the message.
424              */
425         int msgCount = params[0];
426         int remainingInCurrentMessage = params[2];
427 
428         if (!MmsConfig.getMultipartSmsEnabled()) {
429             mWorkingMessage.setLengthRequiresMms(
430                     msgCount >= MmsConfig.getSmsToMmsTextThreshold(), true);
431         }
432 
433         // Show the counter only if:
434         // - We are not in MMS mode
435         // - We are going to send more than one message OR we are getting close
436         boolean showCounter = false;
437         if (!workingMessage.requiresMms() &&
438                 (msgCount > 1 ||
439                  remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) {
440             showCounter = true;
441         }
442 
443         setSendButtonText(workingMessage.requiresMms());
444 
445         if (showCounter) {
446             // Update the remaining characters and number of messages required.
447             String counterText = msgCount > 1 ? remainingInCurrentMessage + " / " + msgCount
448                     : String.valueOf(remainingInCurrentMessage);
449             mTextCounter.setText(counterText);
450             mTextCounter.setVisibility(View.VISIBLE);
451         } else {
452             mTextCounter.setVisibility(View.GONE);
453         }
454     }
455 
456     @Override
startActivityForResult(Intent intent, int requestCode)457     public void startActivityForResult(Intent intent, int requestCode)
458     {
459         // requestCode >= 0 means the activity in question is a sub-activity.
460         if (requestCode >= 0) {
461             mWaitingForSubActivity = true;
462         }
463 
464         super.startActivityForResult(intent, requestCode);
465     }
466 
toastConvertInfo(boolean toMms)467     private void toastConvertInfo(boolean toMms) {
468         final int resId = toMms ? R.string.converting_to_picture_message
469                 : R.string.converting_to_text_message;
470         Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
471     }
472 
473     private class DeleteMessageListener implements OnClickListener {
474         private final Uri mDeleteUri;
475         private final boolean mDeleteLocked;
476 
DeleteMessageListener(Uri uri, boolean deleteLocked)477         public DeleteMessageListener(Uri uri, boolean deleteLocked) {
478             mDeleteUri = uri;
479             mDeleteLocked = deleteLocked;
480         }
481 
DeleteMessageListener(long msgId, String type, boolean deleteLocked)482         public DeleteMessageListener(long msgId, String type, boolean deleteLocked) {
483             if ("mms".equals(type)) {
484                 mDeleteUri = ContentUris.withAppendedId(Mms.CONTENT_URI, msgId);
485             } else {
486                 mDeleteUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
487             }
488             mDeleteLocked = deleteLocked;
489         }
490 
onClick(DialogInterface dialog, int whichButton)491         public void onClick(DialogInterface dialog, int whichButton) {
492             mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN,
493                     null, mDeleteUri, mDeleteLocked ? null : "locked=0", null);
494             dialog.dismiss();
495         }
496     }
497 
498     private class DiscardDraftListener implements OnClickListener {
onClick(DialogInterface dialog, int whichButton)499         public void onClick(DialogInterface dialog, int whichButton) {
500             mWorkingMessage.discard();
501             dialog.dismiss();
502             finish();
503         }
504     }
505 
506     private class SendIgnoreInvalidRecipientListener implements OnClickListener {
onClick(DialogInterface dialog, int whichButton)507         public void onClick(DialogInterface dialog, int whichButton) {
508             sendMessage(true);
509             dialog.dismiss();
510         }
511     }
512 
513     private class CancelSendingListener implements OnClickListener {
onClick(DialogInterface dialog, int whichButton)514         public void onClick(DialogInterface dialog, int whichButton) {
515             if (isRecipientsEditorVisible()) {
516                 mRecipientsEditor.requestFocus();
517             }
518             dialog.dismiss();
519         }
520     }
521 
confirmSendMessageIfNeeded()522     private void confirmSendMessageIfNeeded() {
523         if (!isRecipientsEditorVisible()) {
524             sendMessage(true);
525             return;
526         }
527 
528         boolean isMms = mWorkingMessage.requiresMms();
529         if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
530             if (mRecipientsEditor.hasValidRecipient(isMms)) {
531                 String title = getResourcesString(R.string.has_invalid_recipient,
532                         mRecipientsEditor.formatInvalidNumbers(isMms));
533                 new AlertDialog.Builder(this)
534                     .setIcon(android.R.drawable.ic_dialog_alert)
535                     .setTitle(title)
536                     .setMessage(R.string.invalid_recipient_message)
537                     .setPositiveButton(R.string.try_to_send,
538                             new SendIgnoreInvalidRecipientListener())
539                     .setNegativeButton(R.string.no, new CancelSendingListener())
540                     .show();
541             } else {
542                 new AlertDialog.Builder(this)
543                     .setIcon(android.R.drawable.ic_dialog_alert)
544                     .setTitle(R.string.cannot_send_message)
545                     .setMessage(R.string.cannot_send_message_reason)
546                     .setPositiveButton(R.string.yes, new CancelSendingListener())
547                     .show();
548             }
549         } else {
550             sendMessage(true);
551         }
552     }
553 
554     private final TextWatcher mRecipientsWatcher = new TextWatcher() {
555         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
556         }
557 
558         public void onTextChanged(CharSequence s, int start, int before, int count) {
559             // This is a workaround for bug 1609057.  Since onUserInteraction() is
560             // not called when the user touches the soft keyboard, we pretend it was
561             // called when textfields changes.  This should be removed when the bug
562             // is fixed.
563             onUserInteraction();
564         }
565 
566         public void afterTextChanged(Editable s) {
567             // Bug 1474782 describes a situation in which we send to
568             // the wrong recipient.  We have been unable to reproduce this,
569             // but the best theory we have so far is that the contents of
570             // mRecipientList somehow become stale when entering
571             // ComposeMessageActivity via onNewIntent().  This assertion is
572             // meant to catch one possible path to that, of a non-visible
573             // mRecipientsEditor having its TextWatcher fire and refreshing
574             // mRecipientList with its stale contents.
575             if (!isRecipientsEditorVisible()) {
576                 IllegalStateException e = new IllegalStateException(
577                         "afterTextChanged called with invisible mRecipientsEditor");
578                 // Make sure the crash is uploaded to the service so we
579                 // can see if this is happening in the field.
580                 Log.w(TAG,
581                      "RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor");
582                 return;
583             }
584 
585             mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers());
586             mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true);
587 
588             checkForTooManyRecipients();
589 
590             // Walk backwards in the text box, skipping spaces.  If the last
591             // character is a comma, update the title bar.
592             for (int pos = s.length() - 1; pos >= 0; pos--) {
593                 char c = s.charAt(pos);
594                 if (c == ' ')
595                     continue;
596 
597                 if (c == ',') {
598                     updateTitle(mConversation.getRecipients());
599                 }
600 
601                 break;
602             }
603 
604             // If we have gone to zero recipients, disable send button.
605             updateSendButtonState();
606         }
607     };
608 
checkForTooManyRecipients()609     private void checkForTooManyRecipients() {
610         final int recipientLimit = MmsConfig.getRecipientLimit();
611         if (recipientLimit != Integer.MAX_VALUE) {
612             final int recipientCount = recipientCount();
613             boolean tooMany = recipientCount > recipientLimit;
614 
615             if (recipientCount != mLastRecipientCount) {
616                 // Don't warn the user on every character they type when they're over the limit,
617                 // only when the actual # of recipients changes.
618                 mLastRecipientCount = recipientCount;
619                 if (tooMany) {
620                     String tooManyMsg = getString(R.string.too_many_recipients, recipientCount,
621                             recipientLimit);
622                     Toast.makeText(ComposeMessageActivity.this,
623                             tooManyMsg, Toast.LENGTH_LONG).show();
624                 }
625             }
626         }
627     }
628 
629     private final OnCreateContextMenuListener mRecipientsMenuCreateListener =
630         new OnCreateContextMenuListener() {
631         public void onCreateContextMenu(ContextMenu menu, View v,
632                 ContextMenuInfo menuInfo) {
633             if (menuInfo != null) {
634                 Contact c = ((RecipientContextMenuInfo) menuInfo).recipient;
635                 RecipientsMenuClickListener l = new RecipientsMenuClickListener(c);
636 
637                 menu.setHeaderTitle(c.getName());
638 
639                 if (c.existsInDatabase()) {
640                     menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact)
641                             .setOnMenuItemClickListener(l);
642                 } else if (canAddToContacts(c)){
643                     menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
644                             .setOnMenuItemClickListener(l);
645                 }
646             }
647         }
648     };
649 
650     private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener {
651         private final Contact mRecipient;
652 
RecipientsMenuClickListener(Contact recipient)653         RecipientsMenuClickListener(Contact recipient) {
654             mRecipient = recipient;
655         }
656 
onMenuItemClick(MenuItem item)657         public boolean onMenuItemClick(MenuItem item) {
658             switch (item.getItemId()) {
659                 // Context menu handlers for the recipients editor.
660                 case MENU_VIEW_CONTACT: {
661                     Uri contactUri = mRecipient.getUri();
662                     Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
663                     intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
664                     startActivity(intent);
665                     return true;
666                 }
667                 case MENU_ADD_TO_CONTACTS: {
668                     mAddContactIntent = ConversationList.createAddContactIntent(
669                             mRecipient.getNumber());
670                     ComposeMessageActivity.this.startActivityForResult(mAddContactIntent,
671                             REQUEST_CODE_ADD_CONTACT);
672                     return true;
673                 }
674             }
675             return false;
676         }
677     }
678 
canAddToContacts(Contact contact)679     private boolean canAddToContacts(Contact contact) {
680         // There are some kind of automated messages, like STK messages, that we don't want
681         // to add to contacts. These names begin with special characters, like, "*Info".
682         final String name = contact.getName();
683         if (!TextUtils.isEmpty(contact.getNumber())) {
684             char c = contact.getNumber().charAt(0);
685             if (isSpecialChar(c)) {
686                 return false;
687             }
688         }
689         if (!TextUtils.isEmpty(name)) {
690             char c = name.charAt(0);
691             if (isSpecialChar(c)) {
692                 return false;
693             }
694         }
695         if (!(Mms.isEmailAddress(name) || Mms.isPhoneNumber(name) ||
696                 MessageUtils.isLocalNumber(contact.getNumber()))) {     // Handle "Me"
697             return false;
698         }
699         return true;
700     }
701 
isSpecialChar(char c)702     private boolean isSpecialChar(char c) {
703         return c == '*' || c == '%' || c == '$';
704     }
705 
addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo)706     private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
707         AdapterView.AdapterContextMenuInfo info;
708 
709         try {
710             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
711         } catch (ClassCastException e) {
712             Log.e(TAG, "bad menuInfo");
713             return;
714         }
715         final int position = info.position;
716 
717         addUriSpecificMenuItems(menu, v, position);
718     }
719 
getSelectedUriFromMessageList(ListView listView, int position)720     private Uri getSelectedUriFromMessageList(ListView listView, int position) {
721         // If the context menu was opened over a uri, get that uri.
722         MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position);
723         if (msglistItem == null) {
724             // FIXME: Should get the correct view. No such interface in ListView currently
725             // to get the view by position. The ListView.getChildAt(position) cannot
726             // get correct view since the list doesn't create one child for each item.
727             // And if setSelection(position) then getSelectedView(),
728             // cannot get corrent view when in touch mode.
729             return null;
730         }
731 
732         TextView textView;
733         CharSequence text = null;
734         int selStart = -1;
735         int selEnd = -1;
736 
737         //check if message sender is selected
738         textView = (TextView) msglistItem.findViewById(R.id.text_view);
739         if (textView != null) {
740             text = textView.getText();
741             selStart = textView.getSelectionStart();
742             selEnd = textView.getSelectionEnd();
743         }
744 
745         if (selStart == -1) {
746             //sender is not being selected, it may be within the message body
747             textView = (TextView) msglistItem.findViewById(R.id.body_text_view);
748             if (textView != null) {
749                 text = textView.getText();
750                 selStart = textView.getSelectionStart();
751                 selEnd = textView.getSelectionEnd();
752             }
753         }
754 
755         // Check that some text is actually selected, rather than the cursor
756         // just being placed within the TextView.
757         if (selStart != selEnd) {
758             int min = Math.min(selStart, selEnd);
759             int max = Math.max(selStart, selEnd);
760 
761             URLSpan[] urls = ((Spanned) text).getSpans(min, max,
762                                                         URLSpan.class);
763 
764             if (urls.length == 1) {
765                 return Uri.parse(urls[0].getURL());
766             }
767         }
768 
769         //no uri was selected
770         return null;
771     }
772 
addUriSpecificMenuItems(ContextMenu menu, View v, int position)773     private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) {
774         Uri uri = getSelectedUriFromMessageList((ListView) v, position);
775 
776         if (uri != null) {
777             Intent intent = new Intent(null, uri);
778             intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
779             menu.addIntentOptions(0, 0, 0,
780                     new android.content.ComponentName(this, ComposeMessageActivity.class),
781                     null, intent, 0, null);
782         }
783     }
784 
addCallAndContactMenuItems( ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem)785     private final void addCallAndContactMenuItems(
786             ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) {
787         // Add all possible links in the address & message
788         StringBuilder textToSpannify = new StringBuilder();
789         if (msgItem.mBoxId == Mms.MESSAGE_BOX_INBOX) {
790             textToSpannify.append(msgItem.mAddress + ": ");
791         }
792         textToSpannify.append(msgItem.mBody);
793 
794         SpannableString msg = new SpannableString(textToSpannify.toString());
795         Linkify.addLinks(msg, Linkify.ALL);
796         ArrayList<String> uris =
797             MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class));
798 
799         while (uris.size() > 0) {
800             String uriString = uris.remove(0);
801             // Remove any dupes so they don't get added to the menu multiple times
802             while (uris.contains(uriString)) {
803                 uris.remove(uriString);
804             }
805 
806             int sep = uriString.indexOf(":");
807             String prefix = null;
808             if (sep >= 0) {
809                 prefix = uriString.substring(0, sep);
810                 uriString = uriString.substring(sep + 1);
811             }
812             boolean addToContacts = false;
813             if ("mailto".equalsIgnoreCase(prefix))  {
814                 String sendEmailString = getString(
815                         R.string.menu_send_email).replace("%s", uriString);
816                 Intent intent = new Intent(Intent.ACTION_VIEW,
817                         Uri.parse("mailto:" + uriString));
818                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
819                 menu.add(0, MENU_SEND_EMAIL, 0, sendEmailString)
820                     .setOnMenuItemClickListener(l)
821                     .setIntent(intent);
822                 addToContacts = !haveEmailContact(uriString);
823             } else if ("tel".equalsIgnoreCase(prefix)) {
824                 String callBackString = getString(
825                         R.string.menu_call_back).replace("%s", uriString);
826                 Intent intent = new Intent(Intent.ACTION_CALL,
827                         Uri.parse("tel:" + uriString));
828                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
829                 menu.add(0, MENU_CALL_BACK, 0, callBackString)
830                     .setOnMenuItemClickListener(l)
831                     .setIntent(intent);
832                 addToContacts = !isNumberInContacts(uriString);
833             }
834             if (addToContacts) {
835                 Intent intent = ConversationList.createAddContactIntent(uriString);
836                 String addContactString = getString(
837                         R.string.menu_add_address_to_contacts).replace("%s", uriString);
838                 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString)
839                     .setOnMenuItemClickListener(l)
840                     .setIntent(intent);
841             }
842         }
843     }
844 
haveEmailContact(String emailAddress)845     private boolean haveEmailContact(String emailAddress) {
846         Cursor cursor = SqliteWrapper.query(this, getContentResolver(),
847                 Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
848                 new String[] { Contacts.DISPLAY_NAME }, null, null, null);
849 
850         if (cursor != null) {
851             try {
852                 while (cursor.moveToNext()) {
853                     String name = cursor.getString(0);
854                     if (!TextUtils.isEmpty(name)) {
855                         return true;
856                     }
857                 }
858             } finally {
859                 cursor.close();
860             }
861         }
862         return false;
863     }
864 
isNumberInContacts(String phoneNumber)865     private boolean isNumberInContacts(String phoneNumber) {
866         return Contact.get(phoneNumber, false).existsInDatabase();
867     }
868 
869     private final OnCreateContextMenuListener mMsgListMenuCreateListener =
870         new OnCreateContextMenuListener() {
871         public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
872             Cursor cursor = mMsgListAdapter.getCursor();
873             String type = cursor.getString(COLUMN_MSG_TYPE);
874             long msgId = cursor.getLong(COLUMN_ID);
875 
876             addPositionBasedMenuItems(menu, v, menuInfo);
877 
878             MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor);
879             if (msgItem == null) {
880                 Log.e(TAG, "Cannot load message item for type = " + type
881                         + ", msgId = " + msgId);
882                 return;
883             }
884 
885             menu.setHeaderTitle(R.string.message_options);
886 
887             MsgListMenuClickListener l = new MsgListMenuClickListener();
888 
889             if (msgItem.mLocked) {
890                 menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock)
891                     .setOnMenuItemClickListener(l);
892             } else {
893                 menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock)
894                     .setOnMenuItemClickListener(l);
895             }
896 
897             if (msgItem.isMms()) {
898                 switch (msgItem.mBoxId) {
899                     case Mms.MESSAGE_BOX_INBOX:
900                         break;
901                     case Mms.MESSAGE_BOX_OUTBOX:
902                         // Since we currently break outgoing messages to multiple
903                         // recipients into one message per recipient, only allow
904                         // editing a message for single-recipient conversations.
905                         if (getRecipients().size() == 1) {
906                             menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
907                                     .setOnMenuItemClickListener(l);
908                         }
909                         break;
910                 }
911                 switch (msgItem.mAttachmentType) {
912                     case WorkingMessage.TEXT:
913                         break;
914                     case WorkingMessage.VIDEO:
915                     case WorkingMessage.IMAGE:
916                         if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
917                             menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
918                             .setOnMenuItemClickListener(l);
919                         }
920                         break;
921                     case WorkingMessage.SLIDESHOW:
922                     default:
923                         menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow)
924                         .setOnMenuItemClickListener(l);
925                         if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
926                             menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
927                             .setOnMenuItemClickListener(l);
928                         }
929                         if (haveSomethingToCopyToDrmProvider(msgItem.mMsgId)) {
930                             menu.add(0, MENU_COPY_TO_DRM_PROVIDER, 0,
931                                     getDrmMimeMenuStringRsrc(msgItem.mMsgId))
932                             .setOnMenuItemClickListener(l);
933                         }
934                         break;
935                 }
936             } else {
937                 // Message type is sms. Only allow "edit" if the message has a single recipient
938                 if (getRecipients().size() == 1 &&
939                         (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX ||
940                         msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) {
941                     menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
942                             .setOnMenuItemClickListener(l);
943                 }
944             }
945 
946             addCallAndContactMenuItems(menu, l, msgItem);
947 
948             // Forward is not available for undownloaded messages.
949             if (msgItem.isDownloaded()) {
950                 menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward)
951                         .setOnMenuItemClickListener(l);
952             }
953 
954             // It is unclear what would make most sense for copying an MMS message
955             // to the clipboard, so we currently do SMS only.
956             if (msgItem.isSms()) {
957                 menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text)
958                         .setOnMenuItemClickListener(l);
959             }
960 
961             menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details)
962                     .setOnMenuItemClickListener(l);
963             menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message)
964                     .setOnMenuItemClickListener(l);
965             if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || msgItem.mReadReport) {
966                 menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report)
967                         .setOnMenuItemClickListener(l);
968             }
969         }
970     };
971 
editMessageItem(MessageItem msgItem)972     private void editMessageItem(MessageItem msgItem) {
973         if ("sms".equals(msgItem.mType)) {
974             editSmsMessageItem(msgItem);
975         } else {
976             editMmsMessageItem(msgItem);
977         }
978         if (msgItem.isFailedMessage() && mMsgListAdapter.getCount() <= 1) {
979             // For messages with bad addresses, let the user re-edit the recipients.
980             initRecipientsEditor();
981         }
982     }
983 
editSmsMessageItem(MessageItem msgItem)984     private void editSmsMessageItem(MessageItem msgItem) {
985         // When the message being edited is the only message in the conversation, the delete
986         // below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a
987         // thread contains no messages and silently deletes the thread. Meanwhile, the mConversation
988         // object still holds onto the old thread_id and code thinks there's a backing thread in
989         // the DB when it really has been deleted. Here we try and notice that situation and
990         // clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll
991         // create a new thread if necessary.
992         synchronized(mConversation) {
993             if (mConversation.getMessageCount() <= 1) {
994                 mConversation.clearThreadId();
995             }
996         }
997         // Delete the old undelivered SMS and load its content.
998         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId);
999         SqliteWrapper.delete(ComposeMessageActivity.this,
1000                 mContentResolver, uri, null, null);
1001 
1002         mWorkingMessage.setText(msgItem.mBody);
1003     }
1004 
editMmsMessageItem(MessageItem msgItem)1005     private void editMmsMessageItem(MessageItem msgItem) {
1006         // Discard the current message in progress.
1007         mWorkingMessage.discard();
1008 
1009         // Load the selected message in as the working message.
1010         mWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri);
1011         mWorkingMessage.setConversation(mConversation);
1012 
1013         mAttachmentEditor.update(mWorkingMessage);
1014         drawTopPanel();
1015 
1016         // WorkingMessage.load() above only loads the slideshow. Set the
1017         // subject here because we already know what it is and avoid doing
1018         // another DB lookup in load() just to get it.
1019         mWorkingMessage.setSubject(msgItem.mSubject, false);
1020 
1021         if (mWorkingMessage.hasSubject()) {
1022             showSubjectEditor(true);
1023         }
1024     }
1025 
copyToClipboard(String str)1026     private void copyToClipboard(String str) {
1027         ClipboardManager clip =
1028             (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
1029         clip.setText(str);
1030     }
1031 
forwardMessage(MessageItem msgItem)1032     private void forwardMessage(MessageItem msgItem) {
1033         Intent intent = createIntent(this, 0);
1034 
1035         intent.putExtra("exit_on_sent", true);
1036         intent.putExtra("forwarded_message", true);
1037 
1038         if (msgItem.mType.equals("sms")) {
1039             intent.putExtra("sms_body", msgItem.mBody);
1040         } else {
1041             SendReq sendReq = new SendReq();
1042             String subject = getString(R.string.forward_prefix);
1043             if (msgItem.mSubject != null) {
1044                 subject += msgItem.mSubject;
1045             }
1046             sendReq.setSubject(new EncodedStringValue(subject));
1047             sendReq.setBody(msgItem.mSlideshow.makeCopy(
1048                     ComposeMessageActivity.this));
1049 
1050             Uri uri = null;
1051             try {
1052                 PduPersister persister = PduPersister.getPduPersister(this);
1053                 // Copy the parts of the message here.
1054                 uri = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
1055             } catch (MmsException e) {
1056                 Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri, e);
1057                 Toast.makeText(ComposeMessageActivity.this,
1058                         R.string.cannot_save_message, Toast.LENGTH_SHORT).show();
1059                 return;
1060             }
1061 
1062             intent.putExtra("msg_uri", uri);
1063             intent.putExtra("subject", subject);
1064         }
1065         // ForwardMessageActivity is simply an alias in the manifest for ComposeMessageActivity.
1066         // We have to make an alias because ComposeMessageActivity launch flags specify
1067         // singleTop. When we forward a message, we want to start a separate ComposeMessageActivity.
1068         // The only way to do that is to override the singleTop flag, which is impossible to do
1069         // in code. By creating an alias to the activity, without the singleTop flag, we can
1070         // launch a separate ComposeMessageActivity to edit the forward message.
1071         intent.setClassName(this, "com.android.mms.ui.ForwardMessageActivity");
1072         startActivity(intent);
1073     }
1074 
1075     /**
1076      * Context menu handlers for the message list view.
1077      */
1078     private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener {
onMenuItemClick(MenuItem item)1079         public boolean onMenuItemClick(MenuItem item) {
1080             if (!isCursorValid()) {
1081                 return false;
1082             }
1083             Cursor cursor = mMsgListAdapter.getCursor();
1084             String type = cursor.getString(COLUMN_MSG_TYPE);
1085             long msgId = cursor.getLong(COLUMN_ID);
1086             MessageItem msgItem = getMessageItem(type, msgId, true);
1087 
1088             if (msgItem == null) {
1089                 return false;
1090             }
1091 
1092             switch (item.getItemId()) {
1093                 case MENU_EDIT_MESSAGE:
1094                     editMessageItem(msgItem);
1095                     drawBottomPanel();
1096                     return true;
1097 
1098                 case MENU_COPY_MESSAGE_TEXT:
1099                     copyToClipboard(msgItem.mBody);
1100                     return true;
1101 
1102                 case MENU_FORWARD_MESSAGE:
1103                     forwardMessage(msgItem);
1104                     return true;
1105 
1106                 case MENU_VIEW_SLIDESHOW:
1107                     MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
1108                             ContentUris.withAppendedId(Mms.CONTENT_URI, msgId), null);
1109                     return true;
1110 
1111                 case MENU_VIEW_MESSAGE_DETAILS: {
1112                     String messageDetails = MessageUtils.getMessageDetails(
1113                             ComposeMessageActivity.this, cursor, msgItem.mMessageSize);
1114                     new AlertDialog.Builder(ComposeMessageActivity.this)
1115                             .setTitle(R.string.message_details_title)
1116                             .setMessage(messageDetails)
1117                             .setPositiveButton(android.R.string.ok, null)
1118                             .setCancelable(true)
1119                             .show();
1120                     return true;
1121                 }
1122                 case MENU_DELETE_MESSAGE: {
1123                     DeleteMessageListener l = new DeleteMessageListener(
1124                             msgItem.mMessageUri, msgItem.mLocked);
1125                     confirmDeleteDialog(l, msgItem.mLocked);
1126                     return true;
1127                 }
1128                 case MENU_DELIVERY_REPORT:
1129                     showDeliveryReport(msgId, type);
1130                     return true;
1131 
1132                 case MENU_COPY_TO_SDCARD: {
1133                     int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success :
1134                         R.string.copy_to_sdcard_fail;
1135                     Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1136                     return true;
1137                 }
1138 
1139                 case MENU_COPY_TO_DRM_PROVIDER: {
1140                     int resId = getDrmMimeSavedStringRsrc(msgId, copyToDrmProvider(msgId));
1141                     Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1142                     return true;
1143                 }
1144 
1145                 case MENU_LOCK_MESSAGE: {
1146                     lockMessage(msgItem, true);
1147                     return true;
1148                 }
1149 
1150                 case MENU_UNLOCK_MESSAGE: {
1151                     lockMessage(msgItem, false);
1152                     return true;
1153                 }
1154 
1155                 default:
1156                     return false;
1157             }
1158         }
1159     }
1160 
lockMessage(MessageItem msgItem, boolean locked)1161     private void lockMessage(MessageItem msgItem, boolean locked) {
1162         Uri uri;
1163         if ("sms".equals(msgItem.mType)) {
1164             uri = Sms.CONTENT_URI;
1165         } else {
1166             uri = Mms.CONTENT_URI;
1167         }
1168         final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);
1169 
1170         final ContentValues values = new ContentValues(1);
1171         values.put("locked", locked ? 1 : 0);
1172 
1173         new Thread(new Runnable() {
1174             public void run() {
1175                 getContentResolver().update(lockUri,
1176                         values, null, null);
1177             }
1178         }, "lockMessage").start();
1179     }
1180 
1181     /**
1182      * Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
1183      * @param msgId
1184      */
haveSomethingToCopyToSDCard(long msgId)1185     private boolean haveSomethingToCopyToSDCard(long msgId) {
1186         PduBody body = PduBodyCache.getPduBody(this,
1187                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1188         if (body == null) {
1189             return false;
1190         }
1191 
1192         boolean result = false;
1193         int partNum = body.getPartsNum();
1194         for(int i = 0; i < partNum; i++) {
1195             PduPart part = body.getPart(i);
1196             String type = new String(part.getContentType());
1197 
1198             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1199                 log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
1200             }
1201 
1202             if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1203                     ContentType.isAudioType(type)) {
1204                 result = true;
1205                 break;
1206             }
1207         }
1208         return result;
1209     }
1210 
1211     /**
1212      * Looks to see if there are any drm'd parts of the attachment that can be copied to the
1213      * DrmProvider. Right now we only support saving audio (e.g. ringtones).
1214      * @param msgId
1215      */
haveSomethingToCopyToDrmProvider(long msgId)1216     private boolean haveSomethingToCopyToDrmProvider(long msgId) {
1217         String mimeType = getDrmMimeType(msgId);
1218         return isAudioMimeType(mimeType);
1219     }
1220 
1221     /**
1222      * Simple cache to prevent having to load the same PduBody again and again for the same uri.
1223      */
1224     private static class PduBodyCache {
1225         private static PduBody mLastPduBody;
1226         private static Uri mLastUri;
1227 
getPduBody(Context context, Uri contentUri)1228         static public PduBody getPduBody(Context context, Uri contentUri) {
1229             if (contentUri.equals(mLastUri)) {
1230                 return mLastPduBody;
1231             }
1232             try {
1233                 mLastPduBody = SlideshowModel.getPduBody(context, contentUri);
1234                 mLastUri = contentUri;
1235              } catch (MmsException e) {
1236                  Log.e(TAG, e.getMessage(), e);
1237                  return null;
1238              }
1239              return mLastPduBody;
1240         }
1241     };
1242 
1243     /**
1244      * Copies media from an Mms to the DrmProvider
1245      * @param msgId
1246      */
copyToDrmProvider(long msgId)1247     private boolean copyToDrmProvider(long msgId) {
1248         boolean result = true;
1249         PduBody body = PduBodyCache.getPduBody(this,
1250                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1251         if (body == null) {
1252             return false;
1253         }
1254 
1255         int partNum = body.getPartsNum();
1256         for(int i = 0; i < partNum; i++) {
1257             PduPart part = body.getPart(i);
1258             String type = new String(part.getContentType());
1259 
1260             if (ContentType.isDrmType(type)) {
1261                 // All parts (but there's probably only a single one) have to be successful
1262                 // for a valid result.
1263                 result &= copyPartToDrmProvider(part);
1264             }
1265         }
1266         return result;
1267     }
1268 
mimeTypeOfDrmPart(PduPart part)1269     private String mimeTypeOfDrmPart(PduPart part) {
1270         Uri uri = part.getDataUri();
1271         InputStream input = null;
1272         try {
1273             input = mContentResolver.openInputStream(uri);
1274             if (input instanceof FileInputStream) {
1275                 FileInputStream fin = (FileInputStream) input;
1276 
1277                 DrmRawContent content = new DrmRawContent(fin, fin.available(),
1278                         DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING);
1279                 String mimeType = content.getContentType();
1280                 return mimeType;
1281             }
1282         } catch (IOException e) {
1283             // Ignore
1284             Log.e(TAG, "IOException caught while opening or reading stream", e);
1285         } catch (DrmException e) {
1286             Log.e(TAG, "DrmException caught ", e);
1287         } finally {
1288             if (null != input) {
1289                 try {
1290                     input.close();
1291                 } catch (IOException e) {
1292                     // Ignore
1293                     Log.e(TAG, "IOException caught while closing stream", e);
1294                 }
1295             }
1296         }
1297         return null;
1298     }
1299 
1300     /**
1301      * Returns the type of the first drm'd pdu part.
1302      * @param msgId
1303      */
getDrmMimeType(long msgId)1304     private String getDrmMimeType(long msgId) {
1305         PduBody body = PduBodyCache.getPduBody(this,
1306                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1307         if (body == null) {
1308             return null;
1309         }
1310 
1311         int partNum = body.getPartsNum();
1312         for(int i = 0; i < partNum; i++) {
1313             PduPart part = body.getPart(i);
1314             String type = new String(part.getContentType());
1315 
1316             if (ContentType.isDrmType(type)) {
1317                 return mimeTypeOfDrmPart(part);
1318             }
1319         }
1320         return null;
1321     }
1322 
getDrmMimeMenuStringRsrc(long msgId)1323     private int getDrmMimeMenuStringRsrc(long msgId) {
1324         String mimeType = getDrmMimeType(msgId);
1325         if (isAudioMimeType(mimeType)) {
1326             return R.string.save_ringtone;
1327         }
1328         return 0;
1329     }
1330 
getDrmMimeSavedStringRsrc(long msgId, boolean success)1331     private int getDrmMimeSavedStringRsrc(long msgId, boolean success) {
1332         String mimeType = getDrmMimeType(msgId);
1333         if (isAudioMimeType(mimeType)) {
1334             return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
1335         }
1336         return 0;
1337     }
1338 
isAudioMimeType(String mimeType)1339     private boolean isAudioMimeType(String mimeType) {
1340         return mimeType != null && mimeType.startsWith("audio/");
1341     }
1342 
isImageMimeType(String mimeType)1343     private boolean isImageMimeType(String mimeType) {
1344         return mimeType != null && mimeType.startsWith("image/");
1345     }
1346 
copyPartToDrmProvider(PduPart part)1347     private boolean copyPartToDrmProvider(PduPart part) {
1348         Uri uri = part.getDataUri();
1349 
1350         InputStream input = null;
1351         try {
1352             input = mContentResolver.openInputStream(uri);
1353             if (input instanceof FileInputStream) {
1354                 FileInputStream fin = (FileInputStream) input;
1355 
1356                 // Build a nice title
1357                 byte[] location = part.getName();
1358                 if (location == null) {
1359                     location = part.getFilename();
1360                 }
1361                 if (location == null) {
1362                     location = part.getContentLocation();
1363                 }
1364 
1365                 // Depending on the location, there may be an
1366                 // extension already on the name or not
1367                 String title = new String(location);
1368                 int index;
1369                 if ((index = title.indexOf(".")) == -1) {
1370                     String type = new String(part.getContentType());
1371                 } else {
1372                     title = title.substring(0, index);
1373                 }
1374 
1375                 // transfer the file to the DRM content provider
1376                 Intent item = DrmStore.addDrmFile(mContentResolver, fin, title);
1377                 if (item == null) {
1378                     Log.w(TAG, "unable to add file " + uri + " to DrmProvider");
1379                     return false;
1380                 }
1381             }
1382         } catch (IOException e) {
1383             // Ignore
1384             Log.e(TAG, "IOException caught while opening or reading stream", e);
1385             return false;
1386         } finally {
1387             if (null != input) {
1388                 try {
1389                     input.close();
1390                 } catch (IOException e) {
1391                     // Ignore
1392                     Log.e(TAG, "IOException caught while closing stream", e);
1393                     return false;
1394                 }
1395             }
1396         }
1397         return true;
1398     }
1399 
1400     /**
1401      * Copies media from an Mms to the "download" directory on the SD card
1402      * @param msgId
1403      */
copyMedia(long msgId)1404     private boolean copyMedia(long msgId) {
1405         boolean result = true;
1406         PduBody body = PduBodyCache.getPduBody(this,
1407                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1408         if (body == null) {
1409             return false;
1410         }
1411 
1412         int partNum = body.getPartsNum();
1413         for(int i = 0; i < partNum; i++) {
1414             PduPart part = body.getPart(i);
1415             String type = new String(part.getContentType());
1416 
1417             if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1418                     ContentType.isAudioType(type)) {
1419                 result &= copyPart(part, Long.toHexString(msgId));   // all parts have to be successful for a valid result.
1420             }
1421         }
1422         return result;
1423     }
1424 
copyPart(PduPart part, String fallback)1425     private boolean copyPart(PduPart part, String fallback) {
1426         Uri uri = part.getDataUri();
1427 
1428         InputStream input = null;
1429         FileOutputStream fout = null;
1430         try {
1431             input = mContentResolver.openInputStream(uri);
1432             if (input instanceof FileInputStream) {
1433                 FileInputStream fin = (FileInputStream) input;
1434 
1435                 byte[] location = part.getName();
1436                 if (location == null) {
1437                     location = part.getFilename();
1438                 }
1439                 if (location == null) {
1440                     location = part.getContentLocation();
1441                 }
1442 
1443                 String fileName;
1444                 if (location == null) {
1445                     // Use fallback name.
1446                     fileName = fallback;
1447                 } else {
1448                     fileName = new String(location);
1449                 }
1450                 // Depending on the location, there may be an
1451                 // extension already on the name or not
1452                 String dir = Environment.getExternalStorageDirectory() + "/"
1453                                 + Environment.DIRECTORY_DOWNLOADS  + "/";
1454                 String extension;
1455                 int index;
1456                 if ((index = fileName.indexOf(".")) == -1) {
1457                     String type = new String(part.getContentType());
1458                     extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
1459                 } else {
1460                     extension = fileName.substring(index + 1, fileName.length());
1461                     fileName = fileName.substring(0, index);
1462                 }
1463 
1464                 File file = getUniqueDestination(dir + fileName, extension);
1465 
1466                 // make sure the path is valid and directories created for this file.
1467                 File parentFile = file.getParentFile();
1468                 if (!parentFile.exists() && !parentFile.mkdirs()) {
1469                     Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
1470                     return false;
1471                 }
1472 
1473                 fout = new FileOutputStream(file);
1474 
1475                 byte[] buffer = new byte[8000];
1476                 int size = 0;
1477                 while ((size=fin.read(buffer)) != -1) {
1478                     fout.write(buffer, 0, size);
1479                 }
1480 
1481                 // Notify other applications listening to scanner events
1482                 // that a media file has been added to the sd card
1483                 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
1484                         Uri.fromFile(file)));
1485             }
1486         } catch (IOException e) {
1487             // Ignore
1488             Log.e(TAG, "IOException caught while opening or reading stream", e);
1489             return false;
1490         } finally {
1491             if (null != input) {
1492                 try {
1493                     input.close();
1494                 } catch (IOException e) {
1495                     // Ignore
1496                     Log.e(TAG, "IOException caught while closing stream", e);
1497                     return false;
1498                 }
1499             }
1500             if (null != fout) {
1501                 try {
1502                     fout.close();
1503                 } catch (IOException e) {
1504                     // Ignore
1505                     Log.e(TAG, "IOException caught while closing stream", e);
1506                     return false;
1507                 }
1508             }
1509         }
1510         return true;
1511     }
1512 
getUniqueDestination(String base, String extension)1513     private File getUniqueDestination(String base, String extension) {
1514         File file = new File(base + "." + extension);
1515 
1516         for (int i = 2; file.exists(); i++) {
1517             file = new File(base + "_" + i + "." + extension);
1518         }
1519         return file;
1520     }
1521 
showDeliveryReport(long messageId, String type)1522     private void showDeliveryReport(long messageId, String type) {
1523         Intent intent = new Intent(this, DeliveryReportActivity.class);
1524         intent.putExtra("message_id", messageId);
1525         intent.putExtra("message_type", type);
1526 
1527         startActivity(intent);
1528     }
1529 
1530     private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION);
1531 
1532     private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() {
1533         @Override
1534         public void onReceive(Context context, Intent intent) {
1535             if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) {
1536                 long token = intent.getLongExtra("token",
1537                                     SendingProgressTokenManager.NO_TOKEN);
1538                 if (token != mConversation.getThreadId()) {
1539                     return;
1540                 }
1541 
1542                 int progress = intent.getIntExtra("progress", 0);
1543                 switch (progress) {
1544                     case PROGRESS_START:
1545                         setProgressBarVisibility(true);
1546                         break;
1547                     case PROGRESS_ABORT:
1548                     case PROGRESS_COMPLETE:
1549                         setProgressBarVisibility(false);
1550                         break;
1551                     default:
1552                         setProgress(100 * progress);
1553                 }
1554             }
1555         }
1556     };
1557 
1558     private static ContactList sEmptyContactList;
1559 
getRecipients()1560     private ContactList getRecipients() {
1561         // If the recipients editor is visible, the conversation has
1562         // not really officially 'started' yet.  Recipients will be set
1563         // on the conversation once it has been saved or sent.  In the
1564         // meantime, let anyone who needs the recipient list think it
1565         // is empty rather than giving them a stale one.
1566         if (isRecipientsEditorVisible()) {
1567             if (sEmptyContactList == null) {
1568                 sEmptyContactList = new ContactList();
1569             }
1570             return sEmptyContactList;
1571         }
1572         return mConversation.getRecipients();
1573     }
1574 
updateTitle(ContactList list)1575     private void updateTitle(ContactList list) {
1576         String s;
1577         switch (list.size()) {
1578             case 0: {
1579                 String recipient = "";
1580                 if (mRecipientsEditor != null) {
1581                     recipient = mRecipientsEditor.getText().toString();
1582                 }
1583                 s = recipient;
1584                 break;
1585             }
1586             case 1: {
1587                 s = list.get(0).getNameAndNumber();
1588                 break;
1589             }
1590             default: {
1591                 // Handle multiple recipients
1592                 s = list.formatNames(", ");
1593                 break;
1594             }
1595         }
1596         mDebugRecipients = list.serialize();
1597         getWindow().setTitle(s);
1598     }
1599 
1600     // Get the recipients editor ready to be displayed onscreen.
initRecipientsEditor()1601     private void initRecipientsEditor() {
1602         if (isRecipientsEditorVisible()) {
1603             return;
1604         }
1605         // Must grab the recipients before the view is made visible because getRecipients()
1606         // returns empty recipients when the editor is visible.
1607         ContactList recipients = getRecipients();
1608 
1609         ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub);
1610         if (stub != null) {
1611             mRecipientsEditor = (RecipientsEditor) stub.inflate();
1612         } else {
1613             mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
1614             mRecipientsEditor.setVisibility(View.VISIBLE);
1615         }
1616 
1617         mRecipientsEditor.setAdapter(new RecipientsAdapter(this));
1618         mRecipientsEditor.populate(recipients);
1619         mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener);
1620         mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
1621         mRecipientsEditor.setFilters(new InputFilter[] {
1622                 new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) });
1623         mRecipientsEditor.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1624             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1625                 // After the user selects an item in the pop-up contacts list, move the
1626                 // focus to the text editor if there is only one recipient.  This helps
1627                 // the common case of selecting one recipient and then typing a message,
1628                 // but avoids annoying a user who is trying to add five recipients and
1629                 // keeps having focus stolen away.
1630                 if (mRecipientsEditor.getRecipientCount() == 1) {
1631                     // if we're in extract mode then don't request focus
1632                     final InputMethodManager inputManager = (InputMethodManager)
1633                         getSystemService(Context.INPUT_METHOD_SERVICE);
1634                     if (inputManager == null || !inputManager.isFullscreenMode()) {
1635                         mTextEditor.requestFocus();
1636                     }
1637                 }
1638             }
1639         });
1640 
1641         mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1642             public void onFocusChange(View v, boolean hasFocus) {
1643                 if (!hasFocus) {
1644                     RecipientsEditor editor = (RecipientsEditor) v;
1645                     ContactList contacts = editor.constructContactsFromInput();
1646                     updateTitle(contacts);
1647                 }
1648             }
1649         });
1650 
1651         mTopPanel.setVisibility(View.VISIBLE);
1652     }
1653 
1654     //==========================================================
1655     // Activity methods
1656     //==========================================================
1657 
cancelFailedToDeliverNotification(Intent intent, Context context)1658     public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) {
1659         if (MessagingNotification.isFailedToDeliver(intent)) {
1660             // Cancel any failed message notifications
1661             MessagingNotification.cancelNotification(context,
1662                         MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID);
1663             return true;
1664         }
1665         return false;
1666     }
1667 
cancelFailedDownloadNotification(Intent intent, Context context)1668     public static boolean cancelFailedDownloadNotification(Intent intent, Context context) {
1669         if (MessagingNotification.isFailedToDownload(intent)) {
1670             // Cancel any failed download notifications
1671             MessagingNotification.cancelNotification(context,
1672                         MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID);
1673             return true;
1674         }
1675         return false;
1676     }
1677 
1678     @Override
onCreate(Bundle savedInstanceState)1679     protected void onCreate(Bundle savedInstanceState) {
1680         super.onCreate(savedInstanceState);
1681 
1682         setContentView(R.layout.compose_message_activity);
1683         setProgressBarVisibility(false);
1684 
1685         getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1686                 WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
1687 
1688         // Initialize members for UI elements.
1689         initResourceRefs();
1690 
1691         mContentResolver = getContentResolver();
1692         mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
1693 
1694         initialize(savedInstanceState, 0);
1695 
1696         if (TRACE) {
1697             android.os.Debug.startMethodTracing("compose");
1698         }
1699     }
1700 
showSubjectEditor(boolean show)1701     private void showSubjectEditor(boolean show) {
1702         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1703             log("" + show);
1704         }
1705 
1706         if (mSubjectTextEditor == null) {
1707             // Don't bother to initialize the subject editor if
1708             // we're just going to hide it.
1709             if (show == false) {
1710                 return;
1711             }
1712             mSubjectTextEditor = (EditText)findViewById(R.id.subject);
1713         }
1714 
1715         mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null);
1716 
1717         if (show) {
1718             mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher);
1719         } else {
1720             mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher);
1721         }
1722 
1723         mSubjectTextEditor.setText(mWorkingMessage.getSubject());
1724         mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE);
1725         hideOrShowTopPanel();
1726     }
1727 
hideOrShowTopPanel()1728     private void hideOrShowTopPanel() {
1729         boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible());
1730         mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE);
1731     }
1732 
initialize(Bundle savedInstanceState, long originalThreadId)1733     public void initialize(Bundle savedInstanceState, long originalThreadId) {
1734         Intent intent = getIntent();
1735 
1736         // Create a new empty working message.
1737         mWorkingMessage = WorkingMessage.createEmpty(this);
1738 
1739         // Read parameters or previously saved state of this activity. This will load a new
1740         // mConversation
1741         initActivityState(savedInstanceState, intent);
1742 
1743         if (LogTag.SEVERE_WARNING && originalThreadId != 0 &&
1744                 originalThreadId == mConversation.getThreadId()) {
1745             LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " +
1746                     " threadId didn't change from: " + originalThreadId, this);
1747         }
1748 
1749         log("savedInstanceState = " + savedInstanceState +
1750             " intent = " + intent +
1751             " mConversation = " + mConversation);
1752 
1753         if (cancelFailedToDeliverNotification(getIntent(), this)) {
1754             // Show a pop-up dialog to inform user the message was
1755             // failed to deliver.
1756             undeliveredMessageDialog(getMessageDate(null));
1757         }
1758         cancelFailedDownloadNotification(getIntent(), this);
1759 
1760         // Set up the message history ListAdapter
1761         initMessageList();
1762 
1763         // Load the draft for this thread, if we aren't already handling
1764         // existing data, such as a shared picture or forwarded message.
1765         boolean isForwardedMessage = false;
1766         if (!handleSendIntent(intent)) {
1767             isForwardedMessage = handleForwardedMessage();
1768             if (!isForwardedMessage) {
1769                 loadDraft();
1770             }
1771         }
1772 
1773         // Let the working message know what conversation it belongs to
1774         mWorkingMessage.setConversation(mConversation);
1775 
1776         // Show the recipients editor if we don't have a valid thread. Hide it otherwise.
1777         if (mConversation.getThreadId() <= 0) {
1778             // Hide the recipients editor so the call to initRecipientsEditor won't get
1779             // short-circuited.
1780             hideRecipientEditor();
1781             initRecipientsEditor();
1782 
1783             // Bring up the softkeyboard so the user can immediately enter recipients. This
1784             // call won't do anything on devices with a hard keyboard.
1785             getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1786                     WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
1787         } else {
1788             hideRecipientEditor();
1789         }
1790 
1791         updateSendButtonState();
1792 
1793         drawTopPanel();
1794         drawBottomPanel();
1795         mAttachmentEditor.update(mWorkingMessage);
1796 
1797         Configuration config = getResources().getConfiguration();
1798         mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO;
1799         mIsLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE;
1800         onKeyboardStateChanged(mIsKeyboardOpen);
1801 
1802         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1803             log("update title, mConversation=" + mConversation.toString());
1804         }
1805 
1806         updateTitle(mConversation.getRecipients());
1807 
1808         if (isForwardedMessage && isRecipientsEditorVisible()) {
1809             // The user is forwarding the message to someone. Put the focus on the
1810             // recipient editor rather than in the message editor.
1811             mRecipientsEditor.requestFocus();
1812         }
1813     }
1814 
1815     @Override
onNewIntent(Intent intent)1816     protected void onNewIntent(Intent intent) {
1817         super.onNewIntent(intent);
1818 
1819         setIntent(intent);
1820 
1821         Conversation conversation = null;
1822         mSentMessage = false;
1823 
1824         // If we have been passed a thread_id, use that to find our
1825         // conversation.
1826 
1827         // Note that originalThreadId might be zero but if this is a draft and we save the
1828         // draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage
1829         // the thread will get a threadId behind the UI thread's back.
1830         long originalThreadId = mConversation.getThreadId();
1831         long threadId = intent.getLongExtra("thread_id", 0);
1832         Uri intentUri = intent.getData();
1833 
1834         boolean sameThread = false;
1835         if (threadId > 0) {
1836             conversation = Conversation.get(this, threadId, false);
1837         } else {
1838             if (mConversation.getThreadId() == 0) {
1839                 // We've got a draft. See if the new intent's recipient is the same as
1840                 // the draft's recipient. First make sure the working recipients are synched
1841                 // to the conversation.
1842                 mWorkingMessage.syncWorkingRecipients();
1843                 sameThread = mConversation.sameRecipient(intentUri);
1844             }
1845             if (!sameThread) {
1846                 // Otherwise, try to get a conversation based on the
1847                 // data URI passed to our intent.
1848                 conversation = Conversation.get(this, intentUri, false);
1849             }
1850         }
1851 
1852         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1853             log("data=" + intentUri + ", thread_id extra is " + threadId +
1854                     ", new conversation=" + conversation + ", mConversation=" + mConversation);
1855         }
1856 
1857         if (conversation != null) {
1858             // Don't let any markAsRead DB updates occur before we've loaded the messages for
1859             // the thread.
1860             conversation.blockMarkAsRead(true);
1861 
1862             // this is probably paranoia to compare both thread_ids and recipient lists,
1863             // but we want to make double sure because this is a last minute fix for Froyo
1864             // and the previous code checked thread ids only.
1865             // (we cannot just compare thread ids because there is a case where mConversation
1866             // has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1),
1867             // even though the recipient lists are different)
1868             sameThread = (conversation.getThreadId() == mConversation.getThreadId() &&
1869                     conversation.equals(mConversation));
1870         }
1871 
1872         if (sameThread) {
1873             log("same conversation");
1874         } else {
1875             if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1876                 log("different conversation");
1877             }
1878             saveDraft();    // if we've got a draft, save it first
1879 
1880             initialize(null, originalThreadId);
1881             loadMessageContent();
1882         }
1883 
1884     }
1885 
1886     @Override
onRestart()1887     protected void onRestart() {
1888         super.onRestart();
1889 
1890         if (mWorkingMessage.isDiscarded()) {
1891             // If the message isn't worth saving, don't resurrect it. Doing so can lead to
1892             // a situation where a new incoming message gets the old thread id of the discarded
1893             // draft. This activity can end up displaying the recipients of the old message with
1894             // the contents of the new message. Recognize that dangerous situation and bail out
1895             // to the ConversationList where the user can enter this in a clean manner.
1896             if (mWorkingMessage.isWorthSaving()) {
1897                 mWorkingMessage.unDiscard();    // it was discarded in onStop().
1898             } else if (isRecipientsEditorVisible()) {
1899                 goToConversationList();
1900             } else {
1901                 loadDraft();
1902                 mWorkingMessage.setConversation(mConversation);
1903                 mAttachmentEditor.update(mWorkingMessage);
1904             }
1905         }
1906     }
1907 
1908     @Override
onStart()1909     protected void onStart() {
1910         super.onStart();
1911         mConversation.blockMarkAsRead(true);
1912 
1913         initFocus();
1914 
1915         // Register a BroadcastReceiver to listen on HTTP I/O process.
1916         registerReceiver(mHttpProgressReceiver, mHttpProgressFilter);
1917 
1918         loadMessageContent();
1919 
1920         // Update the fasttrack info in case any of the recipients' contact info changed
1921         // while we were paused. This can happen, for example, if a user changes or adds
1922         // an avatar associated with a contact.
1923         mWorkingMessage.syncWorkingRecipients();
1924 
1925         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1926             log("update title, mConversation=" + mConversation.toString());
1927         }
1928 
1929         updateTitle(mConversation.getRecipients());
1930     }
1931 
loadMessageContent()1932     public void loadMessageContent() {
1933         startMsgListQuery();
1934         updateSendFailedNotification();
1935         drawBottomPanel();
1936     }
1937 
updateSendFailedNotification()1938     private void updateSendFailedNotification() {
1939         final long threadId = mConversation.getThreadId();
1940         if (threadId <= 0)
1941             return;
1942 
1943         // updateSendFailedNotificationForThread makes a database call, so do the work off
1944         // of the ui thread.
1945         new Thread(new Runnable() {
1946             public void run() {
1947                 MessagingNotification.updateSendFailedNotificationForThread(
1948                         ComposeMessageActivity.this, threadId);
1949             }
1950         }, "updateSendFailedNotification").start();
1951     }
1952 
1953     @Override
onSaveInstanceState(Bundle outState)1954     public void onSaveInstanceState(Bundle outState) {
1955         super.onSaveInstanceState(outState);
1956 
1957         outState.putString("recipients", getRecipients().serialize());
1958 
1959         mWorkingMessage.writeStateToBundle(outState);
1960 
1961         if (mExitOnSent) {
1962             outState.putBoolean("exit_on_sent", mExitOnSent);
1963         }
1964     }
1965 
1966     @Override
onResume()1967     protected void onResume() {
1968         super.onResume();
1969 
1970         // OLD: get notified of presence updates to update the titlebar.
1971         // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
1972         //      there is out of our control.
1973         //Contact.startPresenceObserver();
1974 
1975         addRecipientsListeners();
1976 
1977         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1978             log("update title, mConversation=" + mConversation.toString());
1979         }
1980 
1981         // There seems to be a bug in the framework such that setting the title
1982         // here gets overwritten to the original title.  Do this delayed as a
1983         // workaround.
1984         mMessageListItemHandler.postDelayed(new Runnable() {
1985             public void run() {
1986                 ContactList recipients = isRecipientsEditorVisible() ?
1987                         mRecipientsEditor.constructContactsFromInput() : getRecipients();
1988                 updateTitle(recipients);
1989             }
1990         }, 100);
1991     }
1992 
1993     @Override
onPause()1994     protected void onPause() {
1995         super.onPause();
1996 
1997         // OLD: stop getting notified of presence updates to update the titlebar.
1998         // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
1999         //      there is out of our control.
2000         //Contact.stopPresenceObserver();
2001 
2002         removeRecipientsListeners();
2003     }
2004 
2005     @Override
onStop()2006     protected void onStop() {
2007         super.onStop();
2008 
2009         // Allow any blocked calls to update the thread's read status.
2010         mConversation.blockMarkAsRead(false);
2011 
2012         if (mMsgListAdapter != null) {
2013             mMsgListAdapter.changeCursor(null);
2014         }
2015 
2016         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2017             log("save draft");
2018         }
2019         saveDraft();
2020 
2021         // Cleanup the BroadcastReceiver.
2022         unregisterReceiver(mHttpProgressReceiver);
2023     }
2024 
2025     @Override
onDestroy()2026     protected void onDestroy() {
2027         if (TRACE) {
2028             android.os.Debug.stopMethodTracing();
2029         }
2030 
2031         super.onDestroy();
2032     }
2033 
2034     @Override
onConfigurationChanged(Configuration newConfig)2035     public void onConfigurationChanged(Configuration newConfig) {
2036         super.onConfigurationChanged(newConfig);
2037         if (LOCAL_LOGV) {
2038             Log.v(TAG, "onConfigurationChanged: " + newConfig);
2039         }
2040 
2041         mIsKeyboardOpen = newConfig.keyboardHidden == KEYBOARDHIDDEN_NO;
2042         boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
2043         if (mIsLandscape != isLandscape) {
2044             mIsLandscape = isLandscape;
2045 
2046             // Have to re-layout the attachment editor because we have different layouts
2047             // depending on whether we're portrait or landscape.
2048             mAttachmentEditor.update(mWorkingMessage);
2049         }
2050         onKeyboardStateChanged(mIsKeyboardOpen);
2051     }
2052 
onKeyboardStateChanged(boolean isKeyboardOpen)2053     private void onKeyboardStateChanged(boolean isKeyboardOpen) {
2054         // If the keyboard is hidden, don't show focus highlights for
2055         // things that cannot receive input.
2056         if (isKeyboardOpen) {
2057             if (mRecipientsEditor != null) {
2058                 mRecipientsEditor.setFocusableInTouchMode(true);
2059             }
2060             if (mSubjectTextEditor != null) {
2061                 mSubjectTextEditor.setFocusableInTouchMode(true);
2062             }
2063             mTextEditor.setFocusableInTouchMode(true);
2064             mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
2065         } else {
2066             if (mRecipientsEditor != null) {
2067                 mRecipientsEditor.setFocusable(false);
2068             }
2069             if (mSubjectTextEditor != null) {
2070                 mSubjectTextEditor.setFocusable(false);
2071             }
2072             mTextEditor.setFocusable(false);
2073             mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
2074         }
2075     }
2076 
2077     @Override
onUserInteraction()2078     public void onUserInteraction() {
2079         checkPendingNotification();
2080     }
2081 
2082     @Override
onWindowFocusChanged(boolean hasFocus)2083     public void onWindowFocusChanged(boolean hasFocus) {
2084         if (hasFocus) {
2085             checkPendingNotification();
2086         }
2087     }
2088 
2089     @Override
onKeyDown(int keyCode, KeyEvent event)2090     public boolean onKeyDown(int keyCode, KeyEvent event) {
2091         switch (keyCode) {
2092             case KeyEvent.KEYCODE_DEL:
2093                 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
2094                     Cursor cursor;
2095                     try {
2096                         cursor = (Cursor) mMsgListView.getSelectedItem();
2097                     } catch (ClassCastException e) {
2098                         Log.e(TAG, "Unexpected ClassCastException.", e);
2099                         return super.onKeyDown(keyCode, event);
2100                     }
2101 
2102                     if (cursor != null) {
2103                         boolean locked = cursor.getInt(COLUMN_MMS_LOCKED) != 0;
2104                         DeleteMessageListener l = new DeleteMessageListener(
2105                                 cursor.getLong(COLUMN_ID),
2106                                 cursor.getString(COLUMN_MSG_TYPE),
2107                                 locked);
2108                         confirmDeleteDialog(l, locked);
2109                         return true;
2110                     }
2111                 }
2112                 break;
2113             case KeyEvent.KEYCODE_DPAD_CENTER:
2114             case KeyEvent.KEYCODE_ENTER:
2115                 if (isPreparedForSending()) {
2116                     confirmSendMessageIfNeeded();
2117                     return true;
2118                 }
2119                 break;
2120             case KeyEvent.KEYCODE_BACK:
2121                 exitComposeMessageActivity(new Runnable() {
2122                     public void run() {
2123                         finish();
2124                     }
2125                 });
2126                 return true;
2127         }
2128 
2129         return super.onKeyDown(keyCode, event);
2130     }
2131 
exitComposeMessageActivity(final Runnable exit)2132     private void exitComposeMessageActivity(final Runnable exit) {
2133         // If the message is empty, just quit -- finishing the
2134         // activity will cause an empty draft to be deleted.
2135         if (!mWorkingMessage.isWorthSaving()) {
2136             exit.run();
2137             return;
2138         }
2139 
2140         if (isRecipientsEditorVisible() &&
2141                 !mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) {
2142             MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener());
2143             return;
2144         }
2145 
2146         mToastForDraftSave = true;
2147         exit.run();
2148     }
2149 
goToConversationList()2150     private void goToConversationList() {
2151         finish();
2152         startActivity(new Intent(this, ConversationList.class));
2153     }
2154 
hideRecipientEditor()2155     private void hideRecipientEditor() {
2156         if (mRecipientsEditor != null) {
2157             mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
2158             mRecipientsEditor.setVisibility(View.GONE);
2159             hideOrShowTopPanel();
2160         }
2161     }
2162 
isRecipientsEditorVisible()2163     private boolean isRecipientsEditorVisible() {
2164         return (null != mRecipientsEditor)
2165                     && (View.VISIBLE == mRecipientsEditor.getVisibility());
2166     }
2167 
isSubjectEditorVisible()2168     private boolean isSubjectEditorVisible() {
2169         return (null != mSubjectTextEditor)
2170                     && (View.VISIBLE == mSubjectTextEditor.getVisibility());
2171     }
2172 
onAttachmentChanged()2173     public void onAttachmentChanged() {
2174         // Have to make sure we're on the UI thread. This function can be called off of the UI
2175         // thread when we're adding multi-attachments
2176         runOnUiThread(new Runnable() {
2177             public void run() {
2178                 drawBottomPanel();
2179                 updateSendButtonState();
2180                 mAttachmentEditor.update(mWorkingMessage);
2181             }
2182         });
2183     }
2184 
onProtocolChanged(final boolean mms)2185     public void onProtocolChanged(final boolean mms) {
2186         // Have to make sure we're on the UI thread. This function can be called off of the UI
2187         // thread when we're adding multi-attachments
2188         runOnUiThread(new Runnable() {
2189             public void run() {
2190                 toastConvertInfo(mms);
2191                 setSendButtonText(mms);
2192             }
2193         });
2194     }
2195 
setSendButtonText(boolean isMms)2196     private void setSendButtonText(boolean isMms) {
2197         Button sendButton = mSendButton;
2198         sendButton.setText(R.string.send);
2199 
2200         if (isMms) {
2201             // Create and append the "MMS" text in a smaller font than the "Send" text.
2202             sendButton.append("\n");
2203             SpannableString spannable = new SpannableString(getString(R.string.mms));
2204             int mmsTextSize = (int) (sendButton.getTextSize() * 0.75f);
2205             spannable.setSpan(new AbsoluteSizeSpan(mmsTextSize), 0, spannable.length(), 0);
2206             sendButton.append(spannable);
2207             mTextCounter.setText("");
2208         }
2209     }
2210 
2211     Runnable mResetMessageRunnable = new Runnable() {
2212         public void run() {
2213             resetMessage();
2214         }
2215     };
2216 
onPreMessageSent()2217     public void onPreMessageSent() {
2218         runOnUiThread(mResetMessageRunnable);
2219     }
2220 
onMessageSent()2221     public void onMessageSent() {
2222         // If we already have messages in the list adapter, it
2223         // will be auto-requerying; don't thrash another query in.
2224         if (mMsgListAdapter.getCount() == 0) {
2225             startMsgListQuery();
2226         }
2227     }
2228 
onMaxPendingMessagesReached()2229     public void onMaxPendingMessagesReached() {
2230         saveDraft();
2231 
2232         runOnUiThread(new Runnable() {
2233             public void run() {
2234                 Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms,
2235                         Toast.LENGTH_LONG).show();
2236             }
2237         });
2238     }
2239 
onAttachmentError(final int error)2240     public void onAttachmentError(final int error) {
2241         runOnUiThread(new Runnable() {
2242             public void run() {
2243                 handleAddAttachmentError(error, R.string.type_picture);
2244                 onMessageSent();        // now requery the list of messages
2245             }
2246         });
2247     }
2248 
2249     // We don't want to show the "call" option unless there is only one
2250     // recipient and it's a phone number.
isRecipientCallable()2251     private boolean isRecipientCallable() {
2252         ContactList recipients = getRecipients();
2253         return (recipients.size() == 1 && !recipients.containsEmail());
2254     }
2255 
dialRecipient()2256     private void dialRecipient() {
2257         String number = getRecipients().get(0).getNumber();
2258         Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number));
2259         startActivity(dialIntent);
2260     }
2261 
2262     @Override
onPrepareOptionsMenu(Menu menu)2263     public boolean onPrepareOptionsMenu(Menu menu) {
2264         menu.clear();
2265 
2266         if (isRecipientCallable()) {
2267             menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call).setIcon(
2268                     R.drawable.ic_menu_call);
2269         }
2270 
2271         // Only add the "View contact" menu item when there's a single recipient and that
2272         // recipient is someone in contacts.
2273         ContactList recipients = getRecipients();
2274         if (recipients.size() == 1 && recipients.get(0).existsInDatabase()) {
2275             menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact).setIcon(
2276                     R.drawable.ic_menu_contact);
2277         }
2278 
2279         if (MmsConfig.getMmsEnabled()) {
2280             if (!isSubjectEditorVisible()) {
2281                 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon(
2282                         R.drawable.ic_menu_edit);
2283             }
2284 
2285             if (!mWorkingMessage.hasAttachment()) {
2286                 menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment).setIcon(
2287                         R.drawable.ic_menu_attachment);
2288             }
2289         }
2290 
2291         if (isPreparedForSending()) {
2292             menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
2293         }
2294 
2295         menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon(
2296                 R.drawable.ic_menu_emoticons);
2297 
2298         if (mMsgListAdapter.getCount() > 0) {
2299             // Removed search as part of b/1205708
2300             //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
2301             //        R.drawable.ic_menu_search);
2302             Cursor cursor = mMsgListAdapter.getCursor();
2303             if ((null != cursor) && (cursor.getCount() > 0)) {
2304                 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
2305                     android.R.drawable.ic_menu_delete);
2306             }
2307         } else {
2308             menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
2309         }
2310 
2311         menu.add(0, MENU_CONVERSATION_LIST, 0, R.string.all_threads).setIcon(
2312                 R.drawable.ic_menu_friendslist);
2313 
2314         buildAddAddressToContactMenuItem(menu);
2315         return true;
2316     }
2317 
buildAddAddressToContactMenuItem(Menu menu)2318     private void buildAddAddressToContactMenuItem(Menu menu) {
2319         // Look for the first recipient we don't have a contact for and create a menu item to
2320         // add the number to contacts.
2321         for (Contact c : getRecipients()) {
2322             if (!c.existsInDatabase() && canAddToContacts(c)) {
2323                 Intent intent = ConversationList.createAddContactIntent(c.getNumber());
2324                 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
2325                     .setIcon(android.R.drawable.ic_menu_add)
2326                     .setIntent(intent);
2327                 break;
2328             }
2329         }
2330     }
2331 
2332     @Override
onOptionsItemSelected(MenuItem item)2333     public boolean onOptionsItemSelected(MenuItem item) {
2334         switch (item.getItemId()) {
2335             case MENU_ADD_SUBJECT:
2336                 showSubjectEditor(true);
2337                 mWorkingMessage.setSubject("", true);
2338                 mSubjectTextEditor.requestFocus();
2339                 break;
2340             case MENU_ADD_ATTACHMENT:
2341                 // Launch the add-attachment list dialog
2342                 showAddAttachmentDialog(false);
2343                 break;
2344             case MENU_DISCARD:
2345                 mWorkingMessage.discard();
2346                 finish();
2347                 break;
2348             case MENU_SEND:
2349                 if (isPreparedForSending()) {
2350                     confirmSendMessageIfNeeded();
2351                 }
2352                 break;
2353             case MENU_SEARCH:
2354                 onSearchRequested();
2355                 break;
2356             case MENU_DELETE_THREAD:
2357                 confirmDeleteThread(mConversation.getThreadId());
2358                 break;
2359             case MENU_CONVERSATION_LIST:
2360                 exitComposeMessageActivity(new Runnable() {
2361                     public void run() {
2362                         goToConversationList();
2363                     }
2364                 });
2365                 break;
2366             case MENU_CALL_RECIPIENT:
2367                 dialRecipient();
2368                 break;
2369             case MENU_INSERT_SMILEY:
2370                 showSmileyDialog();
2371                 break;
2372             case MENU_VIEW_CONTACT: {
2373                 // View the contact for the first (and only) recipient.
2374                 ContactList list = getRecipients();
2375                 if (list.size() == 1 && list.get(0).existsInDatabase()) {
2376                     Uri contactUri = list.get(0).getUri();
2377                     Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
2378                     intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
2379                     startActivity(intent);
2380                 }
2381                 break;
2382             }
2383             case MENU_ADD_ADDRESS_TO_CONTACTS:
2384                 mAddContactIntent = item.getIntent();
2385                 startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT);
2386                 break;
2387         }
2388 
2389         return true;
2390     }
2391 
confirmDeleteThread(long threadId)2392     private void confirmDeleteThread(long threadId) {
2393         Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler,
2394                 threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN);
2395     }
2396 
2397 //    static class SystemProperties { // TODO, temp class to get unbundling working
2398 //        static int getInt(String s, int value) {
2399 //            return value;       // just return the default value or now
2400 //        }
2401 //    }
2402 
getVideoCaptureDurationLimit()2403     private int getVideoCaptureDurationLimit() {
2404         return CamcorderProfile.get(CamcorderProfile.QUALITY_LOW).duration;
2405     }
2406 
addAttachment(int type, boolean replace)2407     private void addAttachment(int type, boolean replace) {
2408         // Calculate the size of the current slide if we're doing a replace so the
2409         // slide size can optionally be used in computing how much room is left for an attachment.
2410         int currentSlideSize = 0;
2411         SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2412         if (replace && slideShow != null) {
2413             SlideModel slide = slideShow.get(0);
2414             currentSlideSize = slide.getSlideSize();
2415         }
2416         switch (type) {
2417             case AttachmentTypeSelectorAdapter.ADD_IMAGE:
2418                 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE);
2419                 break;
2420 
2421             case AttachmentTypeSelectorAdapter.TAKE_PICTURE: {
2422                 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
2423 
2424                 intent.putExtra(MediaStore.EXTRA_OUTPUT, Mms.ScrapSpace.CONTENT_URI);
2425                 startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE);
2426                 break;
2427             }
2428 
2429             case AttachmentTypeSelectorAdapter.ADD_VIDEO:
2430                 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO);
2431                 break;
2432 
2433             case AttachmentTypeSelectorAdapter.RECORD_VIDEO: {
2434                 // Set video size limit. Subtract 1K for some text.
2435                 long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP;
2436                 if (slideShow != null) {
2437                     sizeLimit -= slideShow.getCurrentMessageSize();
2438 
2439                     // We're about to ask the camera to capture some video which will
2440                     // eventually replace the content on the current slide. Since the current
2441                     // slide already has some content (which was subtracted out just above)
2442                     // and that content is going to get replaced, we can add the size of the
2443                     // current slide into the available space used to capture a video.
2444                     sizeLimit += currentSlideSize;
2445                 }
2446                 if (sizeLimit > 0) {
2447                     int durationLimit = getVideoCaptureDurationLimit();
2448                     Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
2449                     intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
2450                     intent.putExtra("android.intent.extra.sizeLimit", sizeLimit);
2451                     intent.putExtra("android.intent.extra.durationLimit", durationLimit);
2452                     startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO);
2453                 }
2454                 else {
2455                     Toast.makeText(this,
2456                             getString(R.string.message_too_big_for_video),
2457                             Toast.LENGTH_SHORT).show();
2458                 }
2459             }
2460             break;
2461 
2462             case AttachmentTypeSelectorAdapter.ADD_SOUND:
2463                 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND);
2464                 break;
2465 
2466             case AttachmentTypeSelectorAdapter.RECORD_SOUND:
2467                 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND);
2468                 break;
2469 
2470             case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW:
2471                 editSlideshow();
2472                 break;
2473 
2474             default:
2475                 break;
2476         }
2477     }
2478 
showAddAttachmentDialog(final boolean replace)2479     private void showAddAttachmentDialog(final boolean replace) {
2480         AlertDialog.Builder builder = new AlertDialog.Builder(this);
2481         builder.setIcon(R.drawable.ic_dialog_attach);
2482         builder.setTitle(R.string.add_attachment);
2483 
2484         if (mAttachmentTypeSelectorAdapter == null) {
2485             mAttachmentTypeSelectorAdapter = new AttachmentTypeSelectorAdapter(
2486                     this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW);
2487         }
2488         builder.setAdapter(mAttachmentTypeSelectorAdapter, new DialogInterface.OnClickListener() {
2489             public void onClick(DialogInterface dialog, int which) {
2490                 addAttachment(mAttachmentTypeSelectorAdapter.buttonToCommand(which), replace);
2491                 dialog.dismiss();
2492             }
2493         });
2494 
2495         builder.show();
2496     }
2497 
2498     @Override
onActivityResult(int requestCode, int resultCode, Intent data)2499     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2500         if (DEBUG) {
2501             log("requestCode=" + requestCode + ", resultCode=" + resultCode + ", data=" + data);
2502         }
2503         mWaitingForSubActivity = false;          // We're back!
2504         if (mWorkingMessage.isFakeMmsForDraft()) {
2505             // We no longer have to fake the fact we're an Mms. At this point we are or we aren't,
2506             // based on attachments and other Mms attrs.
2507             mWorkingMessage.removeFakeMmsForDraft();
2508         }
2509 
2510         // If there's no data (because the user didn't select a picture and
2511         // just hit BACK, for example), there's nothing to do.
2512         if (requestCode != REQUEST_CODE_TAKE_PICTURE) {
2513             if (data == null) {
2514                 return;
2515             }
2516         } else if (resultCode != RESULT_OK){
2517             if (DEBUG) log("bail due to resultCode=" + resultCode);
2518             return;
2519         }
2520 
2521         switch(requestCode) {
2522             case REQUEST_CODE_CREATE_SLIDESHOW:
2523                 if (data != null) {
2524                     WorkingMessage newMessage = WorkingMessage.load(this, data.getData());
2525                     if (newMessage != null) {
2526                         mWorkingMessage = newMessage;
2527                         mWorkingMessage.setConversation(mConversation);
2528                         mAttachmentEditor.update(mWorkingMessage);
2529                         drawTopPanel();
2530                         updateSendButtonState();
2531                     }
2532                 }
2533                 break;
2534 
2535             case REQUEST_CODE_TAKE_PICTURE: {
2536                 // create a file based uri and pass to addImage(). We want to read the JPEG
2537                 // data directly from file (using UriImage) instead of decoding it into a Bitmap,
2538                 // which takes up too much memory and could easily lead to OOM.
2539                 File file = new File(Mms.ScrapSpace.SCRAP_FILE_PATH);
2540                 Uri uri = Uri.fromFile(file);
2541                 addImage(uri, false);
2542                 break;
2543             }
2544 
2545             case REQUEST_CODE_ATTACH_IMAGE: {
2546                 addImage(data.getData(), false);
2547                 break;
2548             }
2549 
2550             case REQUEST_CODE_TAKE_VIDEO:
2551             case REQUEST_CODE_ATTACH_VIDEO:
2552                 addVideo(data.getData(), false);
2553                 break;
2554 
2555             case REQUEST_CODE_ATTACH_SOUND: {
2556                 Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
2557                 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) {
2558                     break;
2559                 }
2560                 addAudio(uri);
2561                 break;
2562             }
2563 
2564             case REQUEST_CODE_RECORD_SOUND:
2565                 addAudio(data.getData());
2566                 break;
2567 
2568             case REQUEST_CODE_ECM_EXIT_DIALOG:
2569                 boolean outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false);
2570                 if (outOfEmergencyMode) {
2571                     sendMessage(false);
2572                 }
2573                 break;
2574 
2575             case REQUEST_CODE_ADD_CONTACT:
2576                 // The user just added a new contact. We saved the contact info in
2577                 // mAddContactIntent. Get the contact and force our cached contact to
2578                 // get reloaded with the new info (such as contact name). After the
2579                 // contact is reloaded, the function onUpdate() in this file will get called
2580                 // and it will update the title bar, etc.
2581                 if (mAddContactIntent != null) {
2582                     String address =
2583                         mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL);
2584                     if (address == null) {
2585                         address =
2586                             mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE);
2587                     }
2588                     if (address != null) {
2589                         Contact contact = Contact.get(address, false);
2590                         if (contact != null) {
2591                             contact.reload();
2592                         }
2593                     }
2594                 }
2595                 break;
2596 
2597             default:
2598                 // TODO
2599                 break;
2600         }
2601     }
2602 
2603     private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() {
2604         // TODO: make this produce a Uri, that's what we want anyway
2605         public void onResizeResult(PduPart part, boolean append) {
2606             if (part == null) {
2607                 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture);
2608                 return;
2609             }
2610 
2611             Context context = ComposeMessageActivity.this;
2612             PduPersister persister = PduPersister.getPduPersister(context);
2613             int result;
2614 
2615             Uri messageUri = mWorkingMessage.saveAsMms(true);
2616             try {
2617                 Uri dataUri = persister.persistPart(part, ContentUris.parseId(messageUri));
2618                 result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append);
2619                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2620                     log("ResizeImageResultCallback: dataUri=" + dataUri);
2621                 }
2622             } catch (MmsException e) {
2623                 result = WorkingMessage.UNKNOWN_ERROR;
2624             }
2625 
2626             handleAddAttachmentError(result, R.string.type_picture);
2627         }
2628     };
2629 
handleAddAttachmentError(final int error, final int mediaTypeStringId)2630     private void handleAddAttachmentError(final int error, final int mediaTypeStringId) {
2631         if (error == WorkingMessage.OK) {
2632             return;
2633         }
2634 
2635         runOnUiThread(new Runnable() {
2636             public void run() {
2637                 Resources res = getResources();
2638                 String mediaType = res.getString(mediaTypeStringId);
2639                 String title, message;
2640 
2641                 switch(error) {
2642                 case WorkingMessage.UNKNOWN_ERROR:
2643                     message = res.getString(R.string.failed_to_add_media, mediaType);
2644                     Toast.makeText(ComposeMessageActivity.this, message, Toast.LENGTH_SHORT).show();
2645                     return;
2646                 case WorkingMessage.UNSUPPORTED_TYPE:
2647                     title = res.getString(R.string.unsupported_media_format, mediaType);
2648                     message = res.getString(R.string.select_different_media, mediaType);
2649                     break;
2650                 case WorkingMessage.MESSAGE_SIZE_EXCEEDED:
2651                     title = res.getString(R.string.exceed_message_size_limitation, mediaType);
2652                     message = res.getString(R.string.failed_to_add_media, mediaType);
2653                     break;
2654                 case WorkingMessage.IMAGE_TOO_LARGE:
2655                     title = res.getString(R.string.failed_to_resize_image);
2656                     message = res.getString(R.string.resize_image_error_information);
2657                     break;
2658                 default:
2659                     throw new IllegalArgumentException("unknown error " + error);
2660                 }
2661 
2662                 MessageUtils.showErrorDialog(ComposeMessageActivity.this, title, message);
2663             }
2664         });
2665     }
2666 
addImage(Uri uri, boolean append)2667     private void addImage(Uri uri, boolean append) {
2668         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2669             log("append=" + append + ", uri=" + uri);
2670         }
2671 
2672         int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri, append);
2673 
2674         if (result == WorkingMessage.IMAGE_TOO_LARGE ||
2675             result == WorkingMessage.MESSAGE_SIZE_EXCEEDED) {
2676             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2677                 log("resize image " + uri);
2678             }
2679             MessageUtils.resizeImageAsync(this,
2680                     uri, mAttachmentEditorHandler, mResizeImageCallback, append);
2681             return;
2682         }
2683         handleAddAttachmentError(result, R.string.type_picture);
2684     }
2685 
addVideo(Uri uri, boolean append)2686     private void addVideo(Uri uri, boolean append) {
2687         if (uri != null) {
2688             int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri, append);
2689             handleAddAttachmentError(result, R.string.type_video);
2690         }
2691     }
2692 
addAudio(Uri uri)2693     private void addAudio(Uri uri) {
2694         int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri, false);
2695         handleAddAttachmentError(result, R.string.type_audio);
2696     }
2697 
handleForwardedMessage()2698     private boolean handleForwardedMessage() {
2699         Intent intent = getIntent();
2700 
2701         // If this is a forwarded message, it will have an Intent extra
2702         // indicating so.  If not, bail out.
2703         if (intent.getBooleanExtra("forwarded_message", false) == false) {
2704             return false;
2705         }
2706 
2707         Uri uri = intent.getParcelableExtra("msg_uri");
2708 
2709         if (Log.isLoggable(LogTag.APP, Log.DEBUG)) {
2710             log("" + uri);
2711         }
2712 
2713         if (uri != null) {
2714             mWorkingMessage = WorkingMessage.load(this, uri);
2715             mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
2716         } else {
2717             mWorkingMessage.setText(intent.getStringExtra("sms_body"));
2718         }
2719 
2720         // let's clear the message thread for forwarded messages
2721         mMsgListAdapter.changeCursor(null);
2722 
2723         return true;
2724     }
2725 
handleSendIntent(Intent intent)2726     private boolean handleSendIntent(Intent intent) {
2727         Bundle extras = intent.getExtras();
2728         if (extras == null) {
2729             return false;
2730         }
2731 
2732         final String mimeType = intent.getType();
2733         String action = intent.getAction();
2734         if (Intent.ACTION_SEND.equals(action)) {
2735             if (extras.containsKey(Intent.EXTRA_STREAM)) {
2736                 Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM);
2737                 addAttachment(mimeType, uri, false);
2738                 return true;
2739             } else if (extras.containsKey(Intent.EXTRA_TEXT)) {
2740                 mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT));
2741                 return true;
2742             }
2743         } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) &&
2744                 extras.containsKey(Intent.EXTRA_STREAM)) {
2745             SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2746             final ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
2747             int currentSlideCount = slideShow != null ? slideShow.size() : 0;
2748             int importCount = uris.size();
2749             if (importCount + currentSlideCount > SlideshowEditor.MAX_SLIDE_NUM) {
2750                 importCount = Math.min(SlideshowEditor.MAX_SLIDE_NUM - currentSlideCount,
2751                         importCount);
2752                 Toast.makeText(ComposeMessageActivity.this,
2753                         getString(R.string.too_many_attachments,
2754                                 SlideshowEditor.MAX_SLIDE_NUM, importCount),
2755                                 Toast.LENGTH_LONG).show();
2756             }
2757 
2758             // Attach all the pictures/videos off of the UI thread.
2759             // Show a progress alert if adding all the slides hasn't finished
2760             // within one second.
2761             // Stash the runnable for showing it away so we can cancel
2762             // it later if adding completes ahead of the deadline.
2763             final AlertDialog dialog = new AlertDialog.Builder(ComposeMessageActivity.this)
2764                 .setIcon(android.R.drawable.ic_dialog_alert)
2765                 .setTitle(R.string.adding_attachments_title)
2766                 .setMessage(R.string.adding_attachments)
2767                 .create();
2768             final Runnable showProgress = new Runnable() {
2769                 public void run() {
2770                     dialog.show();
2771                 }
2772             };
2773             // Schedule it for one second from now.
2774             mAttachmentEditorHandler.postDelayed(showProgress, 1000);
2775 
2776             final int numberToImport = importCount;
2777             new Thread(new Runnable() {
2778                 public void run() {
2779                     for (int i = 0; i < numberToImport; i++) {
2780                         Parcelable uri = uris.get(i);
2781                         addAttachment(mimeType, (Uri) uri, true);
2782                     }
2783                     // Cancel pending show of the progress alert if necessary.
2784                     mAttachmentEditorHandler.removeCallbacks(showProgress);
2785                     dialog.dismiss();
2786                 }
2787             }, "addAttachment").start();
2788             return true;
2789         }
2790 
2791         return false;
2792     }
2793 
2794     // mVideoUri will look like this: content://media/external/video/media
2795     private static final String mVideoUri = Video.Media.getContentUri("external").toString();
2796     // mImageUri will look like this: content://media/external/images/media
2797     private static final String mImageUri = Images.Media.getContentUri("external").toString();
2798 
addAttachment(String type, Uri uri, boolean append)2799     private void addAttachment(String type, Uri uri, boolean append) {
2800         if (uri != null) {
2801             // When we're handling Intent.ACTION_SEND_MULTIPLE, the passed in items can be
2802             // videos, and/or images, and/or some other unknown types we don't handle. When
2803             // a single attachment is "shared" the type will specify an image or video. When
2804             // there are multiple types, the type passed in is "*/*". In that case, we've got
2805             // to look at the uri to figure out if it is an image or video.
2806             boolean wildcard = "*/*".equals(type);
2807             if (type.startsWith("image/") || (wildcard && uri.toString().startsWith(mImageUri))) {
2808                 addImage(uri, append);
2809             } else if (type.startsWith("video/") ||
2810                     (wildcard && uri.toString().startsWith(mVideoUri))) {
2811                 addVideo(uri, append);
2812             }
2813         }
2814     }
2815 
getResourcesString(int id, String mediaName)2816     private String getResourcesString(int id, String mediaName) {
2817         Resources r = getResources();
2818         return r.getString(id, mediaName);
2819     }
2820 
drawBottomPanel()2821     private void drawBottomPanel() {
2822         // Reset the counter for text editor.
2823         resetCounter();
2824 
2825         if (mWorkingMessage.hasSlideshow()) {
2826             mBottomPanel.setVisibility(View.GONE);
2827             mAttachmentEditor.requestFocus();
2828             return;
2829         }
2830 
2831         mBottomPanel.setVisibility(View.VISIBLE);
2832 
2833         CharSequence text = mWorkingMessage.getText();
2834 
2835         // TextView.setTextKeepState() doesn't like null input.
2836         if (text != null) {
2837             mTextEditor.setTextKeepState(text);
2838         } else {
2839             mTextEditor.setText("");
2840         }
2841     }
2842 
drawTopPanel()2843     private void drawTopPanel() {
2844         showSubjectEditor(mWorkingMessage.hasSubject());
2845     }
2846 
2847     //==========================================================
2848     // Interface methods
2849     //==========================================================
2850 
onClick(View v)2851     public void onClick(View v) {
2852         if ((v == mSendButton) && isPreparedForSending()) {
2853             confirmSendMessageIfNeeded();
2854         }
2855     }
2856 
onEditorAction(TextView v, int actionId, KeyEvent event)2857     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
2858         if (event != null) {
2859             // if shift key is down, then we want to insert the '\n' char in the TextView;
2860             // otherwise, the default action is to send the message.
2861             if (!event.isShiftPressed()) {
2862                 if (isPreparedForSending()) {
2863                     confirmSendMessageIfNeeded();
2864                 }
2865                 return true;
2866             }
2867             return false;
2868         }
2869 
2870         if (isPreparedForSending()) {
2871             confirmSendMessageIfNeeded();
2872         }
2873         return true;
2874     }
2875 
2876     private final TextWatcher mTextEditorWatcher = new TextWatcher() {
2877         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2878         }
2879 
2880         public void onTextChanged(CharSequence s, int start, int before, int count) {
2881             // This is a workaround for bug 1609057.  Since onUserInteraction() is
2882             // not called when the user touches the soft keyboard, we pretend it was
2883             // called when textfields changes.  This should be removed when the bug
2884             // is fixed.
2885             onUserInteraction();
2886 
2887             mWorkingMessage.setText(s);
2888 
2889             updateSendButtonState();
2890 
2891             updateCounter(s, start, before, count);
2892 
2893             ensureCorrectButtonHeight();
2894         }
2895 
2896         public void afterTextChanged(Editable s) {
2897         }
2898     };
2899 
2900     /**
2901      * Ensures that if the text edit box extends past two lines then the
2902      * button will be shifted up to allow enough space for the character
2903      * counter string to be placed beneath it.
2904      */
ensureCorrectButtonHeight()2905     private void ensureCorrectButtonHeight() {
2906         int currentTextLines = mTextEditor.getLineCount();
2907         if (currentTextLines <= 2) {
2908             mTextCounter.setVisibility(View.GONE);
2909         }
2910         else if (currentTextLines > 2 && mTextCounter.getVisibility() == View.GONE) {
2911             // Making the counter invisible ensures that it is used to correctly
2912             // calculate the position of the send button even if we choose not to
2913             // display the text.
2914             mTextCounter.setVisibility(View.INVISIBLE);
2915         }
2916     }
2917 
2918     private final TextWatcher mSubjectEditorWatcher = new TextWatcher() {
2919         public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
2920 
2921         public void onTextChanged(CharSequence s, int start, int before, int count) {
2922             mWorkingMessage.setSubject(s, true);
2923         }
2924 
2925         public void afterTextChanged(Editable s) { }
2926     };
2927 
2928     //==========================================================
2929     // Private methods
2930     //==========================================================
2931 
2932     /**
2933      * Initialize all UI elements from resources.
2934      */
initResourceRefs()2935     private void initResourceRefs() {
2936         mMsgListView = (MessageListView) findViewById(R.id.history);
2937         mMsgListView.setDivider(null);      // no divider so we look like IM conversation.
2938         mBottomPanel = findViewById(R.id.bottom_panel);
2939         mTextEditor = (EditText) findViewById(R.id.embedded_text_editor);
2940         mTextEditor.setOnEditorActionListener(this);
2941         mTextEditor.addTextChangedListener(mTextEditorWatcher);
2942         mTextEditor.setFilters(new InputFilter[] {
2943                 new LengthFilter(MmsConfig.getMaxTextLimit())});
2944         mTextCounter = (TextView) findViewById(R.id.text_counter);
2945         mSendButton = (Button) findViewById(R.id.send_button);
2946         mSendButton.setOnClickListener(this);
2947         mTopPanel = findViewById(R.id.recipients_subject_linear);
2948         mTopPanel.setFocusable(false);
2949         mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor);
2950         mAttachmentEditor.setHandler(mAttachmentEditorHandler);
2951     }
2952 
confirmDeleteDialog(OnClickListener listener, boolean locked)2953     private void confirmDeleteDialog(OnClickListener listener, boolean locked) {
2954         AlertDialog.Builder builder = new AlertDialog.Builder(this);
2955         builder.setTitle(locked ? R.string.confirm_dialog_locked_title :
2956             R.string.confirm_dialog_title);
2957         builder.setIcon(android.R.drawable.ic_dialog_alert);
2958         builder.setCancelable(true);
2959         builder.setMessage(locked ? R.string.confirm_delete_locked_message :
2960                     R.string.confirm_delete_message);
2961         builder.setPositiveButton(R.string.delete, listener);
2962         builder.setNegativeButton(R.string.no, null);
2963         builder.show();
2964     }
2965 
undeliveredMessageDialog(long date)2966     void undeliveredMessageDialog(long date) {
2967         String body;
2968         LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate(
2969                 R.layout.retry_sending_dialog, null);
2970 
2971         if (date >= 0) {
2972             body = getString(R.string.undelivered_msg_dialog_body,
2973                     MessageUtils.formatTimeStampString(this, date));
2974         } else {
2975             // FIXME: we can not get sms retry time.
2976             body = getString(R.string.undelivered_sms_dialog_body);
2977         }
2978 
2979         ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body);
2980 
2981         Toast undeliveredDialog = new Toast(this);
2982         undeliveredDialog.setView(dialog);
2983         undeliveredDialog.setDuration(Toast.LENGTH_LONG);
2984         undeliveredDialog.show();
2985     }
2986 
startMsgListQuery()2987     private void startMsgListQuery() {
2988         Uri conversationUri = mConversation.getUri();
2989 
2990         if (conversationUri == null) {
2991             return;
2992         }
2993 
2994         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2995             log("for " + conversationUri);
2996         }
2997 
2998         // Cancel any pending queries
2999         mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN);
3000         try {
3001             // Kick off the new query
3002             mBackgroundQueryHandler.startQuery(
3003                     MESSAGE_LIST_QUERY_TOKEN, null, conversationUri,
3004                     PROJECTION, null, null, null);
3005         } catch (SQLiteException e) {
3006             SqliteWrapper.checkSQLiteException(this, e);
3007         }
3008     }
3009 
initMessageList()3010     private void initMessageList() {
3011         if (mMsgListAdapter != null) {
3012             return;
3013         }
3014 
3015         String highlightString = getIntent().getStringExtra("highlight");
3016         Pattern highlight = highlightString == null
3017             ? null
3018             : Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE);
3019 
3020         // Initialize the list adapter with a null cursor.
3021         mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true, highlight);
3022         mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener);
3023         mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler);
3024         mMsgListView.setAdapter(mMsgListAdapter);
3025         mMsgListView.setItemsCanFocus(false);
3026         mMsgListView.setVisibility(View.VISIBLE);
3027         mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener);
3028         mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
3029             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3030                 if (view != null) {
3031                     ((MessageListItem) view).onMessageListItemClick();
3032                 }
3033             }
3034         });
3035     }
3036 
loadDraft()3037     private void loadDraft() {
3038         if (mWorkingMessage.isWorthSaving()) {
3039             Log.w(TAG, "called with non-empty working message");
3040             return;
3041         }
3042 
3043         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3044             log("call WorkingMessage.loadDraft");
3045         }
3046 
3047         mWorkingMessage = WorkingMessage.loadDraft(this, mConversation);
3048     }
3049 
saveDraft()3050     private void saveDraft() {
3051         // TODO: Do something better here.  Maybe make discard() legal
3052         // to call twice and make isEmpty() return true if discarded
3053         // so it is caught in the clause above this one?
3054         if (mWorkingMessage.isDiscarded()) {
3055             return;
3056         }
3057 
3058         if (!mWaitingForSubActivity && !mWorkingMessage.isWorthSaving()) {
3059             if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3060                 log("not worth saving, discard WorkingMessage and bail");
3061             }
3062             mWorkingMessage.discard();
3063             return;
3064         }
3065 
3066         mWorkingMessage.saveDraft();
3067 
3068         if (mToastForDraftSave) {
3069             Toast.makeText(this, R.string.message_saved_as_draft,
3070                     Toast.LENGTH_SHORT).show();
3071         }
3072     }
3073 
isPreparedForSending()3074     private boolean isPreparedForSending() {
3075         int recipientCount = recipientCount();
3076 
3077         return recipientCount > 0 && recipientCount <= MmsConfig.getRecipientLimit() &&
3078             (mWorkingMessage.hasAttachment() || mWorkingMessage.hasText());
3079     }
3080 
recipientCount()3081     private int recipientCount() {
3082         int recipientCount;
3083 
3084         // To avoid creating a bunch of invalid Contacts when the recipients
3085         // editor is in flux, we keep the recipients list empty.  So if the
3086         // recipients editor is showing, see if there is anything in it rather
3087         // than consulting the empty recipient list.
3088         if (isRecipientsEditorVisible()) {
3089             recipientCount = mRecipientsEditor.getRecipientCount();
3090         } else {
3091             recipientCount = getRecipients().size();
3092         }
3093         return recipientCount;
3094     }
3095 
sendMessage(boolean bCheckEcmMode)3096     private void sendMessage(boolean bCheckEcmMode) {
3097         if (bCheckEcmMode) {
3098             // TODO: expose this in telephony layer for SDK build
3099             String inEcm = SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE);
3100             if (Boolean.parseBoolean(inEcm)) {
3101                 try {
3102                     startActivityForResult(
3103                             new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null),
3104                             REQUEST_CODE_ECM_EXIT_DIALOG);
3105                     return;
3106                 } catch (ActivityNotFoundException e) {
3107                     // continue to send message
3108                     Log.e(TAG, "Cannot find EmergencyCallbackModeExitDialog", e);
3109                 }
3110             }
3111         }
3112 
3113         if (!mSendingMessage) {
3114             if (LogTag.SEVERE_WARNING) {
3115                 String sendingRecipients = mConversation.getRecipients().serialize();
3116                 if (!sendingRecipients.equals(mDebugRecipients)) {
3117                     String workingRecipients = mWorkingMessage.getWorkingRecipients();
3118                     if (!mDebugRecipients.equals(workingRecipients)) {
3119                         LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.sendMessage recipients in " +
3120                                 "window: \"" +
3121                                 mDebugRecipients + "\" differ from recipients from conv: \"" +
3122                                 sendingRecipients + "\" and working recipients: " +
3123                                 workingRecipients, this);
3124                     }
3125                 }
3126             }
3127 
3128             // send can change the recipients. Make sure we remove the listeners first and then add
3129             // them back once the recipient list has settled.
3130             removeRecipientsListeners();
3131 
3132             mWorkingMessage.send(mDebugRecipients);
3133 
3134             mSentMessage = true;
3135             mSendingMessage = true;
3136             addRecipientsListeners();
3137         }
3138         // But bail out if we are supposed to exit after the message is sent.
3139         if (mExitOnSent) {
3140             finish();
3141         }
3142     }
3143 
resetMessage()3144     private void resetMessage() {
3145         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3146             log("");
3147         }
3148 
3149         // Make the attachment editor hide its view.
3150         mAttachmentEditor.hideView();
3151 
3152         // Hide the subject editor.
3153         showSubjectEditor(false);
3154 
3155         // Focus to the text editor.
3156         mTextEditor.requestFocus();
3157 
3158         // We have to remove the text change listener while the text editor gets cleared and
3159         // we subsequently turn the message back into SMS. When the listener is listening while
3160         // doing the clearing, it's fighting to update its counts and itself try and turn
3161         // the message one way or the other.
3162         mTextEditor.removeTextChangedListener(mTextEditorWatcher);
3163 
3164         // Clear the text box.
3165         TextKeyListener.clear(mTextEditor.getText());
3166 
3167         mWorkingMessage = WorkingMessage.createEmpty(this);
3168         mWorkingMessage.setConversation(mConversation);
3169 
3170         hideRecipientEditor();
3171         drawBottomPanel();
3172 
3173         // "Or not", in this case.
3174         updateSendButtonState();
3175 
3176         // Our changes are done. Let the listener respond to text changes once again.
3177         mTextEditor.addTextChangedListener(mTextEditorWatcher);
3178 
3179         // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
3180         // conversation.
3181         if (mIsLandscape) {
3182             InputMethodManager inputMethodManager =
3183                 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
3184 
3185             inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0);
3186         }
3187 
3188         mLastRecipientCount = 0;
3189         mSendingMessage = false;
3190    }
3191 
updateSendButtonState()3192     private void updateSendButtonState() {
3193         boolean enable = false;
3194         if (isPreparedForSending()) {
3195             // When the type of attachment is slideshow, we should
3196             // also hide the 'Send' button since the slideshow view
3197             // already has a 'Send' button embedded.
3198             if (!mWorkingMessage.hasSlideshow()) {
3199                 enable = true;
3200             } else {
3201                 mAttachmentEditor.setCanSend(true);
3202             }
3203         } else if (null != mAttachmentEditor){
3204             mAttachmentEditor.setCanSend(false);
3205         }
3206 
3207         setSendButtonText(mWorkingMessage.requiresMms());
3208         mSendButton.setEnabled(enable);
3209         mSendButton.setFocusable(enable);
3210     }
3211 
getMessageDate(Uri uri)3212     private long getMessageDate(Uri uri) {
3213         if (uri != null) {
3214             Cursor cursor = SqliteWrapper.query(this, mContentResolver,
3215                     uri, new String[] { Mms.DATE }, null, null, null);
3216             if (cursor != null) {
3217                 try {
3218                     if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
3219                         return cursor.getLong(0) * 1000L;
3220                     }
3221                 } finally {
3222                     cursor.close();
3223                 }
3224             }
3225         }
3226         return NO_DATE_FOR_DIALOG;
3227     }
3228 
initActivityState(Bundle bundle, Intent intent)3229     private void initActivityState(Bundle bundle, Intent intent) {
3230         if (bundle != null) {
3231             String recipients = bundle.getString("recipients");
3232             if (LogTag.VERBOSE) log("get mConversation by recipients " + recipients);
3233             mConversation = Conversation.get(this,
3234                     ContactList.getByNumbers(recipients,
3235                             false /* don't block */, true /* replace number */), false);
3236             addRecipientsListeners();
3237             mExitOnSent = bundle.getBoolean("exit_on_sent", false);
3238             mWorkingMessage.readStateFromBundle(bundle);
3239             return;
3240         }
3241 
3242         // If we have been passed a thread_id, use that to find our conversation.
3243         long threadId = intent.getLongExtra("thread_id", 0);
3244         if (threadId > 0) {
3245             if (LogTag.VERBOSE) log("get mConversation by threadId " + threadId);
3246             mConversation = Conversation.get(this, threadId, false);
3247         } else {
3248             Uri intentData = intent.getData();
3249             if (intentData != null) {
3250                 // try to get a conversation based on the data URI passed to our intent.
3251                 if (LogTag.VERBOSE) log("get mConversation by intentData " + intentData);
3252                 mConversation = Conversation.get(this, intentData, false);
3253                 mWorkingMessage.setText(getBody(intentData));
3254             } else {
3255                 // special intent extra parameter to specify the address
3256                 String address = intent.getStringExtra("address");
3257                 if (!TextUtils.isEmpty(address)) {
3258                     if (LogTag.VERBOSE) log("get mConversation by address " + address);
3259                     mConversation = Conversation.get(this, ContactList.getByNumbers(address,
3260                             false /* don't block */, true /* replace number */), false);
3261                 } else {
3262                     if (LogTag.VERBOSE) log("create new conversation");
3263                     mConversation = Conversation.createNew(this);
3264                 }
3265             }
3266         }
3267         addRecipientsListeners();
3268 
3269         mExitOnSent = intent.getBooleanExtra("exit_on_sent", false);
3270         if (intent.hasExtra("sms_body")) {
3271             mWorkingMessage.setText(intent.getStringExtra("sms_body"));
3272         }
3273         mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
3274     }
3275 
initFocus()3276     private void initFocus() {
3277         if (!mIsKeyboardOpen) {
3278             return;
3279         }
3280 
3281         // If the recipients editor is visible, there is nothing in it,
3282         // and the text editor is not already focused, focus the
3283         // recipients editor.
3284         if (isRecipientsEditorVisible()
3285                 && TextUtils.isEmpty(mRecipientsEditor.getText())
3286                 && !mTextEditor.isFocused()) {
3287             mRecipientsEditor.requestFocus();
3288             return;
3289         }
3290 
3291         // If we decided not to focus the recipients editor, focus the text editor.
3292         mTextEditor.requestFocus();
3293     }
3294 
3295     private final MessageListAdapter.OnDataSetChangedListener
3296                     mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() {
3297         public void onDataSetChanged(MessageListAdapter adapter) {
3298             mPossiblePendingNotification = true;
3299         }
3300 
3301         public void onContentChanged(MessageListAdapter adapter) {
3302             startMsgListQuery();
3303         }
3304     };
3305 
checkPendingNotification()3306     private void checkPendingNotification() {
3307         if (mPossiblePendingNotification && hasWindowFocus()) {
3308             mConversation.markAsRead();
3309             mPossiblePendingNotification = false;
3310         }
3311     }
3312 
3313     private final class BackgroundQueryHandler extends AsyncQueryHandler {
BackgroundQueryHandler(ContentResolver contentResolver)3314         public BackgroundQueryHandler(ContentResolver contentResolver) {
3315             super(contentResolver);
3316         }
3317 
3318         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)3319         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
3320             switch(token) {
3321                 case MESSAGE_LIST_QUERY_TOKEN:
3322                     int newSelectionPos = -1;
3323                     long targetMsgId = getIntent().getLongExtra("select_id", -1);
3324                     if (targetMsgId != -1) {
3325                         cursor.moveToPosition(-1);
3326                         while (cursor.moveToNext()) {
3327                             long msgId = cursor.getLong(COLUMN_ID);
3328                             if (msgId == targetMsgId) {
3329                                 newSelectionPos = cursor.getPosition();
3330                                 break;
3331                             }
3332                         }
3333                     }
3334 
3335                     mMsgListAdapter.changeCursor(cursor);
3336                     if (newSelectionPos != -1) {
3337                         mMsgListView.setSelection(newSelectionPos);
3338                     }
3339 
3340                     // Once we have completed the query for the message history, if
3341                     // there is nothing in the cursor and we are not composing a new
3342                     // message, we must be editing a draft in a new conversation (unless
3343                     // mSentMessage is true).
3344                     // Show the recipients editor to give the user a chance to add
3345                     // more people before the conversation begins.
3346                     if (cursor.getCount() == 0 && !isRecipientsEditorVisible() && !mSentMessage) {
3347                         initRecipientsEditor();
3348                     }
3349 
3350                     // FIXME: freshing layout changes the focused view to an unexpected
3351                     // one, set it back to TextEditor forcely.
3352                     mTextEditor.requestFocus();
3353 
3354                     mConversation.blockMarkAsRead(false);
3355                     return;
3356 
3357                 case ConversationList.HAVE_LOCKED_MESSAGES_TOKEN:
3358                     long threadId = (Long)cookie;
3359                     ConversationList.confirmDeleteThreadDialog(
3360                             new ConversationList.DeleteThreadListener(threadId,
3361                                 mBackgroundQueryHandler, ComposeMessageActivity.this),
3362                             threadId == -1,
3363                             cursor != null && cursor.getCount() > 0,
3364                             ComposeMessageActivity.this);
3365                     break;
3366             }
3367         }
3368 
3369         @Override
onDeleteComplete(int token, Object cookie, int result)3370         protected void onDeleteComplete(int token, Object cookie, int result) {
3371             switch(token) {
3372             case DELETE_MESSAGE_TOKEN:
3373             case ConversationList.DELETE_CONVERSATION_TOKEN:
3374                 // Update the notification for new messages since they
3375                 // may be deleted.
3376                 MessagingNotification.nonBlockingUpdateNewMessageIndicator(
3377                         ComposeMessageActivity.this, false, false);
3378                 // Update the notification for failed messages since they
3379                 // may be deleted.
3380                 updateSendFailedNotification();
3381                 break;
3382             }
3383 
3384             // If we're deleting the whole conversation, throw away
3385             // our current working message and bail.
3386             if (token == ConversationList.DELETE_CONVERSATION_TOKEN) {
3387                 mWorkingMessage.discard();
3388                 Conversation.init(ComposeMessageActivity.this);
3389                 finish();
3390             }
3391         }
3392     }
3393 
showSmileyDialog()3394     private void showSmileyDialog() {
3395         if (mSmileyDialog == null) {
3396             int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS;
3397             String[] names = getResources().getStringArray(
3398                     SmileyParser.DEFAULT_SMILEY_NAMES);
3399             final String[] texts = getResources().getStringArray(
3400                     SmileyParser.DEFAULT_SMILEY_TEXTS);
3401 
3402             final int N = names.length;
3403 
3404             List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>();
3405             for (int i = 0; i < N; i++) {
3406                 // We might have different ASCII for the same icon, skip it if
3407                 // the icon is already added.
3408                 boolean added = false;
3409                 for (int j = 0; j < i; j++) {
3410                     if (icons[i] == icons[j]) {
3411                         added = true;
3412                         break;
3413                     }
3414                 }
3415                 if (!added) {
3416                     HashMap<String, Object> entry = new HashMap<String, Object>();
3417 
3418                     entry. put("icon", icons[i]);
3419                     entry. put("name", names[i]);
3420                     entry.put("text", texts[i]);
3421 
3422                     entries.add(entry);
3423                 }
3424             }
3425 
3426             final SimpleAdapter a = new SimpleAdapter(
3427                     this,
3428                     entries,
3429                     R.layout.smiley_menu_item,
3430                     new String[] {"icon", "name", "text"},
3431                     new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text});
3432             SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
3433                 public boolean setViewValue(View view, Object data, String textRepresentation) {
3434                     if (view instanceof ImageView) {
3435                         Drawable img = getResources().getDrawable((Integer)data);
3436                         ((ImageView)view).setImageDrawable(img);
3437                         return true;
3438                     }
3439                     return false;
3440                 }
3441             };
3442             a.setViewBinder(viewBinder);
3443 
3444             AlertDialog.Builder b = new AlertDialog.Builder(this);
3445 
3446             b.setTitle(getString(R.string.menu_insert_smiley));
3447 
3448             b.setCancelable(true);
3449             b.setAdapter(a, new DialogInterface.OnClickListener() {
3450                 @SuppressWarnings("unchecked")
3451                 public final void onClick(DialogInterface dialog, int which) {
3452                     HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which);
3453                     mTextEditor.append((String)item.get("text"));
3454 
3455                     dialog.dismiss();
3456                 }
3457             });
3458 
3459             mSmileyDialog = b.create();
3460         }
3461 
3462         mSmileyDialog.show();
3463     }
3464 
onUpdate(final Contact updated)3465     public void onUpdate(final Contact updated) {
3466         // Using an existing handler for the post, rather than conjuring up a new one.
3467         mMessageListItemHandler.post(new Runnable() {
3468             public void run() {
3469                 ContactList recipients = isRecipientsEditorVisible() ?
3470                         mRecipientsEditor.constructContactsFromInput() : getRecipients();
3471                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3472                     log("[CMA] onUpdate contact updated: " + updated);
3473                     log("[CMA] onUpdate recipients: " + recipients);
3474                 }
3475                 updateTitle(recipients);
3476 
3477                 // The contact information for one (or more) of the recipients has changed.
3478                 // Rebuild the message list so each MessageItem will get the last contact info.
3479                 ComposeMessageActivity.this.mMsgListAdapter.notifyDataSetChanged();
3480 
3481                 if (mRecipientsEditor != null) {
3482                     mRecipientsEditor.populate(recipients);
3483                 }
3484             }
3485         });
3486     }
3487 
addRecipientsListeners()3488     private void addRecipientsListeners() {
3489         Contact.addListener(this);
3490     }
3491 
removeRecipientsListeners()3492     private void removeRecipientsListeners() {
3493         Contact.removeListener(this);
3494     }
3495 
createIntent(Context context, long threadId)3496     public static Intent createIntent(Context context, long threadId) {
3497         Intent intent = new Intent(context, ComposeMessageActivity.class);
3498 
3499         if (threadId > 0) {
3500             intent.setData(Conversation.getUri(threadId));
3501         }
3502 
3503         return intent;
3504    }
3505 
getBody(Uri uri)3506     private String getBody(Uri uri) {
3507         if (uri == null) {
3508             return null;
3509         }
3510         String urlStr = uri.getSchemeSpecificPart();
3511         if (!urlStr.contains("?")) {
3512             return null;
3513         }
3514         urlStr = urlStr.substring(urlStr.indexOf('?') + 1);
3515         String[] params = urlStr.split("&");
3516         for (String p : params) {
3517             if (p.startsWith("body=")) {
3518                 try {
3519                     return URLDecoder.decode(p.substring(5), "UTF-8");
3520                 } catch (UnsupportedEncodingException e) { }
3521             }
3522         }
3523         return null;
3524     }
3525 }
3526