blob: e3558bd48400df2ee3ed3ab16e58f223cde936b1 [file] [log] [blame]
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001/**
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
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Mindy Pereira326c6602012-01-04 15:32:42 -080019import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070020import android.app.ActionBar.OnNavigationListener;
21import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080022import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.Dialog;
Tony Mantler2558b502013-07-09 10:53:34 -070025import android.app.DialogFragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070026import android.app.Fragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070027import android.app.FragmentTransaction;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070028import android.app.LoaderManager;
Mindy Pereira6349a042012-01-04 11:25:01 -080029import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080030import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080031import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070032import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080033import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080034import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070035import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080036import android.content.pm.ActivityInfo;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -070037import android.content.res.Resources;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080038import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080039import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080040import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080041import android.os.Handler;
42import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070043import android.os.ParcelFileDescriptor;
Alice Yang1ebc2db2013-03-14 21:21:44 -070044import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080045import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080046import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080047import android.text.Html;
mindyped9c2f02012-10-12 10:02:08 -070048import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080050import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080051import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080052import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080053import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070054import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070055import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080056import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080057import android.view.Menu;
58import android.view.MenuInflater;
59import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080060import android.view.View;
61import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070062import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080063import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070064import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080065import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080066import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070067import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080068import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080069import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080070
Mindy Pereirac17d0732011-12-29 10:46:19 -080071import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070072import com.android.common.contacts.DataUsageStatUpdater;
Andy Huang5c5fd572012-04-08 18:19:29 -070073import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080074import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070075import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070076import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070077import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070078import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080079import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080080import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080081import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080082import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080083import com.android.mail.providers.Address;
84import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080085import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070086import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080087import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080088import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070089import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080090import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080091import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070092import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070093import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -070094import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Paul Westbrook83e6b572013-02-05 16:22:42 -080095import com.android.mail.ui.FeedbackEnabledActivity;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -070096import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -070097import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -070098import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -070099import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800100import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -0700101import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800102import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800103import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800104import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800105import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800106import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800107
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700108import java.io.FileNotFoundException;
109import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700110import java.io.UnsupportedEncodingException;
111import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800112import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700113import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800114import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800115import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800116import java.util.HashSet;
117import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700118import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700119import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800120import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800121
122public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700123 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700124 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
125 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
126 FeedbackEnabledActivity {
Mindy Pereira6349a042012-01-04 11:25:01 -0800127 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700128 public static final int COMPOSE = -1;
129 public static final int REPLY = 0;
130 public static final int REPLY_ALL = 1;
131 public static final int FORWARD = 2;
132 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800133
134 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700135 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800136
Mindy Pereira326689d2012-05-17 10:14:14 -0700137 private static final String EXTRA_SHOW_CC = "showCc";
138 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800139 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800140 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700141
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700142 private static final String UTF8_ENCODING_NAME = "UTF-8";
143
144 private static final String MAIL_TO = "mailto";
145
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700146 private static final String EXTRA_SUBJECT = "subject";
147
148 private static final String EXTRA_BODY = "body";
149
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700150 /**
151 * Expected to be html formatted text.
152 */
153 private static final String EXTRA_QUOTED_TEXT = "quotedText";
154
mindypd27b6ea2012-10-05 09:43:49 -0700155 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700156
Mark Wei62066e42012-09-13 12:07:02 -0700157 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
158
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700159 // Extra that we can get passed from other activities
160 private static final String EXTRA_TO = "to";
161 private static final String EXTRA_CC = "cc";
162 private static final String EXTRA_BCC = "bcc";
163
Scott Kennedy60847252013-08-15 15:55:42 -0700164 /**
165 * An optional extra containing a {@link ContentValues} of values to be added to
166 * {@link SendOrSaveMessage#mValues}.
167 */
168 public static final String EXTRA_VALUES = "extra-values";
169
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700170 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700171 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
172 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700173
Mindy Pereira82cc5662012-01-09 17:29:30 -0800174 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
175 // Map containing information about requests to create new messages, and the id of the
176 // messages that were the result of those requests.
177 //
178 // This map is used when the activity that initiated the save a of a new message, is killed
179 // before the save has completed (and when we know the id of the newly created message). When
180 // a save is completed, the service that is running in the background, will update the map
181 //
182 // When a new ComposeActivity instance is created, it will attempt to use the information in
183 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
184 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
185 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700186 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800187 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800188 /**
189 * Notifies the {@code Activity} that the caller is an Email
190 * {@code Activity}, so that the back behavior may be modified accordingly.
191 *
192 * @see #onAppUpPressed
193 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700194 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800195
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700196 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700197
Scott Kennedy5680ec22013-01-07 13:15:20 -0800198 /** If set, we will clear notifications for this folder. */
199 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
200
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800201 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700202 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700203 // If this is a reply/forward then this extra will hold a uri we must query
204 // to get the original message.
205 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700206 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700207 // draft message
208 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800209 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700210 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800211 // Request numbers for activities we start
212 private static final int RESULT_PICK_ATTACHMENT = 1;
213 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700214 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700215 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700216
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700217 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
218 private static final String EXTRA_REQUEST_ID = "requestId";
219 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700220 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700221 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700222 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700223 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700224 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700225 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700226 private static final String TAG_WAIT = "wait-fragment";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700227 private static final String MIME_TYPE_PHOTO = "image/*";
228 private static final String MIME_TYPE_VIDEO = "video/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800229
Andy Huang9f855d62013-05-30 17:15:03 -0700230 private static final String KEY_INNER_SAVED_STATE = "compose_state";
231
Mindy Pereira82cc5662012-01-09 17:29:30 -0800232 /**
233 * A single thread for running tasks in the background.
234 */
235 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800236 private RecipientEditTextView mTo;
237 private RecipientEditTextView mCc;
238 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800239 private Button mCcBccButton;
240 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800241 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800242 private Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700243 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800244 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800245 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800246 private TextView mSubject;
247
Mindy Pereira326c6602012-01-04 15:32:42 -0800248 private ComposeModeAdapter mComposeModeAdapter;
249 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800250 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800251 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700252 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800253 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800254 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800255 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700256 @VisibleForTesting
257 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800258 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800259 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800260 private boolean mTextChanged;
261 private boolean mReplyFromChanged;
262 private MenuItem mSave;
263 private MenuItem mSend;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700264 @VisibleForTesting
265 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800266 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
267 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800268 private ReplyFromAccount mDraftAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800269 private Object mDraftLock = new Object();
mindyp93b079b2012-08-29 16:32:15 -0700270 private View mPhotoAttachmentsButton;
271 private View mVideoAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800272
Mindy Pereira326c6602012-01-04 15:32:42 -0800273 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700274 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
275 */
276 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700277 private RecipientTextWatcher mToListener;
278 private RecipientTextWatcher mCcListener;
279 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700280 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700281 private boolean mShowQuotedText = false;
Andy Huang9f855d62013-05-30 17:15:03 -0700282 private Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700283 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700284
mindyp1623f9b2012-11-21 12:41:16 -0800285 // Array of the outstanding send or save tasks. Access is synchronized
286 // with the object itself
287 /* package for testing */
288 @VisibleForTesting
289 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
290 // FIXME: this variable is never read. related to sRequestMessageIdMap.
291 private int mRequestId;
292 private String mSignature;
293 private Account[] mAccounts;
294 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700295 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800296
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700297 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800298 * Can be called from a non-UI thread.
299 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800300 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700301 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
302 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800303 }
304
Mindy Pereira6349a042012-01-04 11:25:01 -0800305 /**
306 * Can be called from a non-UI thread.
307 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800308 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700309 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800310 }
311
312 /**
313 * Can be called from a non-UI thread.
314 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700315 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700316 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
317 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700318 }
319
320 /**
321 * Can be called from a non-UI thread.
322 */
323 public static void composeWithQuotedText(Context launcher, Account account,
Scott Kennedy60847252013-08-15 15:55:42 -0700324 String quotedText, String subject, final ContentValues extraValues) {
325 launch(launcher, account, null, COMPOSE, null, null, quotedText, subject, extraValues);
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700326 }
327
328 /**
329 * Can be called from a non-UI thread.
330 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800331 public static Intent createReplyIntent(final Context launcher, final Account account,
332 final Uri messageUri, final boolean isReplyAll) {
333 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
334 }
335
336 /**
337 * Can be called from a non-UI thread.
338 */
339 public static Intent createForwardIntent(final Context launcher, final Account account,
340 final Uri messageUri) {
341 return createActionIntent(launcher, account, messageUri, FORWARD);
342 }
343
344 private static Intent createActionIntent(final Context launcher, final Account account,
345 final Uri messageUri, final int action) {
346 final Intent intent = new Intent(launcher, ComposeActivity.class);
347
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700348 updateActionIntent(account, messageUri, action, intent);
349
350 return intent;
351 }
352
353 @VisibleForTesting
354 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800355 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
356 intent.putExtra(EXTRA_ACTION, action);
357 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
358 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
359
360 return intent;
361 }
362
363 /**
364 * Can be called from a non-UI thread.
365 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800366 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700367 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800368 }
369
370 /**
371 * Can be called from a non-UI thread.
372 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800373 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700374 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
375 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800376 }
377
378 /**
379 * Can be called from a non-UI thread.
380 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800381 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700382 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800383 }
384
Alice Yang1ebc2db2013-03-14 21:21:44 -0700385 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
386 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700387 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700388 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700389 }
390
391 private static void launch(Context launcher, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700392 String toAddress, String body, String quotedText, String subject,
393 final ContentValues extraValues) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800394 Intent intent = new Intent(launcher, ComposeActivity.class);
395 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
396 intent.putExtra(EXTRA_ACTION, action);
397 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700398 if (action == EDIT_DRAFT) {
399 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
400 } else {
401 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
402 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700403 if (toAddress != null) {
404 intent.putExtra(EXTRA_TO, toAddress);
405 }
406 if (body != null) {
407 intent.putExtra(EXTRA_BODY, body);
408 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700409 if (quotedText != null) {
410 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
411 }
412 if (subject != null) {
413 intent.putExtra(EXTRA_SUBJECT, subject);
414 }
Scott Kennedy60847252013-08-15 15:55:42 -0700415 if (extraValues != null) {
416 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
417 intent.putExtra(EXTRA_VALUES, extraValues);
418 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800419 launcher.startActivity(intent);
420 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800421
422 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700423 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800424 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800425 setContentView(R.layout.compose);
Andy Huang9f855d62013-05-30 17:15:03 -0700426 mInnerSavedState = (savedInstanceState != null) ?
427 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700428 checkValidAccounts();
429 }
430
431 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700432 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800433 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800434 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700435 Message message;
Mark Wei62066e42012-09-13 12:07:02 -0700436 ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700437 mShowQuotedText = false;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700438 CharSequence quotedText = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700439 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700440 // Check for any of the possibly supplied accounts.;
441 Account account = null;
Andy Huang9f855d62013-05-30 17:15:03 -0700442 if (hadSavedInstanceStateMessage(savedState)) {
443 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
444 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
445 message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700446
Andy Huang9f855d62013-05-30 17:15:03 -0700447 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
448 mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700449 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700450
451 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700452 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700453 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700454 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
455 // Initialize the message from the message in the intent
456 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700457 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700458 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700459 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700460 }
Mark Wei62066e42012-09-13 12:07:02 -0700461 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700462
463 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800464 if (mAccount == null) {
465 return;
466 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700467
Scott Kennedyfe853d32013-06-19 11:47:35 -0700468 initRecipients();
469
Scott Kennedy5680ec22013-01-07 13:15:20 -0800470 // Clear the notification and mark the conversation as seen, if necessary
471 final Folder notificationFolder =
472 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
473 if (notificationFolder != null) {
474 final Intent clearNotifIntent =
475 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800476 clearNotifIntent.setPackage(getPackageName());
Scott Kennedy48cfe462013-04-10 11:32:02 -0700477 clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
478 clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800479
480 startService(clearNotifIntent);
481 }
482
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700483 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
484 mLaunchedFromEmail = true;
485 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
486 final Uri dataUri = intent.getData();
487 if (dataUri != null) {
488 final String dataScheme = intent.getData().getScheme();
489 final String accountScheme = mAccount.composeIntentUri.getScheme();
490 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
491 }
492 }
493
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700494 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700495 mShowQuotedText = true;
496 mComposeMode = action;
497 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700498 return;
499 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700500 initFromDraftMessage(message);
501 initQuotedTextFromRefMessage(mRefMessage, action);
Andy Huang9f855d62013-05-30 17:15:03 -0700502 showCcBcc(savedState);
Alice Yanga990a712013-03-13 18:37:00 -0700503 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700504 // if we should be showing quoted text but mRefMessage is null
505 // and we have some quotedText, display that
506 if (mShowQuotedText && quotedText != null && mRefMessage == null) {
507 initQuotedText(quotedText, false /* shouldQuoteText */);
508 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700509 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700510 initFromDraftMessage(message);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800511 boolean showBcc = !TextUtils.isEmpty(message.getBcc());
512 boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
Mindy Pereiraef388302012-06-18 19:07:44 -0700513 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700514 // Update the action to the draft type of the previous draft
515 switch (message.draftType) {
516 case UIProvider.DraftType.REPLY:
517 action = REPLY;
518 break;
519 case UIProvider.DraftType.REPLY_ALL:
520 action = REPLY_ALL;
521 break;
522 case UIProvider.DraftType.FORWARD:
523 action = FORWARD;
524 break;
525 case UIProvider.DraftType.COMPOSE:
526 default:
527 action = COMPOSE;
528 break;
529 }
Alice Yanga990a712013-03-13 18:37:00 -0700530 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
531
532 mShowQuotedText = message.appendRefMessageContent;
533 if (message.refMessageUri != null) {
534 // If we're editing an existing draft that was in reference to an existing message,
535 // still need to load that original message since we might need to refer to the
536 // original sender and recipients if user switches "reply <-> reply-all".
537 mRefMessageUri = message.refMessageUri;
538 mComposeMode = action;
539 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
540 return;
541 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700542 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
543 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800544 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700545 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700546 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700547 } else {
548 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700549 }
Alice Yanga990a712013-03-13 18:37:00 -0700550
551 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700552 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700553 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700554
Mindy Pereirab199d172012-08-13 11:04:03 -0700555 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700556 final Account[] allAccounts = AccountUtils.getAccounts(this);
557 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700558 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
559 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700560 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700561 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
562 }
563 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700564 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700565 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700566 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700567 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700568 anySyncing = true;
569 break;
570 }
571 }
572 if (!anySyncing) {
573 // There are accounts, but none are sync'd, which is just like having no accounts.
574 mAccounts = null;
575 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
576 return;
577 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700578 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700579 finishCreate();
580 }
581 }
582
Mindy Pereira47d0e652012-07-23 09:45:07 -0700583 private Account obtainAccount(Intent intent) {
584 Account account = null;
585 Object accountExtra = null;
586 if (intent != null && intent.getExtras() != null) {
587 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
588 if (accountExtra instanceof Account) {
589 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700590 } else if (accountExtra instanceof String) {
591 // This is the Account attached to the widget compose intent.
592 account = Account.newinstance((String)accountExtra);
593 if (account != null) {
594 return account;
595 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700596 }
mindyp5ee9dc42013-01-08 09:54:54 -0800597 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
598 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
599 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700600 }
601 if (account == null) {
mindyp06174462012-10-12 09:11:27 -0700602 MailAppProvider provider = MailAppProvider.getInstance();
603 String lastAccountUri = provider.getLastSentFromAccount();
604 if (TextUtils.isEmpty(lastAccountUri)) {
605 lastAccountUri = provider.getLastViewedAccount();
606 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700607 if (!TextUtils.isEmpty(lastAccountUri)) {
608 accountExtra = Uri.parse(lastAccountUri);
609 }
610 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700611 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700612 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
613 // For backwards compatibility, we need to check account
614 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700615 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700616 if (a.name.equals(accountExtra)) {
617 account = a;
618 }
619 }
620 } else if (accountExtra instanceof Uri) {
621 // The uri of the last viewed account is what is stored in
622 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700623 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700624 if (a.uri.equals(accountExtra)) {
625 account = a;
626 }
627 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700628 }
629 if (account == null) {
630 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700631 }
632 }
633 return account;
634 }
635
Alice Yanga990a712013-03-13 18:37:00 -0700636 private void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800637 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700638 // Don't bother with the intent if we have procured a message from the
639 // intent already.
640 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
641 initAttachmentsFromIntent(intent);
642 }
Alice Yanga990a712013-03-13 18:37:00 -0700643 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700644 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
645 action);
mindypd4a48662012-11-08 17:13:49 -0800646
647 // If this is a draft message, the draft account is whatever account was
648 // used to open the draft message in Compose.
649 if (mDraft != null) {
650 mDraftAccount = mReplyFromAccount;
651 }
652
Mindy Pereira75f66632012-01-11 11:42:02 -0800653 initChangeListeners();
Mindy Pereira326689d2012-05-17 10:14:14 -0700654 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700655 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800656
Andy Huang9f855d62013-05-30 17:15:03 -0700657 mRespondedInline = mInnerSavedState != null ?
658 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
mindyp1623f9b2012-11-21 12:41:16 -0800659 if (mRespondedInline) {
660 mQuotedTextView.setVisibility(View.GONE);
661 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700662 }
663
Scott Kennedyff8553f2013-04-05 20:57:44 -0700664 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700665 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
666 }
667
Mindy Pereira71c9e562012-05-17 11:01:02 -0700668 private void updateHideOrShowQuotedText(boolean showQuotedText) {
669 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700670 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700671 }
672
673 private void setFocus(int action) {
674 if (action == EDIT_DRAFT) {
675 int type = mDraft.draftType;
676 switch (type) {
677 case UIProvider.DraftType.COMPOSE:
678 case UIProvider.DraftType.FORWARD:
679 action = COMPOSE;
680 break;
681 case UIProvider.DraftType.REPLY:
682 case UIProvider.DraftType.REPLY_ALL:
683 default:
684 action = REPLY;
685 break;
686 }
687 }
688 switch (action) {
689 case FORWARD:
690 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800691 if (TextUtils.isEmpty(mTo.getText())) {
692 mTo.requestFocus();
693 break;
694 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700695 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700696 case REPLY:
697 case REPLY_ALL:
698 default:
699 focusBody();
700 break;
701 }
702 }
703
704 /**
705 * Focus the body of the message.
706 */
707 public void focusBody() {
708 mBodyView.requestFocus();
709 int length = mBodyView.getText().length();
710
711 int signatureStartPos = getSignatureStartPosition(
712 mSignature, mBodyView.getText().toString());
713 if (signatureStartPos > -1) {
714 // In case the user deleted the newlines...
715 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700716 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700717 // Move cursor to the end.
718 mBodyView.setSelection(length);
719 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800720 }
721
722 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700723 protected void onStart() {
724 super.onStart();
725
726 Analytics.getInstance().activityStart(this);
727 }
728
729 @Override
730 protected void onStop() {
731 super.onStop();
732
733 Analytics.getInstance().activityStop(this);
734 }
735
736 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800737 protected void onResume() {
738 super.onResume();
739 // Update the from spinner as other accounts
740 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800741 if (mFromSpinner != null && mAccount != null) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700742 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800743 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800744 }
745
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800746 @Override
747 protected void onPause() {
748 super.onPause();
749
Mindy Pereiraa2148332012-07-02 13:54:14 -0700750 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700751 // Don't save unnecessary drafts if we are only changing the orientation.
752 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700753 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700754
Andy Huange003b4c2013-08-16 10:32:05 -0700755 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700756 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
757 // because that method can be invoked many times in a single compose session.)
758 logSendOrSave(true /* save */);
759 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700760 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800761 }
762
763 @Override
764 protected final void onActivityResult(int request, int result, Intent data) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700765 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800766 addAttachmentAndUpdateView(data);
Mindy Pereirab199d172012-08-13 11:04:03 -0700767 mAddingAttachment = false;
768 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700769 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700770 if (result != RESULT_OK) {
771 finish();
772 } else {
773 // Watch for accounts to show up!
774 // restart the loader to get the updated list of accounts
775 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
776 showWaitFragment(null);
777 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800778 }
779 }
780
781 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700782 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700783 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
784 if (hasAccounts) {
785 clearChangeListeners();
786 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700787 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700788 if (mInnerSavedState != null) {
789 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
790 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
791 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700792 // There should be a focus and it should be an EditText since we
793 // only save these extras if these conditions are true.
794 EditText focusEditText = (EditText) getCurrentFocus();
795 final int length = focusEditText.getText().length();
796 if (selectionStart < length && selectionEnd < length) {
797 focusEditText.setSelection(selectionStart, selectionEnd);
798 }
799 }
800 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700801 if (hasAccounts) {
802 initChangeListeners();
803 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700804 }
805
806 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700807 protected final void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800808 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700809 final Bundle inner = new Bundle();
810 saveState(inner);
811 state.putBundle(KEY_INNER_SAVED_STATE, inner);
812 }
813
814 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700815 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
816 if (mAccounts == null || mAccounts.length == 0) {
817 return;
818 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700819 // The framework is happy to save and restore the selection but only if it also saves and
820 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
821 // this manually.
822 View focus = getCurrentFocus();
823 if (focus != null && focus instanceof EditText) {
824 EditText focusEditText = (EditText) focus;
825 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
826 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
827 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700828
829 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700830 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700831 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
832 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
833 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700834 if (selectedReplyFromAccount != null) {
835 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
836 .toString());
837 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
838 } else {
839 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
840 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800841
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700842 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
843 // We don't have a draft id, and we have a request id,
844 // save the request id.
845 state.putInt(EXTRA_REQUEST_ID, mRequestId);
846 }
847
848 // We want to restore the current mode after a pause
849 // or rotation.
850 int mode = getMode();
851 state.putInt(EXTRA_ACTION, mode);
852
mindype7b76aa2012-11-14 16:19:13 -0800853 final Message message = createMessage(selectedReplyFromAccount, mode);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700854 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -0800855 message.id = mDraft.id;
856 message.serverId = mDraft.serverId;
857 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700858 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700859 state.putParcelable(EXTRA_MESSAGE, message);
860
861 if (mRefMessage != null) {
862 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700863 } else if (message.appendRefMessageContent) {
864 // If we have no ref message but should be appending
865 // ref message content, we have orphaned quoted text. Save it.
866 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700867 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700868 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
869 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -0800870 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -0800871 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -0700872 state.putParcelableArrayList(
873 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -0700874
875 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700876 }
877
878 private int getMode() {
879 int mode = ComposeActivity.COMPOSE;
880 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700881 if (actionBar != null
882 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700883 mode = actionBar.getSelectedNavigationIndex();
884 }
885 return mode;
886 }
887
888 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
889 Message message = new Message();
890 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -0700891 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700892 message.uri = null;
893 message.conversationUri = null;
894 message.subject = mSubject.getText().toString();
895 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800896 message.setTo(formatSenders(mTo.getText().toString()));
897 message.setCc(formatSenders(mCc.getText().toString()));
898 message.setBcc(formatSenders(mBcc.getText().toString()));
899 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700900 message.dateReceivedMs = 0;
Paul Westbrookb4931c62013-01-14 17:51:18 -0800901 final String htmlBody = Html.toHtml(removeComposingSpans(mBodyView.getText()));
902 final StringBuilder fullBody = new StringBuilder(htmlBody);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700903 message.bodyHtml = fullBody.toString();
904 message.bodyText = mBodyView.getText().toString();
905 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -0700906 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700907 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
908 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
909 message.hasAttachments = attachments != null && attachments.size() > 0;
910 message.attachmentListUri = null;
911 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700912 message.alwaysShowImages = false;
913 message.attachmentsJson = Attachment.toJSONArray(attachments);
914 CharSequence quotedText = mQuotedTextView.getQuotedText();
915 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
916 .getQuotedTextOffset(quotedText.toString()) : -1;
917 message.accountUri = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800918 message.setFrom(selectedReplyFromAccount != null ? selectedReplyFromAccount.address
919 : mAccount != null ? mAccount.name : null);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700920 message.draftType = getDraftType(mode);
mindype7b76aa2012-11-14 16:19:13 -0800921 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700922 }
923
Scott Kennedyff8553f2013-04-05 20:57:44 -0700924 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -0700925 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
926 return string.substring(0, string.length() - 1);
927 }
928 return string;
929 }
930
Mindy Pereira818143e2012-01-11 13:59:49 -0800931 @VisibleForTesting
932 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700933 if (account == null) {
934 return;
935 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700936 if (!account.equals(mAccount)) {
937 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700938 mCachedSettings = mAccount.settings;
939 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700940 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700941 if (mAccount != null) {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800942 MailActivity.setNfcMessage(mAccount.name);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700943 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800944 }
945
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700946 private void initFromSpinner(Bundle bundle, int action) {
947 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700948 action = COMPOSE;
949 }
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700950 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
951
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700952 if (bundle != null) {
953 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
954 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
955 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
956 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700957 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700958 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
959 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700960 }
961 if (mReplyFromAccount == null) {
962 if (mDraft != null) {
963 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
964 } else if (mRefMessage != null) {
965 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
966 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700967 }
968 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -0700969 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700970 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700971
Mindy Pereira62de1b12012-04-06 12:17:56 -0700972 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700973
Mindy Pereira62de1b12012-04-06 12:17:56 -0700974 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700975 // If there is only 1 account, just show that account.
976 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700977 // send mail from / save drafts to.
978 mFromStatic.setVisibility(View.GONE);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700979 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700980 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700981 } else {
982 mFromStatic.setVisibility(View.VISIBLE);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700983 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700984 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700985 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800986 }
987
Mindy Pereira62de1b12012-04-06 12:17:56 -0700988 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
989 if (refMessage.accountUri != null) {
990 // This must be from combined inbox.
991 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
992 for (ReplyFromAccount from : replyFromAccounts) {
993 if (from.account.uri.equals(refMessage.accountUri)) {
994 return from;
995 }
996 }
997 return null;
998 } else {
999 return getReplyFromAccount(account, refMessage);
1000 }
1001 }
1002
1003 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001004 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001005 * return who the message should be sent from.
1006 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001007 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001008 * @return the address from which to reply.
1009 */
1010 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1011 // First see if we are supposed to use the default address or
1012 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001013 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001014 return getDefaultReplyFromAccount(account);
1015 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001016 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001017 // all the message recipients and find one that matches
1018 // a custom from or account.
1019 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001020 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1021 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001022 return getMatchingRecipient(account, allRecipients);
1023 }
1024 }
1025
1026 /**
1027 * Compare all the recipients of an email to the current account and all
1028 * custom addresses associated with that account. Return the match if there
1029 * is one, or the default account if there isn't.
1030 */
1031 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1032 // Tokenize the list and place in a hashmap.
1033 ReplyFromAccount matchingReplyFrom = null;
1034 Rfc822Token[] tokens;
1035 HashSet<String> recipientsMap = new HashSet<String>();
1036 for (String address : sentTo) {
1037 tokens = Rfc822Tokenizer.tokenize(address);
1038 for (int i = 0; i < tokens.length; i++) {
1039 recipientsMap.add(tokens[i].getAddress());
1040 }
1041 }
1042
1043 int matchingAddressCount = 0;
1044 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001045 customFroms = account.getReplyFroms();
1046 if (customFroms != null) {
1047 for (ReplyFromAccount entry : customFroms) {
1048 if (recipientsMap.contains(entry.address)) {
1049 matchingReplyFrom = entry;
1050 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001051 }
1052 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001053 }
1054 if (matchingAddressCount > 1) {
1055 matchingReplyFrom = getDefaultReplyFromAccount(account);
1056 }
1057 return matchingReplyFrom;
1058 }
1059
Scott Kennedyff8553f2013-04-05 20:57:44 -07001060 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1061 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001062 if (from.isDefault) {
1063 return from;
1064 }
1065 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -07001066 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
1067 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001068 }
1069
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001070 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001071 String sender = msg.getFrom();
Mindy Pereira62de1b12012-04-06 12:17:56 -07001072 ReplyFromAccount replyFromAccount = null;
1073 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1074 if (TextUtils.equals(account.name, sender)) {
1075 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -07001076 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001077 } else {
1078 for (ReplyFromAccount fromAccount : replyFromAccounts) {
1079 if (TextUtils.equals(fromAccount.name, sender)) {
1080 replyFromAccount = fromAccount;
1081 break;
1082 }
1083 }
1084 }
1085 return replyFromAccount;
1086 }
1087
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001088 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001089 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001090 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001091 if (mCcBccButton != null) {
1092 mCcBccButton.setOnClickListener(this);
1093 }
1094 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001095 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
mindyp93b079b2012-08-29 16:32:15 -07001096 mPhotoAttachmentsButton = findViewById(R.id.add_photo_attachment);
mindypcd0b0b92012-08-23 14:33:17 -07001097 if (mPhotoAttachmentsButton != null) {
1098 mPhotoAttachmentsButton.setOnClickListener(this);
1099 }
mindyp93b079b2012-08-29 16:32:15 -07001100 mVideoAttachmentsButton = findViewById(R.id.add_video_attachment);
mindypcd0b0b92012-08-23 14:33:17 -07001101 if (mVideoAttachmentsButton != null) {
1102 mVideoAttachmentsButton.setOnClickListener(this);
1103 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001104 mTo = (RecipientEditTextView) findViewById(R.id.to);
Scott Kennedy41500392013-04-24 18:46:36 -07001105 mTo.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001106 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Scott Kennedy41500392013-04-24 18:46:36 -07001107 mCc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001108 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Scott Kennedy41500392013-04-24 18:46:36 -07001109 mBcc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001110 // TODO: add special chips text change watchers before adding
1111 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001112 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001113 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001114 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1115 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001116 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001117 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001118 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001119 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001120 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001121 }
1122
mindyp62d3ec72012-08-24 13:04:09 -07001123 @Override
1124 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1125 if (action == EditorInfo.IME_ACTION_DONE) {
1126 focusBody();
1127 return true;
1128 }
1129 return false;
1130 }
1131
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001132 protected TextView getBody() {
1133 return mBodyView;
1134 }
1135
1136 @VisibleForTesting
1137 public Account getFromAccount() {
1138 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1139 mReplyFromAccount.account : mAccount;
1140 }
1141
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001142 private void clearChangeListeners() {
1143 mSubject.removeTextChangedListener(this);
1144 mBodyView.removeTextChangedListener(this);
1145 mTo.removeTextChangedListener(mToListener);
1146 mCc.removeTextChangedListener(mCcListener);
1147 mBcc.removeTextChangedListener(mBccListener);
1148 mFromSpinner.setOnAccountChangedListener(null);
1149 mAttachmentsView.setAttachmentChangesListener(null);
1150 }
1151
Mindy Pereira75f66632012-01-11 11:42:02 -08001152 // Now that the message has been initialized from any existing draft or
1153 // ref message data, set up listeners for any changes that occur to the
1154 // message.
1155 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001156 // Make sure we only add text changed listeners once!
1157 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001158 mSubject.addTextChangedListener(this);
1159 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001160 if (mToListener == null) {
1161 mToListener = new RecipientTextWatcher(mTo, this);
1162 }
1163 mTo.addTextChangedListener(mToListener);
1164 if (mCcListener == null) {
1165 mCcListener = new RecipientTextWatcher(mCc, this);
1166 }
1167 mCc.addTextChangedListener(mCcListener);
1168 if (mBccListener == null) {
1169 mBccListener = new RecipientTextWatcher(mBcc, this);
1170 }
1171 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001172 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001173 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001174 }
1175
Alice Yanga990a712013-03-13 18:37:00 -07001176 private void initActionBar() {
1177 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001178 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001179 if (actionBar == null) {
1180 return;
1181 }
Alice Yanga990a712013-03-13 18:37:00 -07001182 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001183 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1184 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001185 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001186 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001187 if (mComposeModeAdapter == null) {
1188 mComposeModeAdapter = new ComposeModeAdapter(this);
1189 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001190 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1191 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001192 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001193 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001194 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001195 break;
1196 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001197 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001198 break;
1199 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001200 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001201 break;
1202 }
1203 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001204 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1205 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1206 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001207 }
1208
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001209 private void initFromRefMessage(int action) {
1210 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001211
1212 // Check if To: address and email body needs to be prefilled based on extras.
1213 // This is used for reporting rendering feedback.
1214 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1215 Intent intent = getIntent();
1216 if (intent.getExtras() != null) {
1217 String toAddresses = intent.getStringExtra(EXTRA_TO);
1218 if (toAddresses != null) {
1219 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1220 }
1221 String body = intent.getStringExtra(EXTRA_BODY);
1222 if (body != null) {
1223 setBody(body, false /* withSignature */);
1224 }
1225 }
1226 }
1227
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001228 if (mRefMessage != null) {
1229 // CC field only gets populated when doing REPLY_ALL.
1230 // BCC never gets auto-populated, unless the user is editing
1231 // a draft with one.
Mindy Pereira29a717e2012-07-25 18:05:48 -07001232 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001233 mCcBccView.show(false, true, false);
1234 }
1235 }
1236 updateHideOrShowCcBcc();
1237 }
1238
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001239 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001240 setSubject(mRefMessage, action);
1241 // Setup recipients
1242 if (action == FORWARD) {
1243 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001244 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001245 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001246 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001247 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1248 initAttachments(mRefMessage);
1249 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001250 }
1251
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001252 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001253 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001254
1255 mDraft = message;
1256 mDraftId = message.id;
1257 mSubject.setText(message.subject);
1258 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001259 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001260 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001261 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1262 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001263 if (message.hasAttachments) {
1264 List<Attachment> attachments = message.getAttachments();
1265 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001266 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001267 }
1268 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -07001269 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -07001270 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001271 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001272 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001273 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001274 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001275 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001276 // Find the offset in the htmltext of the actual quoted text and strip it out.
1277 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1278 if (quotedTextIndex > -1) {
mindypc59dd822012-11-13 10:56:21 -08001279 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1280 quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001281 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1282 message.bodyHtml.length());
1283 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001284 } else {
mindypc59dd822012-11-13 10:56:21 -08001285 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001286 }
1287 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001288 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001289 final String body = message.bodyText;
1290 final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1291 (quotedTextIndex > -1 ?
1292 message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1293 : "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001294 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001295 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1296 : null;
Mindy Pereira002ff522012-05-30 10:31:26 -07001297 }
1298 mBodyView.setText(bodyText);
1299 }
1300 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001301 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001302 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001303 }
1304
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001305 /**
1306 * Fill all the widgets with the content found in the Intent Extra, if any.
1307 * Also apply the same style to all widgets. Note: if initFromExtras is
1308 * called as a result of switching between reply, reply all, and forward per
1309 * the latest revision of Gmail, and the user has already made changes to
1310 * attachments on a previous incarnation of the message (as a reply, reply
1311 * all, or forward), the original attachments from the message will not be
1312 * re-instantiated. The user's changes will be respected. This follows the
1313 * web gmail interaction.
1314 */
1315 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001316 // If we were invoked with a SENDTO intent, the value
1317 // should take precedence
1318 final Uri dataUri = intent.getData();
1319 if (dataUri != null) {
1320 if (MAIL_TO.equals(dataUri.getScheme())) {
1321 initFromMailTo(dataUri.toString());
1322 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001323 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001324 String toText = dataUri.getSchemeSpecificPart();
1325 if (toText != null) {
1326 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001327 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001328 }
1329 }
1330 }
1331 }
1332
1333 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1334 if (extraStrings != null) {
1335 addToAddresses(Arrays.asList(extraStrings));
1336 }
1337 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1338 if (extraStrings != null) {
1339 addCcAddresses(Arrays.asList(extraStrings), null);
1340 }
1341 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1342 if (extraStrings != null) {
1343 addBccAddresses(Arrays.asList(extraStrings));
1344 }
1345
1346 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1347 if (extraString != null) {
1348 mSubject.setText(extraString);
1349 }
1350
Scott Kennedy60847252013-08-15 15:55:42 -07001351 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1352 if (mExtraValues != null) {
1353 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1354 }
1355
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001356 for (String extra : ALL_EXTRAS) {
1357 if (intent.hasExtra(extra)) {
1358 String value = intent.getStringExtra(extra);
1359 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001360 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001361 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001362 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001363 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001364 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001365 } else if (EXTRA_SUBJECT.equals(extra)) {
1366 mSubject.setText(value);
1367 } else if (EXTRA_BODY.equals(extra)) {
1368 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001369 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1370 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001371 }
1372 }
1373 }
1374
1375 Bundle extras = intent.getExtras();
1376 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001377 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1378 if (text != null) {
1379 setBody(text, true /* with signature */);
1380 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001381
1382 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001383 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001384 }
1385
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001386
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001387 @VisibleForTesting
1388 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001389 // TODO: handle the case where there are spaces in the display name as
1390 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1391 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001392 // Since URLDecode.decode changes + into ' ', and + is a valid
1393 // email character, we need to find/ replace these ourselves before
1394 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001395 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001396 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001397 } catch (IllegalArgumentException e) {
1398 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1399 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1400 } else {
1401 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1402 }
1403 return null;
1404 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001405 }
1406
1407 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001408 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1409 * changing '+' into ' '
1410 *
1411 * @param toReplace Input string
1412 * @return The string with all "+" characters replaced with "%2B"
1413 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001414 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001415 return toReplace.replace("+", "%2B");
1416 }
1417
1418 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001419 * Initialize the compose view from a String representing a mailTo uri.
1420 * @param mailToString The uri as a string.
1421 */
1422 public void initFromMailTo(String mailToString) {
1423 // We need to disguise this string as a URI in order to parse it
1424 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1425 Uri uri = Uri.parse("foo://" + mailToString);
1426 int index = mailToString.indexOf("?");
1427 int length = "mailto".length() + 1;
1428 String to;
1429 try {
1430 // Extract the recipient after mailto:
1431 if (index == -1) {
1432 to = decodeEmailInUri(mailToString.substring(length));
1433 } else {
1434 to = decodeEmailInUri(mailToString.substring(length, index));
1435 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001436 if (!TextUtils.isEmpty(to)) {
1437 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1438 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001439 } catch (UnsupportedEncodingException e) {
1440 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1441 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1442 } else {
1443 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1444 }
1445 }
1446
1447 List<String> cc = uri.getQueryParameters("cc");
1448 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1449
1450 List<String> otherTo = uri.getQueryParameters("to");
1451 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1452
1453 List<String> bcc = uri.getQueryParameters("bcc");
1454 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1455
1456 List<String> subject = uri.getQueryParameters("subject");
1457 if (subject.size() > 0) {
1458 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001459 mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
1460 UTF8_ENCODING_NAME));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001461 } catch (UnsupportedEncodingException e) {
1462 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1463 e.getMessage(), subject);
1464 }
1465 }
1466
1467 List<String> body = uri.getQueryParameters("body");
1468 if (body.size() > 0) {
1469 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001470 setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001471 true /* with signature */);
1472 } catch (UnsupportedEncodingException e) {
1473 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1474 }
1475 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001476 }
1477
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001478 @VisibleForTesting
1479 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001480 addAttachments(refMessage.getAttachments());
1481 }
1482
1483 public long addAttachments(List<Attachment> attachments) {
1484 long size = 0;
1485 AttachmentFailureException error = null;
1486 for (Attachment a : attachments) {
1487 try {
1488 size += mAttachmentsView.addAttachment(mAccount, a);
1489 } catch (AttachmentFailureException e) {
1490 error = e;
1491 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001492 }
Mark Wei434f2942012-08-24 11:54:02 -07001493 if (error != null) {
1494 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1495 if (attachments.size() > 1) {
1496 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1497 } else {
1498 showAttachmentTooBigToast(error.getErrorRes());
1499 }
1500 }
1501 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001502 }
1503
1504 /**
1505 * When an attachment is too large to be added to a message, show a toast.
1506 * This method also updates the position of the toast so that it is shown
1507 * clearly above they keyboard if it happens to be open.
1508 */
Mark Wei434f2942012-08-24 11:54:02 -07001509 private void showAttachmentTooBigToast(int errorRes) {
1510 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1511 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1512 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001513 }
1514
Mark Wei434f2942012-08-24 11:54:02 -07001515 private void showErrorToast(String message) {
1516 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1517 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001518 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1519 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1520 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001521 }
1522
Paul Westbrookf97588b2012-03-20 11:11:37 -07001523 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001524 Bundle extras = intent.getExtras();
1525 if (extras == null) {
1526 extras = Bundle.EMPTY;
1527 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001528 final String action = intent.getAction();
1529 if (!mAttachmentsChanged) {
1530 long totalSize = 0;
1531 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1532 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1533 for (String uriString : uris) {
1534 final Uri uri = Uri.parse(uriString);
1535 long size = 0;
1536 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001537 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1538 size = mAttachmentsView.addAttachment(mAccount, a);
1539
1540 Analytics.getInstance().sendEvent("send_intent_attachment",
1541 Utils.normalizeMimeType(a.getContentType()), null, size);
1542
Paul Westbrookf97588b2012-03-20 11:11:37 -07001543 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001544 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001545 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001546 }
1547 totalSize += size;
1548 }
1549 }
mindyp9a9e8d62012-10-03 12:24:07 -07001550 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1551 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1552 ArrayList<Parcelable> uris = extras
1553 .getParcelableArrayList(Intent.EXTRA_STREAM);
1554 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1555 for (Parcelable uri : uris) {
1556 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001557 final Attachment a = mAttachmentsView.generateLocalAttachment(
1558 (Uri) uri);
1559 attachments.add(a);
1560
1561 Analytics.getInstance().sendEvent("send_intent_attachment",
1562 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1563
mindyp9a9e8d62012-10-03 12:24:07 -07001564 } catch (AttachmentFailureException e) {
1565 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1566 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1567 getApplicationContext(),
1568 mAccount.settings.getMaxAttachmentSize());
1569 showErrorToast(getString
1570 (R.string.generic_attachment_problem, maxSize));
1571 }
1572 }
1573 totalSize += addAttachments(attachments);
1574 } else {
1575 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1576 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001577 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001578 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1579 size = mAttachmentsView.addAttachment(mAccount, a);
1580
1581 Analytics.getInstance().sendEvent("send_intent_attachment",
1582 Utils.normalizeMimeType(a.getContentType()), null, size);
1583
Paul Westbrookf97588b2012-03-20 11:11:37 -07001584 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001585 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001586 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001587 }
mindyp9a9e8d62012-10-03 12:24:07 -07001588 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001589 }
1590 }
1591
1592 if (totalSize > 0) {
1593 mAttachmentsChanged = true;
1594 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001595
1596 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1597 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001598 }
1599 }
1600 }
1601
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001602 private void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
1603 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1604 mShowQuotedText = true;
1605 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001606
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001607 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1608 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001609 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1610 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001611 }
1612
1613 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001614 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001615 boolean ccVisible = mCcBccView.isCcVisible();
1616 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001617 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001618 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001619 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001620 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001621 : R.string.add_bcc_label));
1622 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001623 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001624 }
1625 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001626 }
1627
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001628 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001629 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1630 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1631 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1632 if (showCc || showBcc) {
1633 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001634 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001635 }
1636 }
1637
Mindy Pereira013194c2012-01-06 15:09:33 -08001638 /**
1639 * Add attachment and update the compose area appropriately.
1640 * @param data
1641 */
1642 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001643 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1644 }
1645
Andy Huang5c5fd572012-04-08 18:19:29 -07001646 public void addAttachmentAndUpdateView(Uri contentUri) {
1647 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001648 return;
1649 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001650 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001651 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1652 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001653 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001654 showErrorToast(getResources().getString(
1655 e.getErrorRes(),
1656 AttachmentUtils.convertToHumanReadableSize(
1657 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001658 }
1659 }
1660
1661 public void addAttachmentAndUpdateView(Attachment attachment) {
1662 try {
Mark Wei434f2942012-08-24 11:54:02 -07001663 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001664 if (size > 0) {
1665 mAttachmentsChanged = true;
1666 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001667 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001668 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001669 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001670 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001671 }
1672 }
1673
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001674 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001675 // Don't populate the address if this is a forward.
1676 if (action == ComposeActivity.FORWARD) {
1677 return;
1678 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001679 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001680 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001681
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001682 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1683 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001684 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001685 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001686 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001687 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001688 final String[] replyToAddresses = refMessage.getReplyToAddressesUnescaped();
1689 String replyToAddress = replyToAddresses.length > 0 ? replyToAddresses[0] : null;
1690 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1691 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
1692
mindyp65b06f52012-11-21 10:35:08 -08001693 // If there is no reply to address, the reply to address is the sender.
Tony Mantler89de9eb2013-07-25 11:43:58 -07001694 if (TextUtils.isEmpty(replyToAddress)) {
1695 replyToAddress = fromAddress;
mindyp65b06f52012-11-21 10:35:08 -08001696 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001697
1698 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1699 // Cc list is the union of the To and Cc recipients of the original
1700 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001701 // already on the To list.
1702 if (action == ComposeActivity.REPLY) {
Tony Mantler89de9eb2013-07-25 11:43:58 -07001703 toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001704 addToAddresses(toAddresses);
1705 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001706 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler89de9eb2013-07-25 11:43:58 -07001707 toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001708 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001709 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001710 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001711 addCcAddresses(ccAddresses, toAddresses);
1712 }
1713 }
1714
1715 private void addToAddresses(Collection<String> addresses) {
1716 addAddressesToList(addresses, mTo);
1717 }
1718
1719 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001720 addCcAddressesToList(tokenizeAddressList(addresses),
1721 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001722 }
1723
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001724 private void addBccAddresses(Collection<String> addresses) {
1725 addAddressesToList(addresses, mBcc);
1726 }
1727
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001728 @VisibleForTesting
1729 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1730 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1731 String address;
1732
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001733 if (compareToList == null) {
1734 for (Rfc822Token[] tokens : addresses) {
1735 for (int i = 0; i < tokens.length; i++) {
1736 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001737 list.append(address + END_TOKEN);
1738 }
1739 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001740 } else {
1741 HashSet<String> compareTo = convertToHashSet(compareToList);
1742 for (Rfc822Token[] tokens : addresses) {
1743 for (int i = 0; i < tokens.length; i++) {
1744 address = tokens[i].toString();
1745 // Check if this is a duplicate:
1746 if (!compareTo.contains(tokens[i].getAddress())) {
1747 // Get the address here
1748 list.append(address + END_TOKEN);
1749 }
1750 }
1751 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001752 }
1753 }
1754
Scott Kennedyff8553f2013-04-05 20:57:44 -07001755 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
1756 final HashSet<String> hash = new HashSet<String>();
1757 for (final Rfc822Token[] tokens : list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001758 for (int i = 0; i < tokens.length; i++) {
1759 hash.add(tokens[i].getAddress());
1760 }
1761 }
1762 return hash;
1763 }
1764
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001765 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1766 @VisibleForTesting
1767 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1768
1769 for (String address: addresses) {
1770 tokenized.add(Rfc822Tokenizer.tokenize(address));
1771 }
1772 return tokenized;
1773 }
1774
1775 @VisibleForTesting
1776 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1777 for (String address : addresses) {
1778 addAddressToList(address, list);
1779 }
1780 }
1781
Scott Kennedyff8553f2013-04-05 20:57:44 -07001782 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001783 if (address == null || list == null)
1784 return;
1785
Scott Kennedyff8553f2013-04-05 20:57:44 -07001786 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001787
1788 for (int i = 0; i < tokens.length; i++) {
1789 list.append(tokens[i] + END_TOKEN);
1790 }
1791 }
1792
1793 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001794 protected Collection<String> initToRecipients(final String fullSenderAddress,
1795 final String replyToAddress, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001796 // The To recipient is the reply-to address specified in the original
1797 // message, unless it is:
1798 // the current user OR a custom from of the current user, in which case
1799 // it's the To recipient list of the original message.
1800 // OR missing, in which case use the sender of the original message
1801 Set<String> toAddresses = Sets.newHashSet();
mindyp65b06f52012-11-21 10:35:08 -08001802 if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001803 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001804 } else {
mindyp65b06f52012-11-21 10:35:08 -08001805 // In this case, the user is replying to a message in which their
1806 // current account or one of their custom from addresses is the only
1807 // recipient and they sent the original message.
1808 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
1809 && recipientMatchesThisAccount(inToAddresses[0])) {
1810 toAddresses.add(inToAddresses[0]);
1811 return toAddresses;
1812 }
1813 // This happens if the user replies to a message they originally
1814 // wrote. In this case, "reply" really means "re-send," so we
1815 // target the original recipients. This works as expected even
1816 // if the user sent the original message to themselves.
1817 for (String address : inToAddresses) {
1818 if (!recipientMatchesThisAccount(address)) {
1819 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08001820 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001821 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001822 }
1823 return toAddresses;
1824 }
1825
Scott Kennedyff8553f2013-04-05 20:57:44 -07001826 private void addRecipients(final Set<String> recipients, final String[] addresses) {
1827 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001828 // Do not add this account, or any of its custom from addresses, to
1829 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001830 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08001831 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001832 recipients.add(email.replace("\"\"", ""));
1833 }
1834 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001835 }
1836
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001837 /**
1838 * A recipient matches this account if it has the same address as the
1839 * currently selected account OR one of the custom from addresses associated
1840 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001841 * @param recipientAddress address we are comparing with the currently selected account
1842 * @return
1843 */
mindyp5ee5d692012-11-19 16:02:16 -08001844 protected boolean recipientMatchesThisAccount(String recipientAddress) {
1845 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08001846 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001847 }
1848
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001849 /**
1850 * Returns a formatted subject string with the appropriate prefix for the action type.
1851 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
1852 */
1853 public static String buildFormattedSubject(Resources res, String subject, int action) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001854 String prefix;
1855 String correctedSubject = null;
1856 if (action == ComposeActivity.COMPOSE) {
1857 prefix = "";
1858 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001859 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001860 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001861 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001862 }
1863
1864 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07001865 if (!TextUtils.isEmpty(subject)
1866 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001867 correctedSubject = subject;
1868 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001869 correctedSubject = String.format(
1870 res.getString(R.string.formatted_subject), prefix, subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001871 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001872
1873 return correctedSubject;
1874 }
1875
1876 private void setSubject(Message refMessage, int action) {
1877 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001878 }
1879
Mindy Pereira818143e2012-01-11 13:59:49 -08001880 private void initRecipients() {
1881 setupRecipients(mTo);
1882 setupRecipients(mCc);
1883 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001884 }
1885
Mindy Pereira818143e2012-01-11 13:59:49 -08001886 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001887 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001888 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001889 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001890 int offset = accountName.indexOf("@") + 1;
1891 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001892 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001893 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001894 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001895 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001896 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001897 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001898 }
1899
1900 @Override
1901 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001902 final int id = v.getId();
1903 if (id == R.id.add_cc_bcc) {
1904 // Verify that cc/ bcc aren't showing.
1905 // Animate in cc/bcc.
1906 showCcBccViews();
1907 } else if (id == R.id.add_photo_attachment) {
1908 doAttach(MIME_TYPE_PHOTO);
1909 } else if (id == R.id.add_video_attachment) {
1910 doAttach(MIME_TYPE_VIDEO);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001911 }
1912 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001913
1914 @Override
1915 public boolean onCreateOptionsMenu(Menu menu) {
1916 super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07001917 // Don't render any menu items when there are no accounts.
1918 if (mAccounts == null || mAccounts.length == 0) {
1919 return true;
1920 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001921 MenuInflater inflater = getMenuInflater();
1922 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08001923
1924 /*
1925 * Start save in the correct enabled state.
1926 * 1) If a user launches compose from within gmail, save is disabled
1927 * until they add something, at which point, save is enabled, auto save
1928 * on exit; if the user empties everything, save is disabled, exiting does not
1929 * auto-save
1930 * 2) if a user replies/ reply all/ forwards from within gmail, save is
1931 * disabled until they change something, at which point, save is
1932 * enabled, auto save on exit; if the user empties everything, save is
1933 * disabled, exiting does not auto-save.
1934 * 3) If a user launches compose from another application and something
1935 * gets populated (attachments, recipients, body, subject, etc), save is
1936 * enabled, auto save on exit; if the user empties everything, save is
1937 * disabled, exiting does not auto-save
1938 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08001939 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08001940 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07001941 enableSave(mInnerSavedState != null ?
1942 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08001943 : (Intent.ACTION_SEND.equals(action)
1944 || Intent.ACTION_SEND_MULTIPLE.equals(action)
1945 || Intent.ACTION_SENDTO.equals(action)
1946 || shouldSave()));
1947
Mindy Pereira82cc5662012-01-09 17:29:30 -08001948 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001949 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1950 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1951 if (helpItem != null) {
1952 helpItem.setVisible(mAccount != null
1953 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1954 }
1955 if (sendFeedbackItem != null) {
1956 sendFeedbackItem.setVisible(mAccount != null
1957 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1958 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001959 return true;
1960 }
1961
1962 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001963 public boolean onPrepareOptionsMenu(Menu menu) {
1964 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001965 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001966 // Its possible there is a menu item OR a button.
1967 boolean ccFieldVisible = mCc.isShown();
1968 boolean bccFieldVisible = mBcc.isShown();
1969 if (!ccFieldVisible || !bccFieldVisible) {
1970 ccBcc.setVisible(true);
1971 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1972 : R.string.add_bcc_label));
1973 } else {
1974 ccBcc.setVisible(false);
1975 }
1976 }
1977 return true;
1978 }
1979
1980 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001981 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001982 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07001983
1984 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id, null, 0);
1985
Mindy Pereira75f66632012-01-11 11:42:02 -08001986 boolean handled = true;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001987 if (id == R.id.add_photo_attachment) {
1988 doAttach(MIME_TYPE_PHOTO);
1989 } else if (id == R.id.add_video_attachment) {
1990 doAttach(MIME_TYPE_VIDEO);
1991 } else if (id == R.id.add_cc_bcc) {
1992 showCcBccViews();
1993 } else if (id == R.id.save) {
1994 doSave(true);
1995 } else if (id == R.id.send) {
1996 doSend();
1997 } else if (id == R.id.discard) {
1998 doDiscard();
1999 } else if (id == R.id.settings) {
2000 Utils.showSettings(this, mAccount);
2001 } else if (id == android.R.id.home) {
2002 onAppUpPressed();
2003 } else if (id == R.id.help_info_menu_item) {
2004 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2005 } else if (id == R.id.feedback_menu_item) {
2006 Utils.sendFeedback(this, mAccount, false);
2007 } else {
2008 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002009 }
2010 return !handled ? super.onOptionsItemSelected(item) : handled;
2011 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002012
Mindy Pereirab199d172012-08-13 11:04:03 -07002013 @Override
2014 public void onBackPressed() {
2015 // If we are showing the wait fragment, just exit.
2016 if (getWaitFragment() != null) {
2017 finish();
2018 } else {
2019 super.onBackPressed();
2020 }
2021 }
2022
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002023 /**
2024 * Carries out the "up" action in the action bar.
2025 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002026 private void onAppUpPressed() {
2027 if (mLaunchedFromEmail) {
2028 // If this was started from Gmail, simply treat app up as the system back button, so
2029 // that the last view is restored.
2030 onBackPressed();
2031 return;
2032 }
2033
2034 // Fire the main activity to ensure it launches the "top" screen of mail.
2035 // Since the main Activity is singleTask, it should revive that task if it was already
2036 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002037 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002038 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2039 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2040 startActivity(mailIntent);
2041 finish();
2042 }
2043
Mindy Pereira33fe9082012-01-09 16:24:30 -08002044 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002045 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002046 logSendOrSave(false /* save */);
2047 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002048 }
2049
Mindy Pereira48e31b02012-05-30 13:12:24 -07002050 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002051 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002052 }
2053
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002054 @VisibleForTesting
2055 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002056 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002057 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2058 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002059 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2060 }
2061
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002062 @VisibleForTesting
2063 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002064 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002065 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002066 public final SendOrSaveCallback mSendOrSaveCallback;
2067 @VisibleForTesting
2068 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002069 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002070
2071 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002072 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002073 mContext = context;
2074 mSendOrSaveCallback = callback;
2075 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002076 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002077 }
2078
2079 @Override
2080 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002081 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002082
Mindy Pereira92551d02012-04-05 11:31:12 -07002083 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002084 Message message = mSendOrSaveCallback.getMessage();
2085 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002086 // If a previous draft has been saved, in an account that is different
2087 // than what the user wants to send from, remove the old draft, and treat this
2088 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002089 if (mExistingDraftAccount != null
2090 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002091 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2092 ContentResolver resolver = mContext.getContentResolver();
2093 ContentValues values = new ContentValues();
2094 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002095 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2096 new ContentProviderTask.UpdateTask()
2097 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2098 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002099 } else {
2100 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002101 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002102 // reset messageId to 0, so a new message will be created
2103 messageId = UIProvider.INVALID_MESSAGE_ID;
2104 }
2105 }
2106
2107 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002108 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002109
2110 if (!sendOrSaveMessage.mSave) {
Tony Mantler9f324232013-08-08 14:24:30 -07002111 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002112 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Tony Mantler9f324232013-08-08 14:24:30 -07002113 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002114 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Tony Mantler9f324232013-08-08 14:24:30 -07002115 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002116 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2117 }
2118 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2119 }
2120
Tony Mantler9f324232013-08-08 14:24:30 -07002121 private static void incrementRecipientsTimesContacted(final Context context,
2122 final String addressString) {
2123 if (TextUtils.isEmpty(addressString)) {
2124 return;
2125 }
2126 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2127 final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
2128 for (int i = 0; i < tokens.length;i++) {
2129 recipients.add(tokens[i].getAddress());
2130 }
2131 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
2132 statsUpdater.updateWithAddress(recipients);
2133 }
2134
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002135 /**
2136 * Send or Save a message.
2137 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002138 private void sendOrSaveMessage(final long messageIdToSave,
2139 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002140 final ContentResolver resolver = mContext.getContentResolver();
2141 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2142
2143 final String accountMethod = sendOrSaveMessage.mSave ?
2144 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2145 UIProvider.AccountCallMethods.SEND_MESSAGE;
2146
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002147 try {
2148 if (updateExistingMessage) {
2149 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002150
Paul Westbrook013a23c2013-02-22 10:37:41 -08002151 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002152 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002153 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002154 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002155 final Bundle result = callAccountSendSaveMethod(resolver,
2156 selectedAccount.account, accountMethod, sendOrSaveMessage);
2157 if (result != null) {
2158 // If a non-null value was returned, then the provider handled the call
2159 // method
2160 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002161 }
2162 if (sendOrSaveMessage.mSave && messageUri != null) {
2163 final Cursor messageCursor = resolver.query(messageUri,
2164 UIProvider.MESSAGE_PROJECTION, null, null, null);
2165 if (messageCursor != null) {
2166 try {
2167 if (messageCursor.moveToFirst()) {
2168 // Broadcast notification that a new message has
2169 // been allocated
2170 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2171 new Message(messageCursor));
2172 }
2173 } finally {
2174 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002175 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002176 }
2177 }
2178 }
2179 } finally {
2180 // Close any opened file descriptors
2181 closeOpenedAttachmentFds(sendOrSaveMessage);
2182 }
2183 }
2184
Scott Kennedyff8553f2013-04-05 20:57:44 -07002185 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002186 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2187 if (openedFds != null) {
2188 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002189 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002190 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2191 if (fd != null) {
2192 try {
2193 fd.close();
2194 } catch (IOException e) {
2195 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002196 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002197 }
2198 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002199 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002200 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002201
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002202 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002203 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002204 *
2205 * If this was successful, this method will return an non-null Bundle instance
2206 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002207 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2208 final Account account, final String method,
2209 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002210 // Copy all of the values from the content values to the bundle
2211 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2212 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2213
2214 for (Entry<String, Object> entry : valueSet) {
2215 final Object entryValue = entry.getValue();
2216 final String key = entry.getKey();
2217 if (entryValue instanceof String) {
2218 methodExtras.putString(key, (String)entryValue);
2219 } else if (entryValue instanceof Boolean) {
2220 methodExtras.putBoolean(key, (Boolean)entryValue);
2221 } else if (entryValue instanceof Integer) {
2222 methodExtras.putInt(key, (Integer)entryValue);
2223 } else if (entryValue instanceof Long) {
2224 methodExtras.putLong(key, (Long)entryValue);
2225 } else {
2226 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2227 entryValue.getClass().getName());
2228 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002229 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002230
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002231 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2232 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2233 if (fdMap != null) {
2234 methodExtras.putParcelable(
2235 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2236 }
2237
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002238 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002239 }
2240 }
2241
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002242 @VisibleForTesting
2243 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002244 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002245 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002246 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002247 @VisibleForTesting
2248 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002249 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002250 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002251
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002252 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2253 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002254 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002255 mValues = values;
2256 mRefMessageId = refMessageId;
2257 mSave = save;
2258 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002259
2260 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002261 }
2262
2263 int requestId() {
2264 return mRequestId;
2265 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002266
2267 Bundle attachmentFds() {
2268 return mAttachmentFds;
2269 }
2270
2271 /**
2272 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2273 * called before the ComposeActivity finishes.
2274 * Note: The caller is responsible for closing these file descriptors.
2275 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002276 private static Bundle initializeAttachmentFds(final Context context,
2277 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002278 if (attachments == null || attachments.size() == 0) {
2279 return null;
2280 }
2281
2282 final Bundle result = new Bundle(attachments.size());
2283 final ContentResolver resolver = context.getContentResolver();
2284
2285 for (Attachment attachment : attachments) {
2286 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2287 continue;
2288 }
2289
2290 ParcelFileDescriptor fileDescriptor;
2291 try {
2292 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2293 } catch (FileNotFoundException e) {
2294 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2295 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002296 } catch (SecurityException e) {
2297 // We have encountered a security exception when attempting to open the file
2298 // specified by the content uri. If the attachment has been cached, this
2299 // isn't a problem, as even through the original permission may have been
2300 // revoked, we have cached the file. This will happen when saving/sending
2301 // a previously saved draft.
2302 // TODO(markwei): Expose whether the attachment has been cached through the
2303 // attachment object. This would allow us to limit when the log is made, as
2304 // if the attachment has been cached, this really isn't an error
2305 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2306 // Just set the file descriptor to null, as the underlying provider needs
2307 // to handle the file descriptor not being set.
2308 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002309 }
2310
2311 if (fileDescriptor != null) {
2312 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2313 }
2314 }
2315
2316 return result;
2317 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002318 }
2319
2320 /**
2321 * Get the to recipients.
2322 */
2323 public String[] getToAddresses() {
2324 return getAddressesFromList(mTo);
2325 }
2326
2327 /**
2328 * Get the cc recipients.
2329 */
2330 public String[] getCcAddresses() {
2331 return getAddressesFromList(mCc);
2332 }
2333
2334 /**
2335 * Get the bcc recipients.
2336 */
2337 public String[] getBccAddresses() {
2338 return getAddressesFromList(mBcc);
2339 }
2340
2341 public String[] getAddressesFromList(RecipientEditTextView list) {
2342 if (list == null) {
2343 return new String[0];
2344 }
2345 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2346 int count = tokens.length;
2347 String[] result = new String[count];
2348 for (int i = 0; i < count; i++) {
2349 result[i] = tokens[i].toString();
2350 }
2351 return result;
2352 }
2353
2354 /**
2355 * Check for invalid email addresses.
2356 * @param to String array of email addresses to check.
2357 * @param wrongEmailsOut Emails addresses that were invalid.
2358 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002359 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002360 if (mValidator == null) {
2361 return;
2362 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002363 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002364 if (!mValidator.isValid(email)) {
2365 wrongEmailsOut.add(email);
2366 }
2367 }
2368 }
2369
Tony Mantler2558b502013-07-09 10:53:34 -07002370 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002371 // Public no-args constructor needed for fragment re-instantiation
2372 public RecipientErrorDialogFragment() {}
2373
Tony Mantler2558b502013-07-09 10:53:34 -07002374 public static RecipientErrorDialogFragment newInstance(final String message) {
2375 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2376 final Bundle args = new Bundle(1);
2377 args.putString("message", message);
2378 frag.setArguments(args);
2379 return frag;
2380 }
2381
2382 @Override
2383 public Dialog onCreateDialog(Bundle savedInstanceState) {
2384 final String message = getArguments().getString("message");
2385 return new AlertDialog.Builder(getActivity()).setMessage(message).setTitle(
2386 R.string.recipient_error_dialog_title)
2387 .setIconAttribute(android.R.attr.alertDialogIcon)
2388 .setPositiveButton(
2389 R.string.ok, new Dialog.OnClickListener() {
2390 @Override
2391 public void onClick(DialogInterface dialog, int which) {
2392 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2393 }
2394 }).create();
2395 }
2396 }
2397
2398 private void finishRecipientErrorDialog() {
2399 // after the user dismisses the recipient error
2400 // dialog we want to make sure to refocus the
2401 // recipient to field so they can fix the issue
2402 // easily
2403 if (mTo != null) {
2404 mTo.requestFocus();
2405 }
2406 }
2407
Mindy Pereira82cc5662012-01-09 17:29:30 -08002408 /**
2409 * Show an error because the user has entered an invalid recipient.
2410 * @param message
2411 */
Tony Mantler2558b502013-07-09 10:53:34 -07002412 private void showRecipientErrorDialog(final String message) {
2413 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2414 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002415 }
2416
2417 /**
2418 * Update the state of the UI based on whether or not the current draft
2419 * needs to be saved and the message is not empty.
2420 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002421 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002422 if (mSave != null) {
2423 mSave.setEnabled((shouldSave() && !isBlank()));
2424 }
2425 }
2426
2427 /**
2428 * Returns true if we need to save the current draft.
2429 */
2430 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002431 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002432 // The message should only be saved if:
2433 // It hasn't been sent AND
2434 // Some text has been added to the message OR
2435 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002436 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002437 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002438 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002439 }
2440 }
2441
2442 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002443 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002444 * @return boolean
2445 */
2446 public boolean isBlank() {
2447 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002448 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2449 mBodyView.getText().toString()) == 0)
2450 && mTo.length() == 0
2451 && mCc.length() == 0 && mBcc.length() == 0
2452 && mAttachmentsView.getAttachments().size() == 0;
2453 }
2454
2455 @VisibleForTesting
2456 protected int getSignatureStartPosition(String signature, String bodyText) {
2457 int startPos = -1;
2458
2459 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2460 return startPos;
2461 }
2462
2463 int bodyLength = bodyText.length();
2464 int signatureLength = signature.length();
2465 String printableVersion = convertToPrintableSignature(signature);
2466 int printableLength = printableVersion.length();
2467
2468 if (bodyLength >= printableLength
2469 && bodyText.substring(bodyLength - printableLength)
2470 .equals(printableVersion)) {
2471 startPos = bodyLength - printableLength;
2472 } else if (bodyLength >= signatureLength
2473 && bodyText.substring(bodyLength - signatureLength)
2474 .equals(signature)) {
2475 startPos = bodyLength - signatureLength;
2476 }
2477 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002478 }
2479
2480 /**
2481 * Allows any changes made by the user to be ignored. Called when the user
2482 * decides to discard a draft.
2483 */
2484 private void discardChanges() {
2485 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002486 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002487 mReplyFromChanged = false;
2488 }
2489
2490 /**
Mindy Pereira181df782012-03-01 13:32:44 -08002491 * @param save
2492 * @param showToast
2493 * @return Whether the send or save succeeded.
2494 */
2495 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002496 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002497 if (mAccounts == null || mAccount == null) {
2498 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002499 if (autoSend) {
2500 finish();
2501 }
Mark Wei009b3712012-10-18 18:07:50 -07002502 return false;
2503 }
2504
Scott Kennedyff8553f2013-04-05 20:57:44 -07002505 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002506 if (orientationChanged) {
2507 to = cc = bcc = new String[0];
2508 } else {
2509 to = getToAddresses();
2510 cc = getCcAddresses();
2511 bcc = getBccAddresses();
2512 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002513
Mindy Pereira181df782012-03-01 13:32:44 -08002514 // Don't let the user send to nobody (but it's okay to save a message
2515 // with no recipients)
2516 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2517 showRecipientErrorDialog(getString(R.string.recipient_needed));
2518 return false;
2519 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002520
Mindy Pereira181df782012-03-01 13:32:44 -08002521 List<String> wrongEmails = new ArrayList<String>();
2522 if (!save) {
2523 checkInvalidEmails(to, wrongEmails);
2524 checkInvalidEmails(cc, wrongEmails);
2525 checkInvalidEmails(bcc, wrongEmails);
2526 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002527
Mindy Pereira181df782012-03-01 13:32:44 -08002528 // Don't let the user send an email with invalid recipients
2529 if (wrongEmails.size() > 0) {
2530 String errorText = String.format(getString(R.string.invalid_recipient),
2531 wrongEmails.get(0));
2532 showRecipientErrorDialog(errorText);
2533 return false;
2534 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002535
Mindy Pereira181df782012-03-01 13:32:44 -08002536 // Show a warning before sending only if there are no attachments.
2537 if (!save) {
2538 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2539 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002540 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002541
Mindy Pereira181df782012-03-01 13:32:44 -08002542 // A warning about an empty body may not be warranted when
2543 // forwarding mails, since a common use case is to forward
2544 // quoted text and not append any more text.
2545 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002546
Mindy Pereira181df782012-03-01 13:32:44 -08002547 // When we bring up a dialog warning the user about a send,
2548 // assume that they accept sending the message. If they do not,
2549 // the dialog listener is required to enable sending again.
2550 if (warnAboutEmptySubject) {
Tony Mantler2558b502013-07-09 10:53:34 -07002551 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, save,
2552 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002553 return true;
2554 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002555
Mindy Pereira181df782012-03-01 13:32:44 -08002556 if (warnAboutEmptyBody) {
Tony Mantler2558b502013-07-09 10:53:34 -07002557 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, save,
2558 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002559 return true;
2560 }
2561 }
2562 // Ask for confirmation to send (if always required)
2563 if (showSendConfirmation()) {
Tony Mantler2558b502013-07-09 10:53:34 -07002564 showSendConfirmDialog(R.string.confirm_send_message, save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002565 return true;
2566 }
2567 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002568
Tony Mantler2558b502013-07-09 10:53:34 -07002569 sendOrSave(save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002570 return true;
2571 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002572
Mindy Pereira181df782012-03-01 13:32:44 -08002573 /**
2574 * Returns a boolean indicating whether warnings should be shown for empty
2575 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002576 *
Mindy Pereira181df782012-03-01 13:32:44 -08002577 * @return True if a warning should be shown for empty text fields
2578 */
2579 protected boolean showEmptyTextWarnings() {
2580 return mAttachmentsView.getAttachments().size() == 0;
2581 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002582
Mindy Pereira181df782012-03-01 13:32:44 -08002583 /**
2584 * Returns a boolean indicating whether the user should confirm each send
2585 *
2586 * @return True if a warning should be on each send
2587 */
2588 protected boolean showSendConfirmation() {
2589 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2590 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002591
Tony Mantler2558b502013-07-09 10:53:34 -07002592 public static class SendConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002593 // Public no-args constructor needed for fragment re-instantiation
2594 public SendConfirmDialogFragment() {}
2595
Tony Mantler2558b502013-07-09 10:53:34 -07002596 public static SendConfirmDialogFragment newInstance(final int messageId,
2597 final boolean save, final boolean showToast) {
2598 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2599 final Bundle args = new Bundle(3);
2600 args.putInt("messageId", messageId);
2601 args.putBoolean("save", save);
2602 args.putBoolean("showToast", showToast);
2603 frag.setArguments(args);
2604 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002605 }
Tony Mantler2558b502013-07-09 10:53:34 -07002606
2607 @Override
2608 public Dialog onCreateDialog(Bundle savedInstanceState) {
2609 final int messageId = getArguments().getInt("messageId");
2610 final boolean save = getArguments().getBoolean("save");
2611 final boolean showToast = getArguments().getBoolean("showToast");
2612
2613 return new AlertDialog.Builder(getActivity())
2614 .setMessage(messageId)
2615 .setTitle(R.string.confirm_send_title)
2616 .setIconAttribute(android.R.attr.alertDialogIcon)
2617 .setPositiveButton(R.string.send,
2618 new DialogInterface.OnClickListener() {
Scott Kennedyaa27bc02013-08-02 08:47:26 -07002619 @Override
Tony Mantler2558b502013-07-09 10:53:34 -07002620 public void onClick(DialogInterface dialog, int whichButton) {
2621 ((ComposeActivity)getActivity()).finishSendConfirmDialog(save,
2622 showToast);
2623 }
2624 })
2625 .create();
2626 }
2627 }
2628
2629 private void finishSendConfirmDialog(final boolean save, final boolean showToast) {
2630 sendOrSave(save, showToast);
2631 }
2632
2633 private void showSendConfirmDialog(final int messageId, final boolean save,
2634 final boolean showToast) {
2635 final DialogFragment frag = SendConfirmDialogFragment.newInstance(messageId, save,
2636 showToast);
2637 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08002638 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002639
Mindy Pereira181df782012-03-01 13:32:44 -08002640 /**
2641 * Returns whether the ComposeArea believes there is any text in the body of
2642 * the composition. TODO: When ComposeArea controls the Body as well, add
2643 * that here.
2644 */
2645 public boolean isBodyEmpty() {
2646 return !mQuotedTextView.isTextIncluded();
2647 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002648
Mindy Pereira181df782012-03-01 13:32:44 -08002649 /**
2650 * Test to see if the subject is empty.
2651 *
2652 * @return boolean.
2653 */
2654 // TODO: this will likely go away when composeArea.focus() is implemented
2655 // after all the widget control is moved over.
2656 public boolean isSubjectEmpty() {
2657 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2658 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002659
Mindy Pereira181df782012-03-01 13:32:44 -08002660 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002661 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07002662 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08002663 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07002664 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08002665 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002666
Paul Westbrookb4931c62013-01-14 17:51:18 -08002667 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07002668
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002669 MessageModification.putToAddresses(values, message.getToAddresses());
2670 MessageModification.putCcAddresses(values, message.getCcAddresses());
2671 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002672
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002673 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07002674
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002675 MessageModification.putSubject(values, message.subject);
Paul Westbrookb4931c62013-01-14 17:51:18 -08002676 // Make sure to remove only the composing spans from the Spannable before saving.
2677 final String htmlBody = Html.toHtml(removeComposingSpans(body));
Paul Westbrook05b92b82012-04-20 13:29:37 -07002678
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002679 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2680 StringBuilder fullBody = new StringBuilder(htmlBody);
2681 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002682 // HTML gets converted to text for now
2683 final String text = quotedText.toString();
2684 if (QuotedTextView.containsQuotedText(text)) {
2685 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07002686 final int quoteStartPos = fullBody.length() + pos;
2687 fullBody.append(text);
2688 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07002689 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07002690 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002691 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002692 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2693 // This shouldn't happen, but just use what we have,
2694 // and don't do server-side expansion
2695 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002696 }
2697 }
Mindy Pereira002ff522012-05-30 10:31:26 -07002698 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07002699 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002700 if (refMessage != null) {
2701 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2702 MessageModification.putBodyHtml(values, fullBody.toString());
2703 }
2704 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08002705 MessageModification.putBody(values,
2706 Utils.convertHtmlToPlainText(fullBody.toString()).toString());
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002707 }
2708 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002709 MessageModification.putBodyHtml(values, fullBody.toString());
mindypc59dd822012-11-13 10:56:21 -08002710 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2711 .toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002712 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002713 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002714 if (!TextUtils.isEmpty(refMessageId)) {
2715 MessageModification.putRefMessageId(values, refMessageId);
2716 }
Scott Kennedy60847252013-08-15 15:55:42 -07002717 if (extraValues != null) {
2718 values.putAll(extraValues);
2719 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002720 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2721 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08002722 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2723 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002724
Mindy Pereira181df782012-03-01 13:32:44 -08002725 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08002726 // Do the send/save action on the specified handler to avoid possible
2727 // ANRs
2728 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002729
Mindy Pereira181df782012-03-01 13:32:44 -08002730 return sendOrSaveMessage.requestId();
2731 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002732
Paul Westbrookb4931c62013-01-14 17:51:18 -08002733 /**
2734 * Removes any composing spans from the specified string. This will create a new
2735 * SpannableString instance, as to not modify the behavior of the EditText view.
2736 */
2737 private static SpannableString removeComposingSpans(Spanned body) {
2738 final SpannableString messageBody = new SpannableString(body);
2739 BaseInputConnection.removeComposingSpans(messageBody);
2740 return messageBody;
2741 }
2742
Mindy Pereira002ff522012-05-30 10:31:26 -07002743 private static int getDraftType(int mode) {
2744 int draftType = -1;
2745 switch (mode) {
2746 case ComposeActivity.COMPOSE:
2747 draftType = DraftType.COMPOSE;
2748 break;
2749 case ComposeActivity.REPLY:
2750 draftType = DraftType.REPLY;
2751 break;
2752 case ComposeActivity.REPLY_ALL:
2753 draftType = DraftType.REPLY_ALL;
2754 break;
2755 case ComposeActivity.FORWARD:
2756 draftType = DraftType.FORWARD;
2757 break;
2758 }
2759 return draftType;
2760 }
2761
Tony Mantler2558b502013-07-09 10:53:34 -07002762 private void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08002763 // Check if user is a monkey. Monkeys can compose and hit send
2764 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002765 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002766 return;
2767 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002768
Tony Mantler2558b502013-07-09 10:53:34 -07002769 final Spanned body = mBodyView.getEditableText();
2770
Mindy Pereira181df782012-03-01 13:32:44 -08002771 SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07002772 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08002773 private int mRestoredRequestId;
2774
Marc Blank0bbc8582012-04-23 15:07:57 -07002775 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002776 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002777 synchronized (mActiveTasks) {
2778 int numTasks = mActiveTasks.size();
2779 if (numTasks == 0) {
2780 // Start service so we won't be killed if this app is
2781 // put in the background.
2782 startService(new Intent(ComposeActivity.this, EmptyService.class));
2783 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002784
Mindy Pereira181df782012-03-01 13:32:44 -08002785 mActiveTasks.add(sendOrSaveTask);
2786 }
2787 if (sTestSendOrSaveCallback != null) {
2788 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2789 }
2790 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002791
Marc Blank0bbc8582012-04-23 15:07:57 -07002792 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002793 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2794 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002795 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08002796 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002797 mDraftId = message.id;
2798 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002799 if (sRequestMessageIdMap != null) {
2800 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2801 }
Mindy Pereira181df782012-03-01 13:32:44 -08002802 // Cache request message map, in case the process is killed
2803 saveRequestMap();
2804 }
2805 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002806 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002807 }
2808 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002809
Marc Blank0bbc8582012-04-23 15:07:57 -07002810 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002811 public Message getMessage() {
2812 synchronized (mDraftLock) {
2813 return mDraft;
2814 }
2815 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002816
Marc Blank0bbc8582012-04-23 15:07:57 -07002817 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002818 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07002819 // Update the last sent from account.
2820 if (mAccount != null) {
2821 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2822 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002823 if (success) {
2824 // Successfully sent or saved so reset change markers
2825 discardChanges();
2826 } else {
2827 // A failure happened with saving/sending the draft
2828 // TODO(pwestbro): add a better string that should be used
2829 // when failing to send or save
2830 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2831 .show();
2832 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002833
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002834 int numTasks;
2835 synchronized (mActiveTasks) {
2836 // Remove the task from the list of active tasks
2837 mActiveTasks.remove(task);
2838 numTasks = mActiveTasks.size();
2839 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002840
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002841 if (numTasks == 0) {
2842 // Stop service so we can be killed.
2843 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2844 }
2845 if (sTestSendOrSaveCallback != null) {
2846 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2847 }
2848 }
Mindy Pereira181df782012-03-01 13:32:44 -08002849 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002850
Tony Mantler1e05a1e2013-08-12 16:44:26 -07002851 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002852
Mindy Pereira181df782012-03-01 13:32:44 -08002853 if (mSendSaveTaskHandler == null) {
2854 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2855 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002856
Mindy Pereira181df782012-03-01 13:32:44 -08002857 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2858 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002859
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002860 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002861 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2862 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Scott Kennedy60847252013-08-15 15:55:42 -07002863 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount, mExtraValues);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002864
Mindy Pereira181df782012-03-01 13:32:44 -08002865 // Don't display the toast if the user is just changing the orientation,
2866 // but we still need to save the draft to the cursor because this is how we restore
2867 // the attachments when the configuration change completes.
2868 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2869 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2870 Toast.LENGTH_LONG).show();
2871 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002872
Mindy Pereira181df782012-03-01 13:32:44 -08002873 // Need to update variables here because the send or save completes
2874 // asynchronously even though the toast shows right away.
2875 discardChanges();
2876 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002877
Mindy Pereira181df782012-03-01 13:32:44 -08002878 // If we are sending, finish the activity
2879 if (!save) {
2880 finish();
2881 }
2882 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002883
Mindy Pereira181df782012-03-01 13:32:44 -08002884 /**
2885 * Save the state of the request messageid map. This allows for the Gmail
2886 * process to be killed, but and still allow for ComposeActivity instances
2887 * to be recreated correctly.
2888 */
2889 private void saveRequestMap() {
2890 // TODO: store the request map in user preferences.
2891 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002892
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002893 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002894 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2895 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002896 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002897 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08002898 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002899 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2900 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002901 }
2902
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002903 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002904 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002905 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07002906 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002907 }
2908 }
2909
Andy Huangdc97bf42013-08-15 16:52:45 -07002910 private void logSendOrSave(boolean save) {
2911 final String category = (save) ? "message_save" : "message_send";
2912 final int attachmentCount = getAttachments().size();
2913 final String msgType;
2914 switch (mComposeMode) {
2915 case COMPOSE:
2916 msgType = "new_message";
2917 break;
2918 case REPLY:
2919 msgType = "reply";
2920 break;
2921 case REPLY_ALL:
2922 msgType = "reply_all";
2923 break;
2924 case FORWARD:
2925 msgType = "forward";
2926 break;
2927 default:
2928 msgType = "unknown";
2929 break;
2930 }
2931 final String label;
2932 final long value;
2933 if (mComposeMode == COMPOSE) {
2934 label = Integer.toString(attachmentCount);
2935 value = attachmentCount;
2936 } else {
2937 label = null;
2938 value = 0;
2939 }
2940 Analytics.getInstance().sendEvent(category, msgType, label, value);
2941 }
2942
Mindy Pereira326c6602012-01-04 15:32:42 -08002943 @Override
2944 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002945 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002946 if (position == ComposeActivity.REPLY) {
2947 mComposeMode = ComposeActivity.REPLY;
2948 } else if (position == ComposeActivity.REPLY_ALL) {
2949 mComposeMode = ComposeActivity.REPLY_ALL;
2950 } else if (position == ComposeActivity.FORWARD) {
2951 mComposeMode = ComposeActivity.FORWARD;
2952 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002953 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002954 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002955 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08002956 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08002957 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002958 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002959 boolean showCc = false;
2960 boolean showBcc = false;
2961 if (mDraft != null) {
2962 // Following desktop behavior, if the user has added a BCC
2963 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002964 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07002965 // Use the draft to determine what to populate.
2966 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002967 showCc = showBcc
2968 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08002969 }
2970 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07002971 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08002972 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07002973 }
2974 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002975 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002976 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002977 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08002978 return true;
2979 }
2980
Mindy Pereirab3112a22012-06-20 12:10:03 -07002981 @VisibleForTesting
2982 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08002983 // When switching between reply, reply all, forward,
2984 // follow the behavior of webview.
2985 // The contents of the following fields are cleared
2986 // so that they can be populated directly from the
2987 // ref message:
2988 // 1) Any recipient fields
2989 // 2) The subject
2990 mTo.setText("");
2991 mCc.setText("");
2992 mBcc.setText("");
2993 // Any edits to the subject are replaced with the original subject.
2994 mSubject.setText("");
2995
2996 // Any changes to the contents of the following fields are kept:
2997 // 1) Body
2998 // 2) Attachments
2999 // If the user made changes to attachments, keep their changes.
3000 if (!mAttachmentsChanged) {
3001 mAttachmentsView.deleteAllAttachments();
3002 }
3003 }
3004
Mindy Pereira326c6602012-01-04 15:32:42 -08003005 private class ComposeModeAdapter extends ArrayAdapter<String> {
3006
3007 private LayoutInflater mInflater;
3008
3009 public ComposeModeAdapter(Context context) {
3010 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3011 .getStringArray(R.array.compose_modes));
3012 }
3013
3014 private LayoutInflater getInflater() {
3015 if (mInflater == null) {
3016 mInflater = LayoutInflater.from(getContext());
3017 }
3018 return mInflater;
3019 }
3020
3021 @Override
3022 public View getView(int position, View convertView, ViewGroup parent) {
3023 if (convertView == null) {
3024 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3025 }
3026 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3027 return super.getView(position, convertView, parent);
3028 }
3029 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003030
3031 @Override
3032 public void onRespondInline(String text) {
3033 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003034 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003035 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003036 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003037 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003038 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003039 }
3040
3041 /**
3042 * Append text to the body of the message. If there is no existing body
3043 * text, just sets the body to text.
3044 *
3045 * @param text
3046 * @param withSignature True to append a signature.
3047 */
3048 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003049 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003050 if (bodyText != null && bodyText.length() > 0) {
3051 bodyText.append(text);
3052 } else {
3053 setBody(text, withSignature);
3054 }
3055 }
3056
3057 /**
3058 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003059 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003060 * @param text
3061 * @param withSignature True to append a signature.
3062 */
3063 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003064 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003065 if (withSignature) {
3066 appendSignature();
3067 }
3068 }
3069
3070 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003071 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07003072 boolean hasFocus = mBodyView.hasFocus();
mindyp27083062012-11-15 09:02:01 -08003073 int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
3074 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003075 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003076 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003077 // Appending a signature does not count as changing text.
3078 mBodyView.removeTextChangedListener(this);
3079 mBodyView.append(convertToPrintableSignature(mSignature));
3080 mBodyView.addTextChangedListener(this);
3081 }
Mindy Pereira433b1982012-04-03 11:53:07 -07003082 if (hasFocus) {
3083 focusBody();
3084 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003085 }
3086 }
3087
3088 private String convertToPrintableSignature(String signature) {
3089 String signatureResource = getResources().getString(R.string.signature);
3090 if (signature == null) {
3091 signature = "";
3092 }
3093 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003094 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003095
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003096 @Override
3097 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003098 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3099 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003100 // Clear a signature, if there was one.
3101 mBodyView.removeTextChangedListener(this);
3102 String oldSignature = mSignature;
3103 String bodyText = getBody().getText().toString();
3104 if (!TextUtils.isEmpty(oldSignature)) {
3105 int pos = getSignatureStartPosition(oldSignature, bodyText);
3106 if (pos > -1) {
3107 mBodyView.setText(bodyText.substring(0, pos));
3108 }
3109 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003110 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003111 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003112 // TODO: handle discarding attachments when switching accounts.
3113 // Only enable save for this draft if there is any other content
3114 // in the message.
3115 if (!isBlank()) {
3116 enableSave(true);
3117 }
3118 mReplyFromChanged = true;
3119 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003120 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003121 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003122
3123 public void enableSave(boolean enabled) {
3124 if (mSave != null) {
3125 mSave.setEnabled(enabled);
3126 }
3127 }
3128
Tony Mantler2558b502013-07-09 10:53:34 -07003129 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003130 // Public no-args constructor needed for fragment re-instantiation
3131 public DiscardConfirmDialogFragment() {}
3132
Tony Mantler2558b502013-07-09 10:53:34 -07003133 @Override
3134 public Dialog onCreateDialog(Bundle savedInstanceState) {
3135 return new AlertDialog.Builder(getActivity())
3136 .setMessage(R.string.confirm_discard_text)
3137 .setPositiveButton(R.string.discard,
3138 new DialogInterface.OnClickListener() {
3139 @Override
3140 public void onClick(DialogInterface dialog, int which) {
3141 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3142 }
3143 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003144 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003145 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003146 }
3147 }
3148
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003149 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003150 final DialogFragment frag = new DiscardConfirmDialogFragment();
3151 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003152 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003153 /**
3154 * Effectively discard the current message.
3155 *
3156 * This method is either invoked from the menu or from the dialog
3157 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003158 */
Tony Mantler2558b502013-07-09 10:53:34 -07003159 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003160 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003161 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3162 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003163 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003164 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003165 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3166 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003167 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003168 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003169 // This is not strictly necessary (since we should not try to
3170 // save the draft after calling this) but it ensures that if we
3171 // do save again for some reason we make a new draft rather than
3172 // trying to resave an expunged draft.
3173 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3174 }
3175 }
3176
Tony Mantler2558b502013-07-09 10:53:34 -07003177 // Display a toast to let the user know
3178 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003179
3180 // This prevents the draft from being saved in onPause().
3181 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003182 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003183 finish();
3184 }
3185
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003186 private void saveIfNeeded() {
3187 if (mAccount == null) {
3188 // We have not chosen an account yet so there's no way that we can save. This is ok,
3189 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3190 // user has not interacted with us yet and there is no real state to save.
3191 return;
3192 }
3193
3194 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003195 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003196 }
3197 }
3198
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003199 @Override
3200 public void onAttachmentDeleted() {
3201 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003202 // If we are showing any attachments, make sure we have an upper
3203 // divider.
3204 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003205 updateSaveUi();
3206 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003207
mindyp40882432012-09-06 11:07:40 -07003208 @Override
3209 public void onAttachmentAdded() {
3210 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3211 mAttachmentsView.focusLastAttachment();
3212 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003213
3214 /**
3215 * This is called any time one of our text fields changes.
3216 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003217 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003218 public void afterTextChanged(Editable s) {
3219 mTextChanged = true;
3220 updateSaveUi();
3221 }
3222
3223 @Override
3224 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3225 // Do nothing.
3226 }
3227
Marc Blank0bbc8582012-04-23 15:07:57 -07003228 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003229 public void onTextChanged(CharSequence s, int start, int before, int count) {
3230 // Do nothing.
3231 }
3232
3233
3234 // There is a big difference between the text associated with an address changing
3235 // to add the display name or to format properly and a recipient being added or deleted.
3236 // Make sure we only notify of changes when a recipient has been added or deleted.
3237 private class RecipientTextWatcher implements TextWatcher {
3238 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3239
3240 private RecipientEditTextView mView;
3241
3242 private TextWatcher mListener;
3243
3244 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3245 mView = view;
3246 mListener = listener;
3247 }
3248
3249 @Override
3250 public void afterTextChanged(Editable s) {
3251 if (hasChanged()) {
3252 mListener.afterTextChanged(s);
3253 }
3254 }
3255
3256 private boolean hasChanged() {
3257 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
3258 int totalCount = currRecips.length;
3259 int totalPrevCount = 0;
3260 for (Entry<String, Integer> entry : mContent.entrySet()) {
3261 totalPrevCount += entry.getValue();
3262 }
3263 if (totalCount != totalPrevCount) {
3264 return true;
3265 }
3266
3267 for (String recip : currRecips) {
3268 if (!mContent.containsKey(recip)) {
3269 return true;
3270 } else {
3271 int count = mContent.get(recip) - 1;
3272 if (count < 0) {
3273 return true;
3274 } else {
3275 mContent.put(recip, count);
3276 }
3277 }
3278 }
3279 return false;
3280 }
3281
3282 private String[] tokenizeRecips(String[] recips) {
3283 // Tokenize them all and put them in the list.
3284 String[] recipAddresses = new String[recips.length];
3285 for (int i = 0; i < recips.length; i++) {
3286 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
3287 }
3288 return recipAddresses;
3289 }
3290
3291 @Override
3292 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3293 String[] recips = tokenizeRecips(getAddressesFromList(mView));
3294 for (String recip : recips) {
3295 if (!mContent.containsKey(recip)) {
3296 mContent.put(recip, 1);
3297 } else {
3298 mContent.put(recip, (mContent.get(recip)) + 1);
3299 }
3300 }
3301 }
3302
3303 @Override
3304 public void onTextChanged(CharSequence s, int start, int before, int count) {
3305 // Do nothing.
3306 }
3307 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003308
3309 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3310 if (sTestSendOrSaveCallback != null && testCallback != null) {
3311 throw new IllegalStateException("Attempting to register more than one test callback");
3312 }
3313 sTestSendOrSaveCallback = testCallback;
3314 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003315
3316 @VisibleForTesting
3317 protected ArrayList<Attachment> getAttachments() {
3318 return mAttachmentsView.getAttachments();
3319 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003320
3321 @Override
3322 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3323 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003324 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3325 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3326 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003327 case REFERENCE_MESSAGE_LOADER:
3328 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3329 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003330 case LOADER_ACCOUNT_CURSOR:
3331 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3332 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003333 }
3334 return null;
3335 }
3336
3337 @Override
3338 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003339 int id = loader.getId();
3340 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003341 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003342 if (data != null && data.moveToFirst()) {
3343 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003344 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003345 initFromRefMessage(mComposeMode);
3346 finishSetup(mComposeMode, intent, null);
3347 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003348 String to = intent.getStringExtra(EXTRA_TO);
3349 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003350 mRefMessage.setTo(null);
3351 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003352 clearChangeListeners();
3353 mTo.append(to);
3354 initChangeListeners();
3355 }
3356 }
3357 } else {
3358 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003359 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003360 break;
Alice Yanga990a712013-03-13 18:37:00 -07003361 case REFERENCE_MESSAGE_LOADER:
3362 // Only populate mRefMessage and leave other fields untouched.
3363 if (data != null && data.moveToFirst()) {
3364 mRefMessage = new Message(data);
3365 }
Andy Huang9f855d62013-05-30 17:15:03 -07003366 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003367 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003368 case LOADER_ACCOUNT_CURSOR:
3369 if (data != null && data.moveToFirst()) {
3370 // there are accounts now!
3371 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003372 final ArrayList<Account> accounts = new ArrayList<Account>();
3373 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003374 do {
3375 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003376 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003377 initializedAccounts.add(account);
3378 }
3379 accounts.add(account);
3380 } while (data.moveToNext());
3381 if (initializedAccounts.size() > 0) {
3382 findViewById(R.id.wait).setVisibility(View.GONE);
3383 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3384 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003385 mAccounts = initializedAccounts.toArray(
3386 new Account[initializedAccounts.size()]);
3387
Mindy Pereirab199d172012-08-13 11:04:03 -07003388 finishCreate();
3389 invalidateOptionsMenu();
3390 } else {
3391 // Show "waiting"
3392 account = accounts.size() > 0 ? accounts.get(0) : null;
3393 showWaitFragment(account);
3394 }
3395 }
3396 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003397 }
3398 }
3399
Mindy Pereirab199d172012-08-13 11:04:03 -07003400 private void showWaitFragment(Account account) {
3401 WaitFragment fragment = getWaitFragment();
3402 if (fragment != null) {
3403 fragment.updateAccount(account);
3404 } else {
3405 findViewById(R.id.wait).setVisibility(View.VISIBLE);
3406 replaceFragment(WaitFragment.newInstance(account, true),
3407 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3408 }
3409 }
3410
3411 private WaitFragment getWaitFragment() {
3412 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3413 }
3414
3415 private int replaceFragment(Fragment fragment, int transition, String tag) {
3416 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003417 fragmentTransaction.setTransition(transition);
3418 fragmentTransaction.replace(R.id.wait, fragment, tag);
3419 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3420 return transactionId;
3421 }
3422
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003423 @Override
3424 public void onLoaderReset(Loader<Cursor> arg0) {
3425 // Do nothing.
3426 }
Paul Westbrook83e6b572013-02-05 16:22:42 -08003427
3428 @Override
3429 public Context getActivityContext() {
3430 return this;
3431 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003432}