ComposeActivity.java revision f441d149b334648917c551137a6b4fffdeb83b84
1/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mail.compose;
18
19import android.app.ActionBar;
20import android.app.ActionBar.OnNavigationListener;
21import android.app.Activity;
22import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.Dialog;
25import android.app.DialogFragment;
26import android.app.Fragment;
27import android.app.FragmentTransaction;
28import android.app.LoaderManager;
29import android.content.ClipData;
30import android.content.ContentResolver;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.CursorLoader;
34import android.content.DialogInterface;
35import android.content.Intent;
36import android.content.Loader;
37import android.content.pm.ActivityInfo;
38import android.content.res.Resources;
39import android.database.Cursor;
40import android.net.Uri;
41import android.os.Build;
42import android.os.Bundle;
43import android.os.Handler;
44import android.os.HandlerThread;
45import android.os.ParcelFileDescriptor;
46import android.os.Parcelable;
47import android.provider.BaseColumns;
48import android.text.Editable;
49import android.text.Html;
50import android.text.SpannableString;
51import android.text.Spanned;
52import android.text.TextUtils;
53import android.text.TextWatcher;
54import android.text.util.Rfc822Token;
55import android.text.util.Rfc822Tokenizer;
56import android.view.Gravity;
57import android.view.KeyEvent;
58import android.view.LayoutInflater;
59import android.view.Menu;
60import android.view.MenuInflater;
61import android.view.MenuItem;
62import android.view.View;
63import android.view.View.OnClickListener;
64import android.view.ViewGroup;
65import android.view.inputmethod.BaseInputConnection;
66import android.view.inputmethod.EditorInfo;
67import android.widget.ArrayAdapter;
68import android.widget.Button;
69import android.widget.EditText;
70import android.widget.TextView;
71import android.widget.Toast;
72
73import com.android.common.Rfc822Validator;
74import com.android.common.contacts.DataUsageStatUpdater;
75import com.android.ex.chips.RecipientEditTextView;
76import com.android.mail.MailIntentService;
77import com.android.mail.R;
78import com.android.mail.analytics.Analytics;
79import com.android.mail.browse.MessageHeaderView;
80import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
81import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
82import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
83import com.android.mail.compose.QuotedTextView.RespondInlineListener;
84import com.android.mail.providers.Account;
85import com.android.mail.providers.Address;
86import com.android.mail.providers.Attachment;
87import com.android.mail.providers.Folder;
88import com.android.mail.providers.MailAppProvider;
89import com.android.mail.providers.Message;
90import com.android.mail.providers.MessageModification;
91import com.android.mail.providers.ReplyFromAccount;
92import com.android.mail.providers.Settings;
93import com.android.mail.providers.UIProvider;
94import com.android.mail.providers.UIProvider.AccountCapabilities;
95import com.android.mail.providers.UIProvider.DraftType;
96import com.android.mail.ui.AttachmentTile.AttachmentPreview;
97import com.android.mail.ui.FeedbackEnabledActivity;
98import com.android.mail.ui.MailActivity;
99import com.android.mail.ui.WaitFragment;
100import com.android.mail.utils.AccountUtils;
101import com.android.mail.utils.AttachmentUtils;
102import com.android.mail.utils.ContentProviderTask;
103import com.android.mail.utils.LogTag;
104import com.android.mail.utils.LogUtils;
105import com.android.mail.utils.Utils;
106import com.google.common.annotations.VisibleForTesting;
107import com.google.common.collect.Lists;
108import com.google.common.collect.Sets;
109
110import java.io.FileNotFoundException;
111import java.io.IOException;
112import java.io.UnsupportedEncodingException;
113import java.net.URLDecoder;
114import java.util.ArrayList;
115import java.util.Arrays;
116import java.util.Collection;
117import java.util.HashMap;
118import java.util.HashSet;
119import java.util.List;
120import java.util.Map.Entry;
121import java.util.Set;
122import java.util.concurrent.ConcurrentHashMap;
123
124public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
125        RespondInlineListener, TextWatcher,
126        AttachmentAddedOrDeletedListener, OnAccountChangedListener,
127        LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
128        FeedbackEnabledActivity {
129    // Identifiers for which type of composition this is
130    public static final int COMPOSE = -1;
131    public static final int REPLY = 0;
132    public static final int REPLY_ALL = 1;
133    public static final int FORWARD = 2;
134    public static final int EDIT_DRAFT = 3;
135
136    // Integer extra holding one of the above compose action
137    protected static final String EXTRA_ACTION = "action";
138
139    private static final String EXTRA_SHOW_CC = "showCc";
140    private static final String EXTRA_SHOW_BCC = "showBcc";
141    private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
142    private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
143
144    private static final String UTF8_ENCODING_NAME = "UTF-8";
145
146    private static final String MAIL_TO = "mailto";
147
148    private static final String EXTRA_SUBJECT = "subject";
149
150    private static final String EXTRA_BODY = "body";
151
152    /**
153     * Expected to be html formatted text.
154     */
155    private static final String EXTRA_QUOTED_TEXT = "quotedText";
156
157    protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
158
159    private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
160
161    // Extra that we can get passed from other activities
162    @VisibleForTesting
163    protected static final String EXTRA_TO = "to";
164    private static final String EXTRA_CC = "cc";
165    private static final String EXTRA_BCC = "bcc";
166
167    /**
168     * An optional extra containing a {@link ContentValues} of values to be added to
169     * {@link SendOrSaveMessage#mValues}.
170     */
171    public static final String EXTRA_VALUES = "extra-values";
172
173    // List of all the fields
174    static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
175            EXTRA_QUOTED_TEXT };
176
177    private static SendOrSaveCallback sTestSendOrSaveCallback = null;
178    // Map containing information about requests to create new messages, and the id of the
179    // messages that were the result of those requests.
180    //
181    // This map is used when the activity that initiated the save a of a new message, is killed
182    // before the save has completed (and when we know the id of the newly created message).  When
183    // a save is completed, the service that is running in the background, will update the map
184    //
185    // When a new ComposeActivity instance is created, it will attempt to use the information in
186    // the previously instantiated map.  If ComposeActivity.onCreate() is called, with a bundle
187    // (restoring data from a previous instance), and the map hasn't been created, we will attempt
188    // to populate the map with data stored in shared preferences.
189    // FIXME: values in this map are never read.
190    private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
191    /**
192     * Notifies the {@code Activity} that the caller is an Email
193     * {@code Activity}, so that the back behavior may be modified accordingly.
194     *
195     * @see #onAppUpPressed
196     */
197    public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
198
199    public static final String EXTRA_ATTACHMENTS = "attachments";
200
201    /** If set, we will clear notifications for this folder. */
202    public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
203
204    //  If this is a reply/forward then this extra will hold the original message
205    private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
206    // If this is a reply/forward then this extra will hold a uri we must query
207    // to get the original message.
208    protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
209    // If this is an action to edit an existing draft message, this extra will hold the
210    // draft message
211    private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
212    private static final String END_TOKEN = ", ";
213    private static final String LOG_TAG = LogTag.getLogTag();
214    // Request numbers for activities we start
215    private static final int RESULT_PICK_ATTACHMENT = 1;
216    private static final int RESULT_CREATE_ACCOUNT = 2;
217    // TODO(mindyp) set mime-type for auto send?
218    public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
219
220    private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
221    private static final String EXTRA_REQUEST_ID = "requestId";
222    private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
223    private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
224    private static final String EXTRA_MESSAGE = "extraMessage";
225    private static final int REFERENCE_MESSAGE_LOADER = 0;
226    private static final int LOADER_ACCOUNT_CURSOR = 1;
227    private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
228    private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
229    private static final String TAG_WAIT = "wait-fragment";
230    private static final String MIME_TYPE_ALL = "*/*";
231    private static final String MIME_TYPE_PHOTO = "image/*";
232
233    private static final String KEY_INNER_SAVED_STATE = "compose_state";
234
235    /**
236     * A single thread for running tasks in the background.
237     */
238    private Handler mSendSaveTaskHandler = null;
239    private RecipientEditTextView mTo;
240    private RecipientEditTextView mCc;
241    private RecipientEditTextView mBcc;
242    private Button mCcBccButton;
243    private CcBccView mCcBccView;
244    private AttachmentsView mAttachmentsView;
245    protected Account mAccount;
246    protected ReplyFromAccount mReplyFromAccount;
247    private Settings mCachedSettings;
248    private Rfc822Validator mValidator;
249    private TextView mSubject;
250
251    private ComposeModeAdapter mComposeModeAdapter;
252    protected int mComposeMode = -1;
253    private boolean mForward;
254    private QuotedTextView mQuotedTextView;
255    protected EditText mBodyView;
256    private View mFromStatic;
257    private TextView mFromStaticText;
258    private View mFromSpinnerWrapper;
259    @VisibleForTesting
260    protected FromAddressSpinner mFromSpinner;
261    private boolean mAddingAttachment;
262    private boolean mAttachmentsChanged;
263    private boolean mTextChanged;
264    private boolean mReplyFromChanged;
265    private MenuItem mSave;
266    @VisibleForTesting
267    protected Message mRefMessage;
268    private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
269    private Message mDraft;
270    private ReplyFromAccount mDraftAccount;
271    private Object mDraftLock = new Object();
272    private View mAddAttachmentsButton;
273
274    /**
275     * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
276     */
277    private boolean mLaunchedFromEmail = false;
278    private RecipientTextWatcher mToListener;
279    private RecipientTextWatcher mCcListener;
280    private RecipientTextWatcher mBccListener;
281    private Uri mRefMessageUri;
282    private boolean mShowQuotedText = false;
283    protected Bundle mInnerSavedState;
284    private ContentValues mExtraValues = null;
285
286    // Array of the outstanding send or save tasks.  Access is synchronized
287    // with the object itself
288    /* package for testing */
289    @VisibleForTesting
290    public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
291    // FIXME: this variable is never read. related to sRequestMessageIdMap.
292    private int mRequestId;
293    private String mSignature;
294    private Account[] mAccounts;
295    private boolean mRespondedInline;
296    private boolean mPerformedSendOrDiscard = false;
297
298    /**
299     * Can be called from a non-UI thread.
300     */
301    public static void editDraft(Context launcher, Account account, Message message) {
302        launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
303                null /* extraValues */);
304    }
305
306    /**
307     * Can be called from a non-UI thread.
308     */
309    public static void compose(Context launcher, Account account) {
310        launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
311    }
312
313    /**
314     * Can be called from a non-UI thread.
315     */
316    public static void composeToAddress(Context launcher, Account account, String toAddress) {
317        launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
318                null /* extraValues */);
319    }
320
321    /**
322     * Can be called from a non-UI thread.
323     */
324    public static void composeWithQuotedText(Context launcher, Account account,
325            String quotedText, String subject, final ContentValues extraValues) {
326        launch(launcher, account, null, COMPOSE, null, null, quotedText, subject, extraValues);
327    }
328
329    /**
330     * Can be called from a non-UI thread.
331     */
332    public static void composeWithExtraValues(Context launcher, Account account,
333            String subject, final ContentValues extraValues) {
334        launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
335    }
336
337    /**
338     * Can be called from a non-UI thread.
339     */
340    public static Intent createReplyIntent(final Context launcher, final Account account,
341            final Uri messageUri, final boolean isReplyAll) {
342        return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
343    }
344
345    /**
346     * Can be called from a non-UI thread.
347     */
348    public static Intent createForwardIntent(final Context launcher, final Account account,
349            final Uri messageUri) {
350        return createActionIntent(launcher, account, messageUri, FORWARD);
351    }
352
353    private static Intent createActionIntent(final Context launcher, final Account account,
354            final Uri messageUri, final int action) {
355        final Intent intent = new Intent(launcher, ComposeActivity.class);
356
357        updateActionIntent(account, messageUri, action, intent);
358
359        return intent;
360    }
361
362    @VisibleForTesting
363    static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
364        intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
365        intent.putExtra(EXTRA_ACTION, action);
366        intent.putExtra(Utils.EXTRA_ACCOUNT, account);
367        intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
368
369        return intent;
370    }
371
372    /**
373     * Can be called from a non-UI thread.
374     */
375    public static void reply(Context launcher, Account account, Message message) {
376        launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
377    }
378
379    /**
380     * Can be called from a non-UI thread.
381     */
382    public static void replyAll(Context launcher, Account account, Message message) {
383        launch(launcher, account, message, REPLY_ALL, null, null, null, null,
384                null /* extraValues */);
385    }
386
387    /**
388     * Can be called from a non-UI thread.
389     */
390    public static void forward(Context launcher, Account account, Message message) {
391        launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
392    }
393
394    public static void reportRenderingFeedback(Context launcher, Account account, Message message,
395            String body) {
396        launch(launcher, account, message, FORWARD,
397                "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
398    }
399
400    private static void launch(Context launcher, Account account, Message message, int action,
401            String toAddress, String body, String quotedText, String subject,
402            final ContentValues extraValues) {
403        Intent intent = new Intent(launcher, ComposeActivity.class);
404        intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
405        intent.putExtra(EXTRA_ACTION, action);
406        intent.putExtra(Utils.EXTRA_ACCOUNT, account);
407        if (action == EDIT_DRAFT) {
408            intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
409        } else {
410            intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
411        }
412        if (toAddress != null) {
413            intent.putExtra(EXTRA_TO, toAddress);
414        }
415        if (body != null) {
416            intent.putExtra(EXTRA_BODY, body);
417        }
418        if (quotedText != null) {
419            intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
420        }
421        if (subject != null) {
422            intent.putExtra(EXTRA_SUBJECT, subject);
423        }
424        if (extraValues != null) {
425            LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
426            intent.putExtra(EXTRA_VALUES, extraValues);
427        }
428        launcher.startActivity(intent);
429    }
430
431    @Override
432    protected void onCreate(Bundle savedInstanceState) {
433        super.onCreate(savedInstanceState);
434        setContentView(R.layout.compose);
435        mInnerSavedState = (savedInstanceState != null) ?
436                savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
437        checkValidAccounts();
438    }
439
440    private void finishCreate() {
441        final Bundle savedState = mInnerSavedState;
442        findViews();
443        Intent intent = getIntent();
444        Message message;
445        ArrayList<AttachmentPreview> previews;
446        mShowQuotedText = false;
447        CharSequence quotedText = null;
448        int action;
449        // Check for any of the possibly supplied accounts.;
450        Account account = null;
451        if (hadSavedInstanceStateMessage(savedState)) {
452            action = savedState.getInt(EXTRA_ACTION, COMPOSE);
453            account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
454            message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
455
456            previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
457            mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
458            quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
459
460            mExtraValues = savedState.getParcelable(EXTRA_VALUES);
461        } else {
462            account = obtainAccount(intent);
463            action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
464            // Initialize the message from the message in the intent
465            message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
466            previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
467            mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
468            mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
469
470            if (Analytics.isLoggable()) {
471                if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
472                    Analytics.getInstance().sendEvent(
473                            "notification_action", "compose", getActionString(action), 0);
474                }
475            }
476        }
477        mAttachmentsView.setAttachmentPreviews(previews);
478
479        setAccount(account);
480        if (mAccount == null) {
481            return;
482        }
483
484        initRecipients();
485
486        // Clear the notification and mark the conversation as seen, if necessary
487        final Folder notificationFolder =
488                intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
489        if (notificationFolder != null) {
490            final Intent clearNotifIntent =
491                    new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
492            clearNotifIntent.setPackage(getPackageName());
493            clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
494            clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
495
496            startService(clearNotifIntent);
497        }
498
499        if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
500            mLaunchedFromEmail = true;
501        } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
502            final Uri dataUri = intent.getData();
503            if (dataUri != null) {
504                final String dataScheme = intent.getData().getScheme();
505                final String accountScheme = mAccount.composeIntentUri.getScheme();
506                mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
507            }
508        }
509
510        if (mRefMessageUri != null) {
511            mShowQuotedText = true;
512            mComposeMode = action;
513            getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
514            return;
515        } else if (message != null && action != EDIT_DRAFT) {
516            initFromDraftMessage(message);
517            initQuotedTextFromRefMessage(mRefMessage, action);
518            showCcBcc(savedState);
519            mShowQuotedText = message.appendRefMessageContent;
520            // if we should be showing quoted text but mRefMessage is null
521            // and we have some quotedText, display that
522            if (mShowQuotedText && mRefMessage == null) {
523                if (quotedText != null) {
524                    initQuotedText(quotedText, false /* shouldQuoteText */);
525                } else if (mExtraValues != null) {
526                    initExtraValues(mExtraValues);
527                    return;
528                }
529            }
530        } else if (action == EDIT_DRAFT) {
531            initFromDraftMessage(message);
532            boolean showBcc = !TextUtils.isEmpty(message.getBcc());
533            boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
534            mCcBccView.show(false, showCc, showBcc);
535            // Update the action to the draft type of the previous draft
536            switch (message.draftType) {
537                case UIProvider.DraftType.REPLY:
538                    action = REPLY;
539                    break;
540                case UIProvider.DraftType.REPLY_ALL:
541                    action = REPLY_ALL;
542                    break;
543                case UIProvider.DraftType.FORWARD:
544                    action = FORWARD;
545                    break;
546                case UIProvider.DraftType.COMPOSE:
547                default:
548                    action = COMPOSE;
549                    break;
550            }
551            LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
552
553            mShowQuotedText = message.appendRefMessageContent;
554            if (message.refMessageUri != null) {
555                // If we're editing an existing draft that was in reference to an existing message,
556                // still need to load that original message since we might need to refer to the
557                // original sender and recipients if user switches "reply <-> reply-all".
558                mRefMessageUri = message.refMessageUri;
559                mComposeMode = action;
560                getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
561                return;
562            }
563        } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
564            if (mRefMessage != null) {
565                initFromRefMessage(action);
566                mShowQuotedText = true;
567            }
568        } else {
569            if (initFromExtras(intent)) {
570                return;
571            }
572        }
573
574        mComposeMode = action;
575        finishSetup(action, intent, savedState);
576    }
577
578    private void checkValidAccounts() {
579        final Account[] allAccounts = AccountUtils.getAccounts(this);
580        if (allAccounts == null || allAccounts.length == 0) {
581            final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
582            if (noAccountIntent != null) {
583                mAccounts = null;
584                startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
585            }
586        } else {
587            // If none of the accounts are syncing, setup a watcher.
588            boolean anySyncing = false;
589            for (Account a : allAccounts) {
590                if (a.isAccountReady()) {
591                    anySyncing = true;
592                    break;
593                }
594            }
595            if (!anySyncing) {
596                // There are accounts, but none are sync'd, which is just like having no accounts.
597                mAccounts = null;
598                getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
599                return;
600            }
601            mAccounts = AccountUtils.getSyncingAccounts(this);
602            finishCreate();
603        }
604    }
605
606    private Account obtainAccount(Intent intent) {
607        Account account = null;
608        Object accountExtra = null;
609        if (intent != null && intent.getExtras() != null) {
610            accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
611            if (accountExtra instanceof Account) {
612                return (Account) accountExtra;
613            } else if (accountExtra instanceof String) {
614                // This is the Account attached to the widget compose intent.
615                account = Account.newinstance((String)accountExtra);
616                if (account != null) {
617                    return account;
618                }
619            }
620            accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
621                    intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
622                        intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
623        }
624        if (account == null) {
625            MailAppProvider provider = MailAppProvider.getInstance();
626            String lastAccountUri = provider.getLastSentFromAccount();
627            if (TextUtils.isEmpty(lastAccountUri)) {
628                lastAccountUri = provider.getLastViewedAccount();
629            }
630            if (!TextUtils.isEmpty(lastAccountUri)) {
631                accountExtra = Uri.parse(lastAccountUri);
632            }
633        }
634        if (mAccounts != null && mAccounts.length > 0) {
635            if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
636                // For backwards compatibility, we need to check account
637                // names.
638                for (Account a : mAccounts) {
639                    if (a.getEmailAddress().equals(accountExtra)) {
640                        account = a;
641                    }
642                }
643            } else if (accountExtra instanceof Uri) {
644                // The uri of the last viewed account is what is stored in
645                // the current code base.
646                for (Account a : mAccounts) {
647                    if (a.uri.equals(accountExtra)) {
648                        account = a;
649                    }
650                }
651            }
652            if (account == null) {
653                account = mAccounts[0];
654            }
655        }
656        return account;
657    }
658
659    protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
660        setFocus(action);
661        // Don't bother with the intent if we have procured a message from the
662        // intent already.
663        if (!hadSavedInstanceStateMessage(savedInstanceState)) {
664            initAttachmentsFromIntent(intent);
665        }
666        initActionBar();
667        initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
668                action);
669
670        // If this is a draft message, the draft account is whatever account was
671        // used to open the draft message in Compose.
672        if (mDraft != null) {
673            mDraftAccount = mReplyFromAccount;
674        }
675
676        initChangeListeners();
677        updateHideOrShowCcBcc();
678        updateHideOrShowQuotedText(mShowQuotedText);
679
680        mRespondedInline = mInnerSavedState != null ?
681                mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
682        if (mRespondedInline) {
683            mQuotedTextView.setVisibility(View.GONE);
684        }
685    }
686
687    private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
688        return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
689    }
690
691    private void updateHideOrShowQuotedText(boolean showQuotedText) {
692        mQuotedTextView.updateCheckedState(showQuotedText);
693        mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
694    }
695
696    private void setFocus(int action) {
697        if (action == EDIT_DRAFT) {
698            int type = mDraft.draftType;
699            switch (type) {
700                case UIProvider.DraftType.COMPOSE:
701                case UIProvider.DraftType.FORWARD:
702                    action = COMPOSE;
703                    break;
704                case UIProvider.DraftType.REPLY:
705                case UIProvider.DraftType.REPLY_ALL:
706                default:
707                    action = REPLY;
708                    break;
709            }
710        }
711        switch (action) {
712            case FORWARD:
713            case COMPOSE:
714                if (TextUtils.isEmpty(mTo.getText())) {
715                    mTo.requestFocus();
716                    break;
717                }
718                //$FALL-THROUGH$
719            case REPLY:
720            case REPLY_ALL:
721            default:
722                focusBody();
723                break;
724        }
725    }
726
727    /**
728     * Focus the body of the message.
729     */
730    public void focusBody() {
731        mBodyView.requestFocus();
732        int length = mBodyView.getText().length();
733
734        int signatureStartPos = getSignatureStartPosition(
735                mSignature, mBodyView.getText().toString());
736        if (signatureStartPos > -1) {
737            // In case the user deleted the newlines...
738            mBodyView.setSelection(signatureStartPos);
739        } else if (length >= 0) {
740            // Move cursor to the end.
741            mBodyView.setSelection(length);
742        }
743    }
744
745    @Override
746    protected void onStart() {
747        super.onStart();
748
749        Analytics.getInstance().activityStart(this);
750    }
751
752    @Override
753    protected void onStop() {
754        super.onStop();
755
756        Analytics.getInstance().activityStop(this);
757    }
758
759    @Override
760    protected void onResume() {
761        super.onResume();
762        // Update the from spinner as other accounts
763        // may now be available.
764        if (mFromSpinner != null && mAccount != null) {
765            mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
766        }
767    }
768
769    @Override
770    protected void onPause() {
771        super.onPause();
772
773        // When the user exits the compose view, see if this draft needs saving.
774        // Don't save unnecessary drafts if we are only changing the orientation.
775        if (!isChangingConfigurations()) {
776            saveIfNeeded();
777
778            if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
779                // log saving upon backing out of activity. (we avoid logging every sendOrSave()
780                // because that method can be invoked many times in a single compose session.)
781                logSendOrSave(true /* save */);
782            }
783        }
784    }
785
786    @Override
787    protected final void onActivityResult(int request, int result, Intent data) {
788        if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
789            addAttachmentAndUpdateView(data);
790            mAddingAttachment = false;
791        } else if (request == RESULT_CREATE_ACCOUNT) {
792            // We were waiting for the user to create an account
793            if (result != RESULT_OK) {
794                finish();
795            } else {
796                // Watch for accounts to show up!
797                // restart the loader to get the updated list of accounts
798                getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
799                showWaitFragment(null);
800            }
801        }
802    }
803
804    @Override
805    protected final void onRestoreInstanceState(Bundle savedInstanceState) {
806        final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
807        if (hasAccounts) {
808            clearChangeListeners();
809        }
810        super.onRestoreInstanceState(savedInstanceState);
811        if (mInnerSavedState != null) {
812            if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
813                int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
814                int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
815                // There should be a focus and it should be an EditText since we
816                // only save these extras if these conditions are true.
817                EditText focusEditText = (EditText) getCurrentFocus();
818                final int length = focusEditText.getText().length();
819                if (selectionStart < length && selectionEnd < length) {
820                    focusEditText.setSelection(selectionStart, selectionEnd);
821                }
822            }
823        }
824        if (hasAccounts) {
825            initChangeListeners();
826        }
827    }
828
829    @Override
830    protected final void onSaveInstanceState(Bundle state) {
831        super.onSaveInstanceState(state);
832        final Bundle inner = new Bundle();
833        saveState(inner);
834        state.putBundle(KEY_INNER_SAVED_STATE, inner);
835    }
836
837    private void saveState(Bundle state) {
838        // We have no accounts so there is nothing to compose, and therefore, nothing to save.
839        if (mAccounts == null || mAccounts.length == 0) {
840            return;
841        }
842        // The framework is happy to save and restore the selection but only if it also saves and
843        // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
844        // this manually.
845        View focus = getCurrentFocus();
846        if (focus != null && focus instanceof EditText) {
847            EditText focusEditText = (EditText) focus;
848            state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
849            state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
850        }
851
852        final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
853        final int selectedPos = mFromSpinner.getSelectedItemPosition();
854        final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
855                && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
856                        replyFromAccounts.get(selectedPos) : null;
857        if (selectedReplyFromAccount != null) {
858            state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
859                    .toString());
860            state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
861        } else {
862            state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
863        }
864
865        if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
866            // We don't have a draft id, and we have a request id,
867            // save the request id.
868            state.putInt(EXTRA_REQUEST_ID, mRequestId);
869        }
870
871        // We want to restore the current mode after a pause
872        // or rotation.
873        int mode = getMode();
874        state.putInt(EXTRA_ACTION, mode);
875
876        final Message message = createMessage(selectedReplyFromAccount, mode);
877        if (mDraft != null) {
878            message.id = mDraft.id;
879            message.serverId = mDraft.serverId;
880            message.uri = mDraft.uri;
881        }
882        state.putParcelable(EXTRA_MESSAGE, message);
883
884        if (mRefMessage != null) {
885            state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
886        } else if (message.appendRefMessageContent) {
887            // If we have no ref message but should be appending
888            // ref message content, we have orphaned quoted text. Save it.
889            state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
890        }
891        state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
892        state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
893        state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
894        state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
895        state.putParcelableArrayList(
896                EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
897
898        state.putParcelable(EXTRA_VALUES, mExtraValues);
899    }
900
901    private int getMode() {
902        int mode = ComposeActivity.COMPOSE;
903        ActionBar actionBar = getActionBar();
904        if (actionBar != null
905                && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
906            mode = actionBar.getSelectedNavigationIndex();
907        }
908        return mode;
909    }
910
911    private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
912        Message message = new Message();
913        message.id = UIProvider.INVALID_MESSAGE_ID;
914        message.serverId = null;
915        message.uri = null;
916        message.conversationUri = null;
917        message.subject = mSubject.getText().toString();
918        message.snippet = null;
919        message.setTo(formatSenders(mTo.getText().toString()));
920        message.setCc(formatSenders(mCc.getText().toString()));
921        message.setBcc(formatSenders(mBcc.getText().toString()));
922        message.setReplyTo(null);
923        message.dateReceivedMs = 0;
924        final String htmlBody = Html.toHtml(removeComposingSpans(mBodyView.getText()));
925        final StringBuilder fullBody = new StringBuilder(htmlBody);
926        message.bodyHtml = fullBody.toString();
927        message.bodyText = mBodyView.getText().toString();
928        message.embedsExternalResources = false;
929        message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
930        message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
931        ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
932        message.hasAttachments = attachments != null && attachments.size() > 0;
933        message.attachmentListUri = null;
934        message.messageFlags = 0;
935        message.alwaysShowImages = false;
936        message.attachmentsJson = Attachment.toJSONArray(attachments);
937        CharSequence quotedText = mQuotedTextView.getQuotedText();
938        message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
939                .getQuotedTextOffset(quotedText.toString()) : -1;
940        message.accountUri = null;
941        final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
942                : mAccount != null ? mAccount.getEmailAddress() : null;
943        final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
944                : mAccount != null ? mAccount.getSenderName() : null;
945        final Address address = new Address(senderName, email);
946        message.setFrom(address.toHeader());
947        message.draftType = getDraftType(mode);
948        return message;
949    }
950
951    private static String formatSenders(final String string) {
952        if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
953            return string.substring(0, string.length() - 1);
954        }
955        return string;
956    }
957
958    @VisibleForTesting
959    void setAccount(Account account) {
960        if (account == null) {
961            return;
962        }
963        if (!account.equals(mAccount)) {
964            mAccount = account;
965            mCachedSettings = mAccount.settings;
966            appendSignature();
967        }
968        if (mAccount != null) {
969            MailActivity.setNfcMessage(mAccount.getEmailAddress());
970        }
971    }
972
973    private void initFromSpinner(Bundle bundle, int action) {
974        if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
975            action = COMPOSE;
976        }
977        mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
978
979        if (bundle != null) {
980            if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
981                mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
982                        bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
983            } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
984                final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
985                mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
986            }
987        }
988        if (mReplyFromAccount == null) {
989            if (mDraft != null) {
990                mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
991            } else if (mRefMessage != null) {
992                mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
993            }
994        }
995        if (mReplyFromAccount == null) {
996            mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
997        }
998
999        mFromSpinner.setCurrentAccount(mReplyFromAccount);
1000
1001        if (mFromSpinner.getCount() > 1) {
1002            // If there is only 1 account, just show that account.
1003            // Otherwise, give the user the ability to choose which account to
1004            // send mail from / save drafts to.
1005            mFromStatic.setVisibility(View.GONE);
1006            // TODO: do we want name or address here?
1007            mFromStaticText.setText(mReplyFromAccount.name);
1008            mFromSpinnerWrapper.setVisibility(View.VISIBLE);
1009        } else {
1010            mFromStatic.setVisibility(View.VISIBLE);
1011            // TODO: do we want name or address here?
1012            mFromStaticText.setText(mReplyFromAccount.name);
1013            mFromSpinnerWrapper.setVisibility(View.GONE);
1014        }
1015    }
1016
1017    private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1018        if (refMessage.accountUri != null) {
1019            // This must be from combined inbox.
1020            List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1021            for (ReplyFromAccount from : replyFromAccounts) {
1022                if (from.account.uri.equals(refMessage.accountUri)) {
1023                    return from;
1024                }
1025            }
1026            return null;
1027        } else {
1028            return getReplyFromAccount(account, refMessage);
1029        }
1030    }
1031
1032    /**
1033     * Given an account and the message we're replying to,
1034     * return who the message should be sent from.
1035     * @param account Account in which the message arrived.
1036     * @param refMessage Message to analyze for account selection
1037     * @return the address from which to reply.
1038     */
1039    public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1040        // First see if we are supposed to use the default address or
1041        // the address it was sentTo.
1042        if (mCachedSettings.forceReplyFromDefault) {
1043            return getDefaultReplyFromAccount(account);
1044        } else {
1045            // If we aren't explicitly told which account to look for, look at
1046            // all the message recipients and find one that matches
1047            // a custom from or account.
1048            List<String> allRecipients = new ArrayList<String>();
1049            allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1050            allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
1051            return getMatchingRecipient(account, allRecipients);
1052        }
1053    }
1054
1055    /**
1056     * Compare all the recipients of an email to the current account and all
1057     * custom addresses associated with that account. Return the match if there
1058     * is one, or the default account if there isn't.
1059     */
1060    protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1061        // Tokenize the list and place in a hashmap.
1062        ReplyFromAccount matchingReplyFrom = null;
1063        Rfc822Token[] tokens;
1064        HashSet<String> recipientsMap = new HashSet<String>();
1065        for (String address : sentTo) {
1066            tokens = Rfc822Tokenizer.tokenize(address);
1067            for (int i = 0; i < tokens.length; i++) {
1068                recipientsMap.add(tokens[i].getAddress());
1069            }
1070        }
1071
1072        int matchingAddressCount = 0;
1073        List<ReplyFromAccount> customFroms;
1074        customFroms = account.getReplyFroms();
1075        if (customFroms != null) {
1076            for (ReplyFromAccount entry : customFroms) {
1077                if (recipientsMap.contains(entry.address)) {
1078                    matchingReplyFrom = entry;
1079                    matchingAddressCount++;
1080                }
1081            }
1082        }
1083        if (matchingAddressCount > 1) {
1084            matchingReplyFrom = getDefaultReplyFromAccount(account);
1085        }
1086        return matchingReplyFrom;
1087    }
1088
1089    private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1090        for (final ReplyFromAccount from : account.getReplyFroms()) {
1091            if (from.isDefault) {
1092                return from;
1093            }
1094        }
1095        return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1096                account.getSenderName(), account.getEmailAddress(), true, false);
1097    }
1098
1099    private ReplyFromAccount getReplyFromAccountFromDraft(final Account account,
1100            final Message msg) {
1101        final Address[] draftFroms = Address.parse(msg.getFrom());
1102        final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
1103        ReplyFromAccount replyFromAccount = null;
1104        List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1105        if (TextUtils.equals(account.getEmailAddress(), sender)) {
1106            replyFromAccount = getDefaultReplyFromAccount(account);
1107        } else {
1108            for (ReplyFromAccount fromAccount : replyFromAccounts) {
1109                if (TextUtils.equals(fromAccount.address, sender)) {
1110                    replyFromAccount = fromAccount;
1111                    break;
1112                }
1113            }
1114        }
1115        return replyFromAccount;
1116    }
1117
1118    private void findViews() {
1119        findViewById(R.id.compose).setVisibility(View.VISIBLE);
1120        mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
1121        if (mCcBccButton != null) {
1122            mCcBccButton.setOnClickListener(this);
1123        }
1124        mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
1125        mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
1126        mAddAttachmentsButton = findViewById(R.id.add_attachment);
1127        if (mAddAttachmentsButton != null) {
1128            mAddAttachmentsButton.setOnClickListener(this);
1129        }
1130        mTo = (RecipientEditTextView) findViewById(R.id.to);
1131        mTo.setTokenizer(new Rfc822Tokenizer());
1132        mCc = (RecipientEditTextView) findViewById(R.id.cc);
1133        mCc.setTokenizer(new Rfc822Tokenizer());
1134        mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
1135        mBcc.setTokenizer(new Rfc822Tokenizer());
1136        // TODO: add special chips text change watchers before adding
1137        // this as a text changed watcher to the to, cc, bcc fields.
1138        mSubject = (TextView) findViewById(R.id.subject);
1139        mSubject.setOnEditorActionListener(this);
1140        mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1141        mQuotedTextView.setRespondInlineListener(this);
1142        mBodyView = (EditText) findViewById(R.id.body);
1143        mFromStatic = findViewById(R.id.static_from_content);
1144        mFromStaticText = (TextView) findViewById(R.id.from_account_name);
1145        mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
1146        mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
1147    }
1148
1149    @Override
1150    public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1151        if (action == EditorInfo.IME_ACTION_DONE) {
1152            focusBody();
1153            return true;
1154        }
1155        return false;
1156    }
1157
1158    protected TextView getBody() {
1159        return mBodyView;
1160    }
1161
1162    @VisibleForTesting
1163    public Account getFromAccount() {
1164        return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1165                mReplyFromAccount.account : mAccount;
1166    }
1167
1168    private void clearChangeListeners() {
1169        mSubject.removeTextChangedListener(this);
1170        mBodyView.removeTextChangedListener(this);
1171        mTo.removeTextChangedListener(mToListener);
1172        mCc.removeTextChangedListener(mCcListener);
1173        mBcc.removeTextChangedListener(mBccListener);
1174        mFromSpinner.setOnAccountChangedListener(null);
1175        mAttachmentsView.setAttachmentChangesListener(null);
1176    }
1177
1178    // Now that the message has been initialized from any existing draft or
1179    // ref message data, set up listeners for any changes that occur to the
1180    // message.
1181    private void initChangeListeners() {
1182        // Make sure we only add text changed listeners once!
1183        clearChangeListeners();
1184        mSubject.addTextChangedListener(this);
1185        mBodyView.addTextChangedListener(this);
1186        if (mToListener == null) {
1187            mToListener = new RecipientTextWatcher(mTo, this);
1188        }
1189        mTo.addTextChangedListener(mToListener);
1190        if (mCcListener == null) {
1191            mCcListener = new RecipientTextWatcher(mCc, this);
1192        }
1193        mCc.addTextChangedListener(mCcListener);
1194        if (mBccListener == null) {
1195            mBccListener = new RecipientTextWatcher(mBcc, this);
1196        }
1197        mBcc.addTextChangedListener(mBccListener);
1198        mFromSpinner.setOnAccountChangedListener(this);
1199        mAttachmentsView.setAttachmentChangesListener(this);
1200    }
1201
1202    private void initActionBar() {
1203        LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
1204        ActionBar actionBar = getActionBar();
1205        if (actionBar == null) {
1206            return;
1207        }
1208        if (mComposeMode == ComposeActivity.COMPOSE) {
1209            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1210            actionBar.setTitle(R.string.compose);
1211        } else {
1212            actionBar.setTitle(null);
1213            if (mComposeModeAdapter == null) {
1214                mComposeModeAdapter = new ComposeModeAdapter(this);
1215            }
1216            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1217            actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
1218            switch (mComposeMode) {
1219                case ComposeActivity.REPLY:
1220                    actionBar.setSelectedNavigationItem(0);
1221                    break;
1222                case ComposeActivity.REPLY_ALL:
1223                    actionBar.setSelectedNavigationItem(1);
1224                    break;
1225                case ComposeActivity.FORWARD:
1226                    actionBar.setSelectedNavigationItem(2);
1227                    break;
1228            }
1229        }
1230        actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1231                ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1232        actionBar.setHomeButtonEnabled(true);
1233    }
1234
1235    private void initFromRefMessage(int action) {
1236        setFieldsFromRefMessage(action);
1237
1238        // Check if To: address and email body needs to be prefilled based on extras.
1239        // This is used for reporting rendering feedback.
1240        if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1241            Intent intent = getIntent();
1242            if (intent.getExtras() != null) {
1243                String toAddresses = intent.getStringExtra(EXTRA_TO);
1244                if (toAddresses != null) {
1245                    addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1246                }
1247                String body = intent.getStringExtra(EXTRA_BODY);
1248                if (body != null) {
1249                    setBody(body, false /* withSignature */);
1250                }
1251            }
1252        }
1253
1254        if (mRefMessage != null) {
1255            // CC field only gets populated when doing REPLY_ALL.
1256            // BCC never gets auto-populated, unless the user is editing
1257            // a draft with one.
1258            if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
1259                mCcBccView.show(false, true, false);
1260            }
1261        }
1262        updateHideOrShowCcBcc();
1263    }
1264
1265    private void setFieldsFromRefMessage(int action) {
1266        setSubject(mRefMessage, action);
1267        // Setup recipients
1268        if (action == FORWARD) {
1269            mForward = true;
1270        }
1271        initRecipientsFromRefMessage(mRefMessage, action);
1272        initQuotedTextFromRefMessage(mRefMessage, action);
1273        if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1274            initAttachments(mRefMessage);
1275        }
1276    }
1277
1278    private void initFromDraftMessage(Message message) {
1279        LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
1280
1281        mDraft = message;
1282        mDraftId = message.id;
1283        mSubject.setText(message.subject);
1284        mForward = message.draftType == UIProvider.DraftType.FORWARD;
1285        final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
1286        addToAddresses(toAddresses);
1287        addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1288        addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
1289        if (message.hasAttachments) {
1290            List<Attachment> attachments = message.getAttachments();
1291            for (Attachment a : attachments) {
1292                addAttachmentAndUpdateView(a);
1293            }
1294        }
1295        int quotedTextIndex = message.appendRefMessageContent ?
1296                message.quotedTextOffset : -1;
1297        // Set the body
1298        CharSequence quotedText = null;
1299        if (!TextUtils.isEmpty(message.bodyHtml)) {
1300            CharSequence htmlText = "";
1301            if (quotedTextIndex > -1) {
1302                // Find the offset in the htmltext of the actual quoted text and strip it out.
1303                quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1304                if (quotedTextIndex > -1) {
1305                    htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1306                            quotedTextIndex));
1307                    quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1308                            message.bodyHtml.length());
1309                }
1310            } else {
1311                htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
1312            }
1313            mBodyView.setText(htmlText);
1314        } else {
1315            final String body = message.bodyText;
1316            final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1317                    (quotedTextIndex > -1 ?
1318                            message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1319                            : "";
1320            if (quotedTextIndex > -1) {
1321                quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1322                        : null;
1323            }
1324            mBodyView.setText(bodyText);
1325        }
1326        if (quotedTextIndex > -1 && quotedText != null) {
1327            mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
1328        }
1329    }
1330
1331    /**
1332     * Fill all the widgets with the content found in the Intent Extra, if any.
1333     * Also apply the same style to all widgets. Note: if initFromExtras is
1334     * called as a result of switching between reply, reply all, and forward per
1335     * the latest revision of Gmail, and the user has already made changes to
1336     * attachments on a previous incarnation of the message (as a reply, reply
1337     * all, or forward), the original attachments from the message will not be
1338     * re-instantiated. The user's changes will be respected. This follows the
1339     * web gmail interaction.
1340     * @return {@code true} if the activity should not call {@link #finishSetup}.
1341     */
1342    public boolean initFromExtras(Intent intent) {
1343        // If we were invoked with a SENDTO intent, the value
1344        // should take precedence
1345        final Uri dataUri = intent.getData();
1346        if (dataUri != null) {
1347            if (MAIL_TO.equals(dataUri.getScheme())) {
1348                initFromMailTo(dataUri.toString());
1349            } else {
1350                if (!mAccount.composeIntentUri.equals(dataUri)) {
1351                    String toText = dataUri.getSchemeSpecificPart();
1352                    if (toText != null) {
1353                        mTo.setText("");
1354                        addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
1355                    }
1356                }
1357            }
1358        }
1359
1360        String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1361        if (extraStrings != null) {
1362            addToAddresses(Arrays.asList(extraStrings));
1363        }
1364        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1365        if (extraStrings != null) {
1366            addCcAddresses(Arrays.asList(extraStrings), null);
1367        }
1368        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1369        if (extraStrings != null) {
1370            addBccAddresses(Arrays.asList(extraStrings));
1371        }
1372
1373        String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1374        if (extraString != null) {
1375            mSubject.setText(extraString);
1376        }
1377
1378        for (String extra : ALL_EXTRAS) {
1379            if (intent.hasExtra(extra)) {
1380                String value = intent.getStringExtra(extra);
1381                if (EXTRA_TO.equals(extra)) {
1382                    addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
1383                } else if (EXTRA_CC.equals(extra)) {
1384                    addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
1385                } else if (EXTRA_BCC.equals(extra)) {
1386                    addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
1387                } else if (EXTRA_SUBJECT.equals(extra)) {
1388                    mSubject.setText(value);
1389                } else if (EXTRA_BODY.equals(extra)) {
1390                    setBody(value, true /* with signature */);
1391                } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1392                    initQuotedText(value, true /* shouldQuoteText */);
1393                }
1394            }
1395        }
1396
1397        Bundle extras = intent.getExtras();
1398        if (extras != null) {
1399            CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1400            if (text != null) {
1401                setBody(text, true /* with signature */);
1402            }
1403
1404            // TODO - support EXTRA_HTML_TEXT
1405        }
1406
1407        mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1408        if (mExtraValues != null) {
1409            LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1410            initExtraValues(mExtraValues);
1411            return true;
1412        }
1413
1414        return false;
1415    }
1416
1417    protected void initExtraValues(ContentValues extraValues) {
1418        // DO NOTHING - Gmail will override
1419    }
1420
1421
1422    @VisibleForTesting
1423    protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
1424        // TODO: handle the case where there are spaces in the display name as
1425        // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1426        // as they could be encoded ambiguously.
1427        // Since URLDecode.decode changes + into ' ', and + is a valid
1428        // email character, we need to find/ replace these ourselves before
1429        // decoding.
1430        try {
1431            return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
1432        } catch (IllegalArgumentException e) {
1433            if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1434                LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1435            } else {
1436                LogUtils.e(LOG_TAG, e, "Exception  while decoding mailto address");
1437            }
1438            return null;
1439        }
1440    }
1441
1442    /**
1443     * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1444     * changing '+' into ' '
1445     *
1446     * @param toReplace Input string
1447     * @return The string with all "+" characters replaced with "%2B"
1448     */
1449    private static String replacePlus(String toReplace) {
1450        return toReplace.replace("+", "%2B");
1451    }
1452
1453    /**
1454     * Initialize the compose view from a String representing a mailTo uri.
1455     * @param mailToString The uri as a string.
1456     */
1457    public void initFromMailTo(String mailToString) {
1458        // We need to disguise this string as a URI in order to parse it
1459        // TODO:  Remove this hack when http://b/issue?id=1445295 gets fixed
1460        Uri uri = Uri.parse("foo://" + mailToString);
1461        int index = mailToString.indexOf("?");
1462        int length = "mailto".length() + 1;
1463        String to;
1464        try {
1465            // Extract the recipient after mailto:
1466            if (index == -1) {
1467                to = decodeEmailInUri(mailToString.substring(length));
1468            } else {
1469                to = decodeEmailInUri(mailToString.substring(length, index));
1470            }
1471            if (!TextUtils.isEmpty(to)) {
1472                addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1473            }
1474        } catch (UnsupportedEncodingException e) {
1475            if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1476                LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1477            } else {
1478                LogUtils.e(LOG_TAG, e, "Exception  while decoding mailto address");
1479            }
1480        }
1481
1482        List<String> cc = uri.getQueryParameters("cc");
1483        addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1484
1485        List<String> otherTo = uri.getQueryParameters("to");
1486        addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1487
1488        List<String> bcc = uri.getQueryParameters("bcc");
1489        addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1490
1491        List<String> subject = uri.getQueryParameters("subject");
1492        if (subject.size() > 0) {
1493            try {
1494                mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
1495                        UTF8_ENCODING_NAME));
1496            } catch (UnsupportedEncodingException e) {
1497                LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1498                        e.getMessage(), subject);
1499            }
1500        }
1501
1502        List<String> body = uri.getQueryParameters("body");
1503        if (body.size() > 0) {
1504            try {
1505                setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
1506                        true /* with signature */);
1507            } catch (UnsupportedEncodingException e) {
1508                LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1509            }
1510        }
1511    }
1512
1513    @VisibleForTesting
1514    protected void initAttachments(Message refMessage) {
1515        addAttachments(refMessage.getAttachments());
1516    }
1517
1518    public long addAttachments(List<Attachment> attachments) {
1519        long size = 0;
1520        AttachmentFailureException error = null;
1521        for (Attachment a : attachments) {
1522            try {
1523                size += mAttachmentsView.addAttachment(mAccount, a);
1524            } catch (AttachmentFailureException e) {
1525                error = e;
1526            }
1527        }
1528        if (error != null) {
1529            LogUtils.e(LOG_TAG, error, "Error adding attachment");
1530            if (attachments.size() > 1) {
1531                showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1532            } else {
1533                showAttachmentTooBigToast(error.getErrorRes());
1534            }
1535        }
1536        return size;
1537    }
1538
1539    /**
1540     * When an attachment is too large to be added to a message, show a toast.
1541     * This method also updates the position of the toast so that it is shown
1542     * clearly above they keyboard if it happens to be open.
1543     */
1544    private void showAttachmentTooBigToast(int errorRes) {
1545        String maxSize = AttachmentUtils.convertToHumanReadableSize(
1546                getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1547        showErrorToast(getString(errorRes, maxSize));
1548    }
1549
1550    private void showErrorToast(String message) {
1551        Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1552        t.setText(message);
1553        t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1554                getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1555        t.show();
1556    }
1557
1558    private void initAttachmentsFromIntent(Intent intent) {
1559        Bundle extras = intent.getExtras();
1560        if (extras == null) {
1561            extras = Bundle.EMPTY;
1562        }
1563        final String action = intent.getAction();
1564        if (!mAttachmentsChanged) {
1565            long totalSize = 0;
1566            if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1567                String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1568                for (String uriString : uris) {
1569                    final Uri uri = Uri.parse(uriString);
1570                    long size = 0;
1571                    try {
1572                        final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1573                        size = mAttachmentsView.addAttachment(mAccount, a);
1574
1575                        Analytics.getInstance().sendEvent("send_intent_attachment",
1576                                Utils.normalizeMimeType(a.getContentType()), null, size);
1577
1578                    } catch (AttachmentFailureException e) {
1579                        LogUtils.e(LOG_TAG, e, "Error adding attachment");
1580                        showAttachmentTooBigToast(e.getErrorRes());
1581                    }
1582                    totalSize += size;
1583                }
1584            }
1585            if (extras.containsKey(Intent.EXTRA_STREAM)) {
1586                if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1587                    ArrayList<Parcelable> uris = extras
1588                            .getParcelableArrayList(Intent.EXTRA_STREAM);
1589                    ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1590                    for (Parcelable uri : uris) {
1591                        try {
1592                            final Attachment a = mAttachmentsView.generateLocalAttachment(
1593                                    (Uri) uri);
1594                            attachments.add(a);
1595
1596                            Analytics.getInstance().sendEvent("send_intent_attachment",
1597                                    Utils.normalizeMimeType(a.getContentType()), null, a.size);
1598
1599                        } catch (AttachmentFailureException e) {
1600                            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1601                            String maxSize = AttachmentUtils.convertToHumanReadableSize(
1602                                    getApplicationContext(),
1603                                    mAccount.settings.getMaxAttachmentSize());
1604                            showErrorToast(getString
1605                                    (R.string.generic_attachment_problem, maxSize));
1606                        }
1607                    }
1608                    totalSize += addAttachments(attachments);
1609                } else {
1610                    final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1611                    long size = 0;
1612                    try {
1613                        final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1614                        size = mAttachmentsView.addAttachment(mAccount, a);
1615
1616                        Analytics.getInstance().sendEvent("send_intent_attachment",
1617                                Utils.normalizeMimeType(a.getContentType()), null, size);
1618
1619                    } catch (AttachmentFailureException e) {
1620                        LogUtils.e(LOG_TAG, e, "Error adding attachment");
1621                        showAttachmentTooBigToast(e.getErrorRes());
1622                    }
1623                    totalSize += size;
1624                }
1625            }
1626
1627            if (totalSize > 0) {
1628                mAttachmentsChanged = true;
1629                updateSaveUi();
1630
1631                Analytics.getInstance().sendEvent("send_intent_with_attachments",
1632                        Integer.toString(getAttachments().size()), null, totalSize);
1633            }
1634        }
1635    }
1636
1637    protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
1638        mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1639        mShowQuotedText = true;
1640    }
1641
1642    private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1643        if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
1644            mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1645        }
1646    }
1647
1648    private void updateHideOrShowCcBcc() {
1649        // Its possible there is a menu item OR a button.
1650        boolean ccVisible = mCcBccView.isCcVisible();
1651        boolean bccVisible = mCcBccView.isBccVisible();
1652        if (mCcBccButton != null) {
1653            if (!ccVisible || !bccVisible) {
1654                mCcBccButton.setVisibility(View.VISIBLE);
1655                mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
1656                        : R.string.add_bcc_label));
1657            } else {
1658                mCcBccButton.setVisibility(View.INVISIBLE);
1659            }
1660        }
1661    }
1662
1663    private void showCcBcc(Bundle state) {
1664        if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1665            boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1666            boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1667            if (showCc || showBcc) {
1668                mCcBccView.show(false, showCc, showBcc);
1669            }
1670        }
1671    }
1672
1673    /**
1674     * Add attachment and update the compose area appropriately.
1675     * @param data
1676     */
1677    public void addAttachmentAndUpdateView(Intent data) {
1678        if (data == null) {
1679            return;
1680        }
1681
1682        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1683            final ClipData clipData = data.getClipData();
1684            if (clipData != null) {
1685                for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1686                    addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1687                }
1688                return;
1689            }
1690        }
1691
1692        addAttachmentAndUpdateView(data.getData());
1693    }
1694
1695    public void addAttachmentAndUpdateView(Uri contentUri) {
1696        if (contentUri == null) {
1697            return;
1698        }
1699        try {
1700            addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1701        } catch (AttachmentFailureException e) {
1702            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1703            showErrorToast(getResources().getString(
1704                    e.getErrorRes(),
1705                    AttachmentUtils.convertToHumanReadableSize(
1706                            getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
1707        }
1708    }
1709
1710    public void addAttachmentAndUpdateView(Attachment attachment) {
1711        try {
1712            long size = mAttachmentsView.addAttachment(mAccount, attachment);
1713            if (size > 0) {
1714                mAttachmentsChanged = true;
1715                updateSaveUi();
1716            }
1717        } catch (AttachmentFailureException e) {
1718            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1719            showAttachmentTooBigToast(e.getErrorRes());
1720        }
1721    }
1722
1723    void initRecipientsFromRefMessage(Message refMessage, int action) {
1724        // Don't populate the address if this is a forward.
1725        if (action == ComposeActivity.FORWARD) {
1726            return;
1727        }
1728        initReplyRecipients(refMessage, action);
1729    }
1730
1731    // TODO: This should be private.  This method shouldn't be used by ComposeActivityTests, as
1732    // it doesn't setup the state of the activity correctly
1733    @VisibleForTesting
1734    void initReplyRecipients(final Message refMessage, final int action) {
1735        String[] sentToAddresses = refMessage.getToAddressesUnescaped();
1736        final Collection<String> toAddresses;
1737        final String[] replyToAddresses = refMessage.getReplyToAddressesUnescaped();
1738        String replyToAddress = replyToAddresses.length > 0 ? replyToAddresses[0] : null;
1739        final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1740        final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
1741
1742        // If there is no reply to address, the reply to address is the sender.
1743        if (TextUtils.isEmpty(replyToAddress)) {
1744            replyToAddress = fromAddress;
1745        }
1746
1747        // If this is a reply, the Cc list is empty. If this is a reply-all, the
1748        // Cc list is the union of the To and Cc recipients of the original
1749        // message, excluding the current user's email address and any addresses
1750        // already on the To list.
1751        if (action == ComposeActivity.REPLY) {
1752            toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
1753            addToAddresses(toAddresses);
1754        } else if (action == ComposeActivity.REPLY_ALL) {
1755            final Set<String> ccAddresses = Sets.newHashSet();
1756            toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
1757            addToAddresses(toAddresses);
1758            addRecipients(ccAddresses, sentToAddresses);
1759            addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
1760            addCcAddresses(ccAddresses, toAddresses);
1761        }
1762    }
1763
1764    private void addToAddresses(Collection<String> addresses) {
1765        addAddressesToList(addresses, mTo);
1766    }
1767
1768    private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
1769        addCcAddressesToList(tokenizeAddressList(addresses),
1770                toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
1771    }
1772
1773    private void addBccAddresses(Collection<String> addresses) {
1774        addAddressesToList(addresses, mBcc);
1775    }
1776
1777    @VisibleForTesting
1778    protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1779            List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1780        String address;
1781
1782        if (compareToList == null) {
1783            for (Rfc822Token[] tokens : addresses) {
1784                for (int i = 0; i < tokens.length; i++) {
1785                    address = tokens[i].toString();
1786                    list.append(address + END_TOKEN);
1787                }
1788            }
1789        } else {
1790            HashSet<String> compareTo = convertToHashSet(compareToList);
1791            for (Rfc822Token[] tokens : addresses) {
1792                for (int i = 0; i < tokens.length; i++) {
1793                    address = tokens[i].toString();
1794                    // Check if this is a duplicate:
1795                    if (!compareTo.contains(tokens[i].getAddress())) {
1796                        // Get the address here
1797                        list.append(address + END_TOKEN);
1798                    }
1799                }
1800            }
1801        }
1802    }
1803
1804    private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
1805        final HashSet<String> hash = new HashSet<String>();
1806        for (final Rfc822Token[] tokens : list) {
1807            for (int i = 0; i < tokens.length; i++) {
1808                hash.add(tokens[i].getAddress());
1809            }
1810        }
1811        return hash;
1812    }
1813
1814    protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1815        @VisibleForTesting
1816        List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1817
1818        for (String address: addresses) {
1819            tokenized.add(Rfc822Tokenizer.tokenize(address));
1820        }
1821        return tokenized;
1822    }
1823
1824    @VisibleForTesting
1825    void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1826        for (String address : addresses) {
1827            addAddressToList(address, list);
1828        }
1829    }
1830
1831    private static void addAddressToList(final String address, final RecipientEditTextView list) {
1832        if (address == null || list == null)
1833            return;
1834
1835        final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1836
1837        for (int i = 0; i < tokens.length; i++) {
1838            list.append(tokens[i] + END_TOKEN);
1839        }
1840    }
1841
1842    @VisibleForTesting
1843    protected Collection<String> initToRecipients(final String fullSenderAddress,
1844            final String replyToAddress, final String[] inToAddresses) {
1845        // The To recipient is the reply-to address specified in the original
1846        // message, unless it is:
1847        // the current user OR a custom from of the current user, in which case
1848        // it's the To recipient list of the original message.
1849        // OR missing, in which case use the sender of the original message
1850        Set<String> toAddresses = Sets.newHashSet();
1851        if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) {
1852            toAddresses.add(replyToAddress);
1853        } else {
1854            // In this case, the user is replying to a message in which their
1855            // current account or one of their custom from addresses is the only
1856            // recipient and they sent the original message.
1857            if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
1858                    && recipientMatchesThisAccount(inToAddresses[0])) {
1859                toAddresses.add(inToAddresses[0]);
1860                return toAddresses;
1861            }
1862            // This happens if the user replies to a message they originally
1863            // wrote. In this case, "reply" really means "re-send," so we
1864            // target the original recipients. This works as expected even
1865            // if the user sent the original message to themselves.
1866            for (String address : inToAddresses) {
1867                if (!recipientMatchesThisAccount(address)) {
1868                    toAddresses.add(address);
1869                }
1870            }
1871        }
1872        return toAddresses;
1873    }
1874
1875    private void addRecipients(final Set<String> recipients, final String[] addresses) {
1876        for (final String email : addresses) {
1877            // Do not add this account, or any of its custom from addresses, to
1878            // the list of recipients.
1879            final String recipientAddress = Address.getEmailAddress(email).getAddress();
1880            if (!recipientMatchesThisAccount(recipientAddress)) {
1881                recipients.add(email.replace("\"\"", ""));
1882            }
1883        }
1884    }
1885
1886    /**
1887     * A recipient matches this account if it has the same address as the
1888     * currently selected account OR one of the custom from addresses associated
1889     * with the currently selected account.
1890     * @param recipientAddress address we are comparing with the currently selected account
1891     * @return
1892     */
1893    protected boolean recipientMatchesThisAccount(String recipientAddress) {
1894        return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
1895                        mAccount.getReplyFroms());
1896    }
1897
1898    /**
1899     * Returns a formatted subject string with the appropriate prefix for the action type.
1900     * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
1901     */
1902    public static String buildFormattedSubject(Resources res, String subject, int action) {
1903        String prefix;
1904        String correctedSubject = null;
1905        if (action == ComposeActivity.COMPOSE) {
1906            prefix = "";
1907        } else if (action == ComposeActivity.FORWARD) {
1908            prefix = res.getString(R.string.forward_subject_label);
1909        } else {
1910            prefix = res.getString(R.string.reply_subject_label);
1911        }
1912
1913        // Don't duplicate the prefix
1914        if (!TextUtils.isEmpty(subject)
1915                && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
1916            correctedSubject = subject;
1917        } else {
1918            correctedSubject = String.format(
1919                    res.getString(R.string.formatted_subject), prefix, subject);
1920        }
1921
1922        return correctedSubject;
1923    }
1924
1925    private void setSubject(Message refMessage, int action) {
1926        mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
1927    }
1928
1929    private void initRecipients() {
1930        setupRecipients(mTo);
1931        setupRecipients(mCc);
1932        setupRecipients(mBcc);
1933    }
1934
1935    private void setupRecipients(RecipientEditTextView view) {
1936        view.setAdapter(new RecipientAdapter(this, mAccount));
1937        if (mValidator == null) {
1938            final String accountName = mAccount.getEmailAddress();
1939            int offset = accountName.indexOf("@") + 1;
1940            String account = accountName;
1941            if (offset > 0) {
1942                account = account.substring(offset);
1943            }
1944            mValidator = new Rfc822Validator(account);
1945        }
1946        view.setValidator(mValidator);
1947    }
1948
1949    @Override
1950    public void onClick(View v) {
1951        final int id = v.getId();
1952        if (id == R.id.add_cc_bcc) {
1953            // Verify that cc/ bcc aren't showing.
1954            // Animate in cc/bcc.
1955            showCcBccViews();
1956        } else if (id == R.id.add_attachment) {
1957            doAttach(Utils.isRunningKitkatOrLater() ? MIME_TYPE_ALL : MIME_TYPE_PHOTO);
1958        }
1959    }
1960
1961    @Override
1962    public boolean onCreateOptionsMenu(Menu menu) {
1963        final boolean superCreated = super.onCreateOptionsMenu(menu);
1964        // Don't render any menu items when there are no accounts.
1965        if (mAccounts == null || mAccounts.length == 0) {
1966            return superCreated;
1967        }
1968        MenuInflater inflater = getMenuInflater();
1969        inflater.inflate(R.menu.compose_menu, menu);
1970
1971        /*
1972         * Start save in the correct enabled state.
1973         * 1) If a user launches compose from within gmail, save is disabled
1974         * until they add something, at which point, save is enabled, auto save
1975         * on exit; if the user empties everything, save is disabled, exiting does not
1976         * auto-save
1977         * 2) if a user replies/ reply all/ forwards from within gmail, save is
1978         * disabled until they change something, at which point, save is
1979         * enabled, auto save on exit; if the user empties everything, save is
1980         * disabled, exiting does not auto-save.
1981         * 3) If a user launches compose from another application and something
1982         * gets populated (attachments, recipients, body, subject, etc), save is
1983         * enabled, auto save on exit; if the user empties everything, save is
1984         * disabled, exiting does not auto-save
1985         */
1986        mSave = menu.findItem(R.id.save);
1987        String action = getIntent() != null ? getIntent().getAction() : null;
1988        enableSave(mInnerSavedState != null ?
1989                mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
1990                    : (Intent.ACTION_SEND.equals(action)
1991                            || Intent.ACTION_SEND_MULTIPLE.equals(action)
1992                            || Intent.ACTION_SENDTO.equals(action)
1993                            || shouldSave()));
1994
1995        MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1996        MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1997        if (helpItem != null) {
1998            helpItem.setVisible(mAccount != null
1999                    && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2000        }
2001        if (sendFeedbackItem != null) {
2002            sendFeedbackItem.setVisible(mAccount != null
2003                    && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2004        }
2005
2006        // Show attach picture on pre-K devices.
2007        menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
2008
2009        return true;
2010    }
2011
2012    @Override
2013    public boolean onPrepareOptionsMenu(Menu menu) {
2014        MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
2015        if (ccBcc != null && mCc != null) {
2016            // Its possible there is a menu item OR a button.
2017            boolean ccFieldVisible = mCc.isShown();
2018            boolean bccFieldVisible = mBcc.isShown();
2019            if (!ccFieldVisible || !bccFieldVisible) {
2020                ccBcc.setVisible(true);
2021                ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
2022                        : R.string.add_bcc_label));
2023            } else {
2024                ccBcc.setVisible(false);
2025            }
2026        }
2027        return true;
2028    }
2029
2030    @Override
2031    public boolean onOptionsItemSelected(MenuItem item) {
2032        final int id = item.getItemId();
2033
2034        Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id, null, 0);
2035
2036        boolean handled = true;
2037        if (id == R.id.add_file_attachment) {
2038            doAttach(MIME_TYPE_ALL);
2039        } else if (id == R.id.add_photo_attachment) {
2040            doAttach(MIME_TYPE_PHOTO);
2041        } else if (id == R.id.add_cc_bcc) {
2042            showCcBccViews();
2043        } else if (id == R.id.save) {
2044            doSave(true);
2045        } else if (id == R.id.send) {
2046            doSend();
2047        } else if (id == R.id.discard) {
2048            doDiscard();
2049        } else if (id == R.id.settings) {
2050            Utils.showSettings(this, mAccount);
2051        } else if (id == android.R.id.home) {
2052            onAppUpPressed();
2053        } else if (id == R.id.help_info_menu_item) {
2054            Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2055        } else if (id == R.id.feedback_menu_item) {
2056            Utils.sendFeedback(this, mAccount, false);
2057        } else {
2058            handled = false;
2059        }
2060        return !handled ? super.onOptionsItemSelected(item) : handled;
2061    }
2062
2063    @Override
2064    public void onBackPressed() {
2065        // If we are showing the wait fragment, just exit.
2066        if (getWaitFragment() != null) {
2067            finish();
2068        } else {
2069            super.onBackPressed();
2070        }
2071    }
2072
2073    /**
2074     * Carries out the "up" action in the action bar.
2075     */
2076    private void onAppUpPressed() {
2077        if (mLaunchedFromEmail) {
2078            // If this was started from Gmail, simply treat app up as the system back button, so
2079            // that the last view is restored.
2080            onBackPressed();
2081            return;
2082        }
2083
2084        // Fire the main activity to ensure it launches the "top" screen of mail.
2085        // Since the main Activity is singleTask, it should revive that task if it was already
2086        // started.
2087        final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
2088        mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2089                Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2090        startActivity(mailIntent);
2091        finish();
2092    }
2093
2094    private void doSend() {
2095        sendOrSaveWithSanityChecks(false, true, false, false);
2096        logSendOrSave(false /* save */);
2097        mPerformedSendOrDiscard = true;
2098    }
2099
2100    private void doSave(boolean showToast) {
2101        sendOrSaveWithSanityChecks(true, showToast, false, false);
2102    }
2103
2104    @VisibleForTesting
2105    public interface SendOrSaveCallback {
2106        public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
2107        public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2108        public Message getMessage();
2109        public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2110    }
2111
2112    @VisibleForTesting
2113    public static class SendOrSaveTask implements Runnable {
2114        private final Context mContext;
2115        @VisibleForTesting
2116        public final SendOrSaveCallback mSendOrSaveCallback;
2117        @VisibleForTesting
2118        public final SendOrSaveMessage mSendOrSaveMessage;
2119        private ReplyFromAccount mExistingDraftAccount;
2120
2121        public SendOrSaveTask(Context context, SendOrSaveMessage message,
2122                SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
2123            mContext = context;
2124            mSendOrSaveCallback = callback;
2125            mSendOrSaveMessage = message;
2126            mExistingDraftAccount = draftAccount;
2127        }
2128
2129        @Override
2130        public void run() {
2131            final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
2132
2133            final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
2134            Message message = mSendOrSaveCallback.getMessage();
2135            long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
2136            // If a previous draft has been saved, in an account that is different
2137            // than what the user wants to send from, remove the old draft, and treat this
2138            // as a new message
2139            if (mExistingDraftAccount != null
2140                    && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
2141                if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2142                    ContentResolver resolver = mContext.getContentResolver();
2143                    ContentValues values = new ContentValues();
2144                    values.put(BaseColumns._ID, messageId);
2145                    if (mExistingDraftAccount.account.expungeMessageUri != null) {
2146                        new ContentProviderTask.UpdateTask()
2147                                .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2148                                        values, null, null);
2149                    } else {
2150                        // TODO(mindyp) delete the conversation.
2151                    }
2152                    // reset messageId to 0, so a new message will be created
2153                    messageId = UIProvider.INVALID_MESSAGE_ID;
2154                }
2155            }
2156
2157            final long messageIdToSave = messageId;
2158            sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
2159
2160            if (!sendOrSaveMessage.mSave) {
2161                incrementRecipientsTimesContacted(mContext,
2162                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
2163                incrementRecipientsTimesContacted(mContext,
2164                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
2165                incrementRecipientsTimesContacted(mContext,
2166                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2167            }
2168            mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2169        }
2170
2171        private static void incrementRecipientsTimesContacted(final Context context,
2172                final String addressString) {
2173            if (TextUtils.isEmpty(addressString)) {
2174                return;
2175            }
2176            final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2177            final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
2178            for (int i = 0; i < tokens.length;i++) {
2179                recipients.add(tokens[i].getAddress());
2180            }
2181            final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
2182            statsUpdater.updateWithAddress(recipients);
2183        }
2184
2185        /**
2186         * Send or Save a message.
2187         */
2188        private void sendOrSaveMessage(final long messageIdToSave,
2189                final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
2190            final ContentResolver resolver = mContext.getContentResolver();
2191            final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2192
2193            final String accountMethod = sendOrSaveMessage.mSave ?
2194                    UIProvider.AccountCallMethods.SAVE_MESSAGE :
2195                    UIProvider.AccountCallMethods.SEND_MESSAGE;
2196
2197            try {
2198                if (updateExistingMessage) {
2199                    sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
2200
2201                    callAccountSendSaveMethod(resolver,
2202                            selectedAccount.account, accountMethod, sendOrSaveMessage);
2203                } else {
2204                    Uri messageUri = null;
2205                    final Bundle result = callAccountSendSaveMethod(resolver,
2206                            selectedAccount.account, accountMethod, sendOrSaveMessage);
2207                    if (result != null) {
2208                        // If a non-null value was returned, then the provider handled the call
2209                        // method
2210                        messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
2211                    }
2212                    if (sendOrSaveMessage.mSave && messageUri != null) {
2213                        final Cursor messageCursor = resolver.query(messageUri,
2214                                UIProvider.MESSAGE_PROJECTION, null, null, null);
2215                        if (messageCursor != null) {
2216                            try {
2217                                if (messageCursor.moveToFirst()) {
2218                                    // Broadcast notification that a new message has
2219                                    // been allocated
2220                                    mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2221                                            new Message(messageCursor));
2222                                }
2223                            } finally {
2224                                messageCursor.close();
2225                            }
2226                        }
2227                    }
2228                }
2229            } finally {
2230                // Close any opened file descriptors
2231                closeOpenedAttachmentFds(sendOrSaveMessage);
2232            }
2233        }
2234
2235        private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
2236            final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2237            if (openedFds != null) {
2238                final Set<String> keys = openedFds.keySet();
2239                for (final String key : keys) {
2240                    final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2241                    if (fd != null) {
2242                        try {
2243                            fd.close();
2244                        } catch (IOException e) {
2245                            // Do nothing
2246                        }
2247                    }
2248                }
2249            }
2250        }
2251
2252        /**
2253         * Use the {@link ContentResolver#call} method to send or save the message.
2254         *
2255         * If this was successful, this method will return an non-null Bundle instance
2256         */
2257        private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2258                final Account account, final String method,
2259                final SendOrSaveMessage sendOrSaveMessage) {
2260            // Copy all of the values from the content values to the bundle
2261            final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2262            final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2263
2264            for (Entry<String, Object> entry : valueSet) {
2265                final Object entryValue = entry.getValue();
2266                final String key = entry.getKey();
2267                if (entryValue instanceof String) {
2268                    methodExtras.putString(key, (String)entryValue);
2269                } else if (entryValue instanceof Boolean) {
2270                    methodExtras.putBoolean(key, (Boolean)entryValue);
2271                } else if (entryValue instanceof Integer) {
2272                    methodExtras.putInt(key, (Integer)entryValue);
2273                } else if (entryValue instanceof Long) {
2274                    methodExtras.putLong(key, (Long)entryValue);
2275                } else {
2276                    LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2277                            entryValue.getClass().getName());
2278                }
2279            }
2280
2281            // If the SendOrSaveMessage has some opened fds, add them to the bundle
2282            final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2283            if (fdMap != null) {
2284                methodExtras.putParcelable(
2285                        UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2286            }
2287
2288            return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
2289        }
2290    }
2291
2292    @VisibleForTesting
2293    public static class SendOrSaveMessage {
2294        final ReplyFromAccount mAccount;
2295        final ContentValues mValues;
2296        final String mRefMessageId;
2297        @VisibleForTesting
2298        public final boolean mSave;
2299        final int mRequestId;
2300        private final Bundle mAttachmentFds;
2301
2302        public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2303                String refMessageId, List<Attachment> attachments, boolean save) {
2304            mAccount = account;
2305            mValues = values;
2306            mRefMessageId = refMessageId;
2307            mSave = save;
2308            mRequestId = mValues.hashCode() ^ hashCode();
2309
2310            mAttachmentFds = initializeAttachmentFds(context, attachments);
2311        }
2312
2313        int requestId() {
2314            return mRequestId;
2315        }
2316
2317        Bundle attachmentFds() {
2318            return mAttachmentFds;
2319        }
2320
2321        /**
2322         * Opens {@link ParcelFileDescriptor} for each of the attachments.  This method must be
2323         * called before the ComposeActivity finishes.
2324         * Note: The caller is responsible for closing these file descriptors.
2325         */
2326        private static Bundle initializeAttachmentFds(final Context context,
2327                final List<Attachment> attachments) {
2328            if (attachments == null || attachments.size() == 0) {
2329                return null;
2330            }
2331
2332            final Bundle result = new Bundle(attachments.size());
2333            final ContentResolver resolver = context.getContentResolver();
2334
2335            for (Attachment attachment : attachments) {
2336                if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2337                    continue;
2338                }
2339
2340                ParcelFileDescriptor fileDescriptor;
2341                try {
2342                    fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2343                } catch (FileNotFoundException e) {
2344                    LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2345                    fileDescriptor = null;
2346                } catch (SecurityException e) {
2347                    // We have encountered a security exception when attempting to open the file
2348                    // specified by the content uri.  If the attachment has been cached, this
2349                    // isn't a problem, as even through the original permission may have been
2350                    // revoked, we have cached the file.  This will happen when saving/sending
2351                    // a previously saved draft.
2352                    // TODO(markwei): Expose whether the attachment has been cached through the
2353                    // attachment object.  This would allow us to limit when the log is made, as
2354                    // if the attachment has been cached, this really isn't an error
2355                    LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2356                    // Just set the file descriptor to null, as the underlying provider needs
2357                    // to handle the file descriptor not being set.
2358                    fileDescriptor = null;
2359                }
2360
2361                if (fileDescriptor != null) {
2362                    result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2363                }
2364            }
2365
2366            return result;
2367        }
2368    }
2369
2370    /**
2371     * Get the to recipients.
2372     */
2373    public String[] getToAddresses() {
2374        return getAddressesFromList(mTo);
2375    }
2376
2377    /**
2378     * Get the cc recipients.
2379     */
2380    public String[] getCcAddresses() {
2381        return getAddressesFromList(mCc);
2382    }
2383
2384    /**
2385     * Get the bcc recipients.
2386     */
2387    public String[] getBccAddresses() {
2388        return getAddressesFromList(mBcc);
2389    }
2390
2391    public String[] getAddressesFromList(RecipientEditTextView list) {
2392        if (list == null) {
2393            return new String[0];
2394        }
2395        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2396        int count = tokens.length;
2397        String[] result = new String[count];
2398        for (int i = 0; i < count; i++) {
2399            result[i] = tokens[i].toString();
2400        }
2401        return result;
2402    }
2403
2404    /**
2405     * Check for invalid email addresses.
2406     * @param to String array of email addresses to check.
2407     * @param wrongEmailsOut Emails addresses that were invalid.
2408     */
2409    public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
2410        if (mValidator == null) {
2411            return;
2412        }
2413        for (final String email : to) {
2414            if (!mValidator.isValid(email)) {
2415                wrongEmailsOut.add(email);
2416            }
2417        }
2418    }
2419
2420    public static class RecipientErrorDialogFragment extends DialogFragment {
2421        // Public no-args constructor needed for fragment re-instantiation
2422        public RecipientErrorDialogFragment() {}
2423
2424        public static RecipientErrorDialogFragment newInstance(final String message) {
2425            final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2426            final Bundle args = new Bundle(1);
2427            args.putString("message", message);
2428            frag.setArguments(args);
2429            return frag;
2430        }
2431
2432        @Override
2433        public Dialog onCreateDialog(Bundle savedInstanceState) {
2434            final String message = getArguments().getString("message");
2435            return new AlertDialog.Builder(getActivity())
2436                    .setMessage(message)
2437                    .setPositiveButton(
2438                            R.string.ok, new Dialog.OnClickListener() {
2439                        @Override
2440                        public void onClick(DialogInterface dialog, int which) {
2441                            ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2442                        }
2443                    }).create();
2444        }
2445    }
2446
2447    private void finishRecipientErrorDialog() {
2448        // after the user dismisses the recipient error
2449        // dialog we want to make sure to refocus the
2450        // recipient to field so they can fix the issue
2451        // easily
2452        if (mTo != null) {
2453            mTo.requestFocus();
2454        }
2455    }
2456
2457    /**
2458     * Show an error because the user has entered an invalid recipient.
2459     * @param message
2460     */
2461    private void showRecipientErrorDialog(final String message) {
2462        final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2463        frag.show(getFragmentManager(), "recipient error");
2464    }
2465
2466    /**
2467     * Update the state of the UI based on whether or not the current draft
2468     * needs to be saved and the message is not empty.
2469     */
2470    public void updateSaveUi() {
2471        if (mSave != null) {
2472            mSave.setEnabled((shouldSave() && !isBlank()));
2473        }
2474    }
2475
2476    /**
2477     * Returns true if we need to save the current draft.
2478     */
2479    private boolean shouldSave() {
2480        synchronized (mDraftLock) {
2481            // The message should only be saved if:
2482            // It hasn't been sent AND
2483            // Some text has been added to the message OR
2484            // an attachment has been added or removed
2485            // AND there is actually something in the draft to save.
2486            return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
2487                    && !isBlank();
2488        }
2489    }
2490
2491    /**
2492     * Check if all fields are blank.
2493     * @return boolean
2494     */
2495    public boolean isBlank() {
2496        // Need to check for null since isBlank() can be called from onPause()
2497        // before findViews() is called
2498        if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2499                mAttachmentsView == null) {
2500            LogUtils.w(LOG_TAG, "null views in isBlank check");
2501            return true;
2502        }
2503        return mSubject.getText().length() == 0
2504                && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2505                        mBodyView.getText().toString()) == 0)
2506                && mTo.length() == 0
2507                && mCc.length() == 0 && mBcc.length() == 0
2508                && mAttachmentsView.getAttachments().size() == 0;
2509    }
2510
2511    @VisibleForTesting
2512    protected int getSignatureStartPosition(String signature, String bodyText) {
2513        int startPos = -1;
2514
2515        if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2516            return startPos;
2517        }
2518
2519        int bodyLength = bodyText.length();
2520        int signatureLength = signature.length();
2521        String printableVersion = convertToPrintableSignature(signature);
2522        int printableLength = printableVersion.length();
2523
2524        if (bodyLength >= printableLength
2525                && bodyText.substring(bodyLength - printableLength)
2526                .equals(printableVersion)) {
2527            startPos = bodyLength - printableLength;
2528        } else if (bodyLength >= signatureLength
2529                && bodyText.substring(bodyLength - signatureLength)
2530                .equals(signature)) {
2531            startPos = bodyLength - signatureLength;
2532        }
2533        return startPos;
2534    }
2535
2536    /**
2537     * Allows any changes made by the user to be ignored. Called when the user
2538     * decides to discard a draft.
2539     */
2540    private void discardChanges() {
2541        mTextChanged = false;
2542        mAttachmentsChanged = false;
2543        mReplyFromChanged = false;
2544    }
2545
2546    /**
2547     * @param save
2548     * @param showToast
2549     * @return Whether the send or save succeeded.
2550     */
2551    protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
2552            final boolean orientationChanged, final boolean autoSend) {
2553        if (mAccounts == null || mAccount == null) {
2554            Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
2555            if (autoSend) {
2556                finish();
2557            }
2558            return false;
2559        }
2560
2561        final String[] to, cc, bcc;
2562        if (orientationChanged) {
2563            to = cc = bcc = new String[0];
2564        } else {
2565            to = getToAddresses();
2566            cc = getCcAddresses();
2567            bcc = getBccAddresses();
2568        }
2569
2570        // Don't let the user send to nobody (but it's okay to save a message
2571        // with no recipients)
2572        if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2573            showRecipientErrorDialog(getString(R.string.recipient_needed));
2574            return false;
2575        }
2576
2577        List<String> wrongEmails = new ArrayList<String>();
2578        if (!save) {
2579            checkInvalidEmails(to, wrongEmails);
2580            checkInvalidEmails(cc, wrongEmails);
2581            checkInvalidEmails(bcc, wrongEmails);
2582        }
2583
2584        // Don't let the user send an email with invalid recipients
2585        if (wrongEmails.size() > 0) {
2586            String errorText = String.format(getString(R.string.invalid_recipient),
2587                    wrongEmails.get(0));
2588            showRecipientErrorDialog(errorText);
2589            return false;
2590        }
2591
2592        // Show a warning before sending only if there are no attachments.
2593        if (!save) {
2594            if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2595                boolean warnAboutEmptySubject = isSubjectEmpty();
2596                boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
2597
2598                // A warning about an empty body may not be warranted when
2599                // forwarding mails, since a common use case is to forward
2600                // quoted text and not append any more text.
2601                boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
2602
2603                // When we bring up a dialog warning the user about a send,
2604                // assume that they accept sending the message. If they do not,
2605                // the dialog listener is required to enable sending again.
2606                if (warnAboutEmptySubject) {
2607                    showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, save,
2608                            showToast);
2609                    return true;
2610                }
2611
2612                if (warnAboutEmptyBody) {
2613                    showSendConfirmDialog(R.string.confirm_send_message_with_no_body, save,
2614                            showToast);
2615                    return true;
2616                }
2617            }
2618            // Ask for confirmation to send (if always required)
2619            if (showSendConfirmation()) {
2620                showSendConfirmDialog(R.string.confirm_send_message, save, showToast);
2621                return true;
2622            }
2623        }
2624
2625        sendOrSave(save, showToast);
2626        return true;
2627    }
2628
2629    /**
2630     * Returns a boolean indicating whether warnings should be shown for empty
2631     * subject and body fields
2632     *
2633     * @return True if a warning should be shown for empty text fields
2634     */
2635    protected boolean showEmptyTextWarnings() {
2636        return mAttachmentsView.getAttachments().size() == 0;
2637    }
2638
2639    /**
2640     * Returns a boolean indicating whether the user should confirm each send
2641     *
2642     * @return True if a warning should be on each send
2643     */
2644    protected boolean showSendConfirmation() {
2645        return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2646    }
2647
2648    public static class SendConfirmDialogFragment extends DialogFragment
2649            implements DialogInterface.OnClickListener {
2650
2651        private boolean mSave;
2652        private boolean mShowToast;
2653
2654        // Public no-args constructor needed for fragment re-instantiation
2655        public SendConfirmDialogFragment() {}
2656
2657        public static SendConfirmDialogFragment newInstance(final int messageId,
2658                final boolean save, final boolean showToast) {
2659            final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2660            final Bundle args = new Bundle(3);
2661            args.putInt("messageId", messageId);
2662            args.putBoolean("save", save);
2663            args.putBoolean("showToast", showToast);
2664            frag.setArguments(args);
2665            return frag;
2666        }
2667
2668        @Override
2669        public Dialog onCreateDialog(Bundle savedInstanceState) {
2670            final int messageId = getArguments().getInt("messageId");
2671            mSave = getArguments().getBoolean("save");
2672            mShowToast = getArguments().getBoolean("showToast");
2673
2674            final int confirmTextId = (messageId == R.string.confirm_send_message) ?
2675                    R.string.ok : R.string.send;
2676
2677            return new AlertDialog.Builder(getActivity())
2678                    .setMessage(messageId)
2679                    .setPositiveButton(confirmTextId, this)
2680                    .setNegativeButton(R.string.cancel, null)
2681                    .create();
2682        }
2683
2684        @Override
2685        public void onClick(DialogInterface dialog, int which) {
2686            if (which == DialogInterface.BUTTON_POSITIVE) {
2687                ((ComposeActivity) getActivity()).finishSendConfirmDialog(mSave, mShowToast);
2688            }
2689        }
2690    }
2691
2692    private void finishSendConfirmDialog(final boolean save, final boolean showToast) {
2693        sendOrSave(save, showToast);
2694    }
2695
2696    private void showSendConfirmDialog(final int messageId, final boolean save,
2697            final boolean showToast) {
2698        final DialogFragment frag = SendConfirmDialogFragment.newInstance(messageId, save,
2699                showToast);
2700        frag.show(getFragmentManager(), "send confirm");
2701    }
2702
2703    /**
2704     * Returns whether the ComposeArea believes there is any text in the body of
2705     * the composition. TODO: When ComposeArea controls the Body as well, add
2706     * that here.
2707     */
2708    public boolean isBodyEmpty() {
2709        return !mQuotedTextView.isTextIncluded();
2710    }
2711
2712    /**
2713     * Test to see if the subject is empty.
2714     *
2715     * @return boolean.
2716     */
2717    // TODO: this will likely go away when composeArea.focus() is implemented
2718    // after all the widget control is moved over.
2719    public boolean isSubjectEmpty() {
2720        return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2721    }
2722
2723    /* package */
2724    static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
2725            Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
2726            SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
2727            ReplyFromAccount draftAccount, final ContentValues extraValues) {
2728        final ContentValues values = new ContentValues();
2729
2730        final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
2731
2732        MessageModification.putToAddresses(values, message.getToAddresses());
2733        MessageModification.putCcAddresses(values, message.getCcAddresses());
2734        MessageModification.putBccAddresses(values, message.getBccAddresses());
2735
2736        MessageModification.putCustomFromAddress(values, message.getFrom());
2737
2738        MessageModification.putSubject(values, message.subject);
2739        // Make sure to remove only the composing spans from the Spannable before saving.
2740        final String htmlBody = Html.toHtml(removeComposingSpans(body));
2741
2742        boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2743        StringBuilder fullBody = new StringBuilder(htmlBody);
2744        if (includeQuotedText) {
2745            // HTML gets converted to text for now
2746            final String text = quotedText.toString();
2747            if (QuotedTextView.containsQuotedText(text)) {
2748                int pos = QuotedTextView.getQuotedTextOffset(text);
2749                final int quoteStartPos = fullBody.length() + pos;
2750                fullBody.append(text);
2751                MessageModification.putQuoteStartPos(values, quoteStartPos);
2752                MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
2753                MessageModification.putAppendRefMessageContent(values, includeQuotedText);
2754            } else {
2755                LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2756                // This shouldn't happen, but just use what we have,
2757                // and don't do server-side expansion
2758                fullBody.append(text);
2759            }
2760        }
2761        int draftType = getDraftType(composeMode);
2762        MessageModification.putDraftType(values, draftType);
2763        if (refMessage != null) {
2764            if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2765                MessageModification.putBodyHtml(values, fullBody.toString());
2766            }
2767            if (!TextUtils.isEmpty(refMessage.bodyText)) {
2768                MessageModification.putBody(values,
2769                        Utils.convertHtmlToPlainText(fullBody.toString()).toString());
2770            }
2771        } else {
2772            MessageModification.putBodyHtml(values, fullBody.toString());
2773            MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2774                    .toString());
2775        }
2776        MessageModification.putAttachments(values, message.getAttachments());
2777        if (!TextUtils.isEmpty(refMessageId)) {
2778            MessageModification.putRefMessageId(values, refMessageId);
2779        }
2780        if (extraValues != null) {
2781            values.putAll(extraValues);
2782        }
2783        SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2784                values, refMessageId, message.getAttachments(), save);
2785        SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2786                draftAccount);
2787
2788        callback.initializeSendOrSave(sendOrSaveTask);
2789        // Do the send/save action on the specified handler to avoid possible
2790        // ANRs
2791        handler.post(sendOrSaveTask);
2792
2793        return sendOrSaveMessage.requestId();
2794    }
2795
2796    /**
2797     * Removes any composing spans from the specified string.  This will create a new
2798     * SpannableString instance, as to not modify the behavior of the EditText view.
2799     */
2800    private static SpannableString removeComposingSpans(Spanned body) {
2801        final SpannableString messageBody = new SpannableString(body);
2802        BaseInputConnection.removeComposingSpans(messageBody);
2803        return messageBody;
2804    }
2805
2806    private static int getDraftType(int mode) {
2807        int draftType = -1;
2808        switch (mode) {
2809            case ComposeActivity.COMPOSE:
2810                draftType = DraftType.COMPOSE;
2811                break;
2812            case ComposeActivity.REPLY:
2813                draftType = DraftType.REPLY;
2814                break;
2815            case ComposeActivity.REPLY_ALL:
2816                draftType = DraftType.REPLY_ALL;
2817                break;
2818            case ComposeActivity.FORWARD:
2819                draftType = DraftType.FORWARD;
2820                break;
2821        }
2822        return draftType;
2823    }
2824
2825    private void sendOrSave(final boolean save, final boolean showToast) {
2826        // Check if user is a monkey. Monkeys can compose and hit send
2827        // button but are not allowed to send anything off the device.
2828        if (ActivityManager.isUserAMonkey()) {
2829            return;
2830        }
2831
2832        final Spanned body = mBodyView.getEditableText();
2833
2834        SendOrSaveCallback callback = new SendOrSaveCallback() {
2835            // FIXME: unused
2836            private int mRestoredRequestId;
2837
2838            @Override
2839            public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
2840                synchronized (mActiveTasks) {
2841                    int numTasks = mActiveTasks.size();
2842                    if (numTasks == 0) {
2843                        // Start service so we won't be killed if this app is
2844                        // put in the background.
2845                        startService(new Intent(ComposeActivity.this, EmptyService.class));
2846                    }
2847
2848                    mActiveTasks.add(sendOrSaveTask);
2849                }
2850                if (sTestSendOrSaveCallback != null) {
2851                    sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2852                }
2853            }
2854
2855            @Override
2856            public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2857                    Message message) {
2858                synchronized (mDraftLock) {
2859                    mDraftAccount = sendOrSaveMessage.mAccount;
2860                    mDraftId = message.id;
2861                    mDraft = message;
2862                    if (sRequestMessageIdMap != null) {
2863                        sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2864                    }
2865                    // Cache request message map, in case the process is killed
2866                    saveRequestMap();
2867                }
2868                if (sTestSendOrSaveCallback != null) {
2869                    sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
2870                }
2871            }
2872
2873            @Override
2874            public Message getMessage() {
2875                synchronized (mDraftLock) {
2876                    return mDraft;
2877                }
2878            }
2879
2880            @Override
2881            public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
2882                // Update the last sent from account.
2883                if (mAccount != null) {
2884                    MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2885                }
2886                if (success) {
2887                    // Successfully sent or saved so reset change markers
2888                    discardChanges();
2889                } else {
2890                    // A failure happened with saving/sending the draft
2891                    // TODO(pwestbro): add a better string that should be used
2892                    // when failing to send or save
2893                    Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2894                            .show();
2895                }
2896
2897                int numTasks;
2898                synchronized (mActiveTasks) {
2899                    // Remove the task from the list of active tasks
2900                    mActiveTasks.remove(task);
2901                    numTasks = mActiveTasks.size();
2902                }
2903
2904                if (numTasks == 0) {
2905                    // Stop service so we can be killed.
2906                    stopService(new Intent(ComposeActivity.this, EmptyService.class));
2907                }
2908                if (sTestSendOrSaveCallback != null) {
2909                    sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2910                }
2911            }
2912        };
2913
2914        setAccount(mReplyFromAccount.account);
2915
2916        if (mSendSaveTaskHandler == null) {
2917            HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2918            handlerThread.start();
2919
2920            mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2921        }
2922
2923        Message msg = createMessage(mReplyFromAccount, getMode());
2924        mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2925                mQuotedTextView.getQuotedTextIfIncluded(), callback,
2926                mSendSaveTaskHandler, save, mComposeMode, mDraftAccount, mExtraValues);
2927
2928        // Don't display the toast if the user is just changing the orientation,
2929        // but we still need to save the draft to the cursor because this is how we restore
2930        // the attachments when the configuration change completes.
2931        if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2932            Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2933                    Toast.LENGTH_LONG).show();
2934        }
2935
2936        // Need to update variables here because the send or save completes
2937        // asynchronously even though the toast shows right away.
2938        discardChanges();
2939        updateSaveUi();
2940
2941        // If we are sending, finish the activity
2942        if (!save) {
2943            finish();
2944        }
2945    }
2946
2947    /**
2948     * Save the state of the request messageid map. This allows for the Gmail
2949     * process to be killed, but and still allow for ComposeActivity instances
2950     * to be recreated correctly.
2951     */
2952    private void saveRequestMap() {
2953        // TODO: store the request map in user preferences.
2954    }
2955
2956    private void doAttach(String type) {
2957        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2958        i.addCategory(Intent.CATEGORY_OPENABLE);
2959        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
2960        i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
2961        i.setType(type);
2962        mAddingAttachment = true;
2963        startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2964                RESULT_PICK_ATTACHMENT);
2965    }
2966
2967    private void showCcBccViews() {
2968        mCcBccView.show(true, true, true);
2969        if (mCcBccButton != null) {
2970            mCcBccButton.setVisibility(View.INVISIBLE);
2971        }
2972    }
2973
2974    private static String getActionString(int action) {
2975        final String msgType;
2976        switch (action) {
2977            case COMPOSE:
2978                msgType = "new_message";
2979                break;
2980            case REPLY:
2981                msgType = "reply";
2982                break;
2983            case REPLY_ALL:
2984                msgType = "reply_all";
2985                break;
2986            case FORWARD:
2987                msgType = "forward";
2988                break;
2989            default:
2990                msgType = "unknown";
2991                break;
2992        }
2993        return msgType;
2994    }
2995
2996    private void logSendOrSave(boolean save) {
2997        if (!Analytics.isLoggable() || mAttachmentsView == null) {
2998            return;
2999        }
3000
3001        final String category = (save) ? "message_save" : "message_send";
3002        final int attachmentCount = getAttachments().size();
3003        final String msgType = getActionString(mComposeMode);
3004        final String label;
3005        final long value;
3006        if (mComposeMode == COMPOSE) {
3007            label = Integer.toString(attachmentCount);
3008            value = attachmentCount;
3009        } else {
3010            label = null;
3011            value = 0;
3012        }
3013        Analytics.getInstance().sendEvent(category, msgType, label, value);
3014    }
3015
3016    @Override
3017    public boolean onNavigationItemSelected(int position, long itemId) {
3018        int initialComposeMode = mComposeMode;
3019        if (position == ComposeActivity.REPLY) {
3020            mComposeMode = ComposeActivity.REPLY;
3021        } else if (position == ComposeActivity.REPLY_ALL) {
3022            mComposeMode = ComposeActivity.REPLY_ALL;
3023        } else if (position == ComposeActivity.FORWARD) {
3024            mComposeMode = ComposeActivity.FORWARD;
3025        }
3026        clearChangeListeners();
3027        if (initialComposeMode != mComposeMode) {
3028            resetMessageForModeChange();
3029            if (mRefMessage != null) {
3030                setFieldsFromRefMessage(mComposeMode);
3031            }
3032            boolean showCc = false;
3033            boolean showBcc = false;
3034            if (mDraft != null) {
3035                // Following desktop behavior, if the user has added a BCC
3036                // field to a draft, we show it regardless of compose mode.
3037                showBcc = !TextUtils.isEmpty(mDraft.getBcc());
3038                // Use the draft to determine what to populate.
3039                // If the Bcc field is showing, show the Cc field whether it is populated or not.
3040                showCc = showBcc
3041                        || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
3042            }
3043            if (mRefMessage != null) {
3044                showCc = !TextUtils.isEmpty(mCc.getText());
3045                showBcc = !TextUtils.isEmpty(mBcc.getText());
3046            }
3047            mCcBccView.show(false, showCc, showBcc);
3048        }
3049        updateHideOrShowCcBcc();
3050        initChangeListeners();
3051        return true;
3052    }
3053
3054    @VisibleForTesting
3055    protected void resetMessageForModeChange() {
3056        // When switching between reply, reply all, forward,
3057        // follow the behavior of webview.
3058        // The contents of the following fields are cleared
3059        // so that they can be populated directly from the
3060        // ref message:
3061        // 1) Any recipient fields
3062        // 2) The subject
3063        mTo.setText("");
3064        mCc.setText("");
3065        mBcc.setText("");
3066        // Any edits to the subject are replaced with the original subject.
3067        mSubject.setText("");
3068
3069        // Any changes to the contents of the following fields are kept:
3070        // 1) Body
3071        // 2) Attachments
3072        // If the user made changes to attachments, keep their changes.
3073        if (!mAttachmentsChanged) {
3074            mAttachmentsView.deleteAllAttachments();
3075        }
3076    }
3077
3078    private class ComposeModeAdapter extends ArrayAdapter<String> {
3079
3080        private LayoutInflater mInflater;
3081
3082        public ComposeModeAdapter(Context context) {
3083            super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3084                    .getStringArray(R.array.compose_modes));
3085        }
3086
3087        private LayoutInflater getInflater() {
3088            if (mInflater == null) {
3089                mInflater = LayoutInflater.from(getContext());
3090            }
3091            return mInflater;
3092        }
3093
3094        @Override
3095        public View getView(int position, View convertView, ViewGroup parent) {
3096            if (convertView == null) {
3097                convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3098            }
3099            ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3100            return super.getView(position, convertView, parent);
3101        }
3102    }
3103
3104    @Override
3105    public void onRespondInline(String text) {
3106        appendToBody(text, false);
3107        mQuotedTextView.setUpperDividerVisible(false);
3108        mRespondedInline = true;
3109        if (!mBodyView.hasFocus()) {
3110            mBodyView.requestFocus();
3111        }
3112    }
3113
3114    /**
3115     * Append text to the body of the message. If there is no existing body
3116     * text, just sets the body to text.
3117     *
3118     * @param text
3119     * @param withSignature True to append a signature.
3120     */
3121    public void appendToBody(CharSequence text, boolean withSignature) {
3122        Editable bodyText = mBodyView.getEditableText();
3123        if (bodyText != null && bodyText.length() > 0) {
3124            bodyText.append(text);
3125        } else {
3126            setBody(text, withSignature);
3127        }
3128    }
3129
3130    /**
3131     * Set the body of the message.
3132     *
3133     * @param text
3134     * @param withSignature True to append a signature.
3135     */
3136    public void setBody(CharSequence text, boolean withSignature) {
3137        mBodyView.setText(text);
3138        if (withSignature) {
3139            appendSignature();
3140        }
3141    }
3142
3143    private void appendSignature() {
3144        String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3145        boolean hasFocus = mBodyView.hasFocus();
3146        int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
3147        if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
3148            mSignature = newSignature;
3149            if (!TextUtils.isEmpty(mSignature)) {
3150                // Appending a signature does not count as changing text.
3151                mBodyView.removeTextChangedListener(this);
3152                mBodyView.append(convertToPrintableSignature(mSignature));
3153                mBodyView.addTextChangedListener(this);
3154            }
3155            if (hasFocus) {
3156                focusBody();
3157            }
3158        }
3159    }
3160
3161    private String convertToPrintableSignature(String signature) {
3162        String signatureResource = getResources().getString(R.string.signature);
3163        if (signature == null) {
3164            signature = "";
3165        }
3166        return String.format(signatureResource, signature);
3167    }
3168
3169    @Override
3170    public void onAccountChanged() {
3171        mReplyFromAccount = mFromSpinner.getCurrentAccount();
3172        if (!mAccount.equals(mReplyFromAccount.account)) {
3173            // Clear a signature, if there was one.
3174            mBodyView.removeTextChangedListener(this);
3175            String oldSignature = mSignature;
3176            String bodyText = getBody().getText().toString();
3177            if (!TextUtils.isEmpty(oldSignature)) {
3178                int pos = getSignatureStartPosition(oldSignature, bodyText);
3179                if (pos > -1) {
3180                    mBodyView.setText(bodyText.substring(0, pos));
3181                }
3182            }
3183            setAccount(mReplyFromAccount.account);
3184            mBodyView.addTextChangedListener(this);
3185            // TODO: handle discarding attachments when switching accounts.
3186            // Only enable save for this draft if there is any other content
3187            // in the message.
3188            if (!isBlank()) {
3189                enableSave(true);
3190            }
3191            mReplyFromChanged = true;
3192            initRecipients();
3193        }
3194    }
3195
3196    public void enableSave(boolean enabled) {
3197        if (mSave != null) {
3198            mSave.setEnabled(enabled);
3199        }
3200    }
3201
3202    public static class DiscardConfirmDialogFragment extends DialogFragment {
3203        // Public no-args constructor needed for fragment re-instantiation
3204        public DiscardConfirmDialogFragment() {}
3205
3206        @Override
3207        public Dialog onCreateDialog(Bundle savedInstanceState) {
3208            return new AlertDialog.Builder(getActivity())
3209                    .setMessage(R.string.confirm_discard_text)
3210                    .setPositiveButton(R.string.discard,
3211                            new DialogInterface.OnClickListener() {
3212                                @Override
3213                                public void onClick(DialogInterface dialog, int which) {
3214                                    ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3215                                }
3216                            })
3217                    .setNegativeButton(R.string.cancel, null)
3218                    .create();
3219        }
3220    }
3221
3222    private void doDiscard() {
3223        final DialogFragment frag = new DiscardConfirmDialogFragment();
3224        frag.show(getFragmentManager(), "discard confirm");
3225    }
3226    /**
3227     * Effectively discard the current message.
3228     *
3229     * This method is either invoked from the menu or from the dialog
3230     * once the user has confirmed that they want to discard the message.
3231     */
3232    private void doDiscardWithoutConfirmation() {
3233        synchronized (mDraftLock) {
3234            if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3235                ContentValues values = new ContentValues();
3236                values.put(BaseColumns._ID, mDraftId);
3237                if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
3238                    getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3239                } else {
3240                    getContentResolver().delete(mDraft.uri, null, null);
3241                }
3242                // This is not strictly necessary (since we should not try to
3243                // save the draft after calling this) but it ensures that if we
3244                // do save again for some reason we make a new draft rather than
3245                // trying to resave an expunged draft.
3246                mDraftId = UIProvider.INVALID_MESSAGE_ID;
3247            }
3248        }
3249
3250        // Display a toast to let the user know
3251        Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
3252
3253        // This prevents the draft from being saved in onPause().
3254        discardChanges();
3255        mPerformedSendOrDiscard = true;
3256        finish();
3257    }
3258
3259    private void saveIfNeeded() {
3260        if (mAccount == null) {
3261            // We have not chosen an account yet so there's no way that we can save. This is ok,
3262            // though, since we are saving our state before AccountsActivity is activated. Thus, the
3263            // user has not interacted with us yet and there is no real state to save.
3264            return;
3265        }
3266
3267        if (shouldSave()) {
3268            doSave(!mAddingAttachment /* show toast */);
3269        }
3270    }
3271
3272    @Override
3273    public void onAttachmentDeleted() {
3274        mAttachmentsChanged = true;
3275        // If we are showing any attachments, make sure we have an upper
3276        // divider.
3277        mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3278        updateSaveUi();
3279    }
3280
3281    @Override
3282    public void onAttachmentAdded() {
3283        mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3284        mAttachmentsView.focusLastAttachment();
3285    }
3286
3287    /**
3288     * This is called any time one of our text fields changes.
3289     */
3290    @Override
3291    public void afterTextChanged(Editable s) {
3292        mTextChanged = true;
3293        updateSaveUi();
3294    }
3295
3296    @Override
3297    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3298        // Do nothing.
3299    }
3300
3301    @Override
3302    public void onTextChanged(CharSequence s, int start, int before, int count) {
3303        // Do nothing.
3304    }
3305
3306
3307    // There is a big difference between the text associated with an address changing
3308    // to add the display name or to format properly and a recipient being added or deleted.
3309    // Make sure we only notify of changes when a recipient has been added or deleted.
3310    private class RecipientTextWatcher implements TextWatcher {
3311        private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3312
3313        private RecipientEditTextView mView;
3314
3315        private TextWatcher mListener;
3316
3317        public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3318            mView = view;
3319            mListener = listener;
3320        }
3321
3322        @Override
3323        public void afterTextChanged(Editable s) {
3324            if (hasChanged()) {
3325                mListener.afterTextChanged(s);
3326            }
3327        }
3328
3329        private boolean hasChanged() {
3330            String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
3331            int totalCount = currRecips.length;
3332            int totalPrevCount = 0;
3333            for (Entry<String, Integer> entry : mContent.entrySet()) {
3334                totalPrevCount += entry.getValue();
3335            }
3336            if (totalCount != totalPrevCount) {
3337                return true;
3338            }
3339
3340            for (String recip : currRecips) {
3341                if (!mContent.containsKey(recip)) {
3342                    return true;
3343                } else {
3344                    int count = mContent.get(recip) - 1;
3345                    if (count < 0) {
3346                        return true;
3347                    } else {
3348                        mContent.put(recip, count);
3349                    }
3350                }
3351            }
3352            return false;
3353        }
3354
3355        private String[] tokenizeRecips(String[] recips) {
3356            // Tokenize them all and put them in the list.
3357            String[] recipAddresses = new String[recips.length];
3358            for (int i = 0; i < recips.length; i++) {
3359                recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
3360            }
3361            return recipAddresses;
3362        }
3363
3364        @Override
3365        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3366            String[] recips = tokenizeRecips(getAddressesFromList(mView));
3367            for (String recip : recips) {
3368                if (!mContent.containsKey(recip)) {
3369                    mContent.put(recip, 1);
3370                } else {
3371                    mContent.put(recip, (mContent.get(recip)) + 1);
3372                }
3373            }
3374        }
3375
3376        @Override
3377        public void onTextChanged(CharSequence s, int start, int before, int count) {
3378            // Do nothing.
3379        }
3380    }
3381
3382    public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3383        if (sTestSendOrSaveCallback != null && testCallback != null) {
3384            throw new IllegalStateException("Attempting to register more than one test callback");
3385        }
3386        sTestSendOrSaveCallback = testCallback;
3387    }
3388
3389    @VisibleForTesting
3390    protected ArrayList<Attachment> getAttachments() {
3391        return mAttachmentsView.getAttachments();
3392    }
3393
3394    @Override
3395    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3396        switch (id) {
3397            case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3398                return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3399                        null, null);
3400            case REFERENCE_MESSAGE_LOADER:
3401                return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3402                        null, null);
3403            case LOADER_ACCOUNT_CURSOR:
3404                return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3405                        UIProvider.ACCOUNTS_PROJECTION, null, null, null);
3406        }
3407        return null;
3408    }
3409
3410    @Override
3411    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
3412        int id = loader.getId();
3413        switch (id) {
3414            case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3415                if (data != null && data.moveToFirst()) {
3416                    mRefMessage = new Message(data);
3417                    Intent intent = getIntent();
3418                    initFromRefMessage(mComposeMode);
3419                    finishSetup(mComposeMode, intent, null);
3420                    if (mComposeMode != FORWARD) {
3421                        String to = intent.getStringExtra(EXTRA_TO);
3422                        if (!TextUtils.isEmpty(to)) {
3423                            mRefMessage.setTo(null);
3424                            mRefMessage.setFrom(null);
3425                            clearChangeListeners();
3426                            mTo.append(to);
3427                            initChangeListeners();
3428                        }
3429                    }
3430                } else {
3431                    finish();
3432                }
3433                break;
3434            case REFERENCE_MESSAGE_LOADER:
3435                // Only populate mRefMessage and leave other fields untouched.
3436                if (data != null && data.moveToFirst()) {
3437                    mRefMessage = new Message(data);
3438                }
3439                finishSetup(mComposeMode, getIntent(), mInnerSavedState);
3440                break;
3441            case LOADER_ACCOUNT_CURSOR:
3442                if (data != null && data.moveToFirst()) {
3443                    // there are accounts now!
3444                    Account account;
3445                    final ArrayList<Account> accounts = new ArrayList<Account>();
3446                    final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
3447                    do {
3448                        account = new Account(data);
3449                        if (account.isAccountReady()) {
3450                            initializedAccounts.add(account);
3451                        }
3452                        accounts.add(account);
3453                    } while (data.moveToNext());
3454                    if (initializedAccounts.size() > 0) {
3455                        findViewById(R.id.wait).setVisibility(View.GONE);
3456                        getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3457                        findViewById(R.id.compose).setVisibility(View.VISIBLE);
3458                        mAccounts = initializedAccounts.toArray(
3459                                new Account[initializedAccounts.size()]);
3460
3461                        finishCreate();
3462                        invalidateOptionsMenu();
3463                    } else {
3464                        // Show "waiting"
3465                        account = accounts.size() > 0 ? accounts.get(0) : null;
3466                        showWaitFragment(account);
3467                    }
3468                }
3469                break;
3470        }
3471    }
3472
3473    private void showWaitFragment(Account account) {
3474        WaitFragment fragment = getWaitFragment();
3475        if (fragment != null) {
3476            fragment.updateAccount(account);
3477        } else {
3478            findViewById(R.id.wait).setVisibility(View.VISIBLE);
3479            replaceFragment(WaitFragment.newInstance(account, true),
3480                    FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3481        }
3482    }
3483
3484    private WaitFragment getWaitFragment() {
3485        return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3486    }
3487
3488    private int replaceFragment(Fragment fragment, int transition, String tag) {
3489        FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
3490        fragmentTransaction.setTransition(transition);
3491        fragmentTransaction.replace(R.id.wait, fragment, tag);
3492        final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3493        return transactionId;
3494    }
3495
3496    @Override
3497    public void onLoaderReset(Loader<Cursor> arg0) {
3498        // Do nothing.
3499    }
3500
3501    @Override
3502    public Context getActivityContext() {
3503        return this;
3504    }
3505}
3506