blob: af53fb959ddcb4762250db9758005b48cb6a6808 [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
Tony Mantler581edd42014-02-18 15:41:22 -080019import android.annotation.SuppressLint;
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -070020import android.annotation.TargetApi;
Andy Huang5c5fd572012-04-08 18:19:29 -070021import 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;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070029import android.content.ClipData;
Mindy Pereira6349a042012-01-04 11:25:01 -080030import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080031import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070033import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080034import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080035import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070036import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080037import android.content.pm.ActivityInfo;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -070038import android.content.res.Resources;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080039import android.database.Cursor;
Jin Cao36e23872014-07-29 13:41:12 -070040import android.graphics.Rect;
Mindy Pereira6349a042012-01-04 11:25:01 -080041import android.net.Uri;
Alan Lau15490232014-03-06 14:53:14 -080042import android.os.AsyncTask;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070043import android.os.Build;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080044import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080045import android.os.Handler;
46import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070047import android.os.ParcelFileDescriptor;
Mindy Pereira82cc5662012-01-09 17:29:30 -080048import android.provider.BaseColumns;
Alan Lau439aa5d2014-05-27 17:57:13 -070049import android.support.v4.app.RemoteInput;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -070050import android.support.v7.app.ActionBar;
51import android.support.v7.app.ActionBarActivity;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080052import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080053import android.text.Html;
Andy Huangff017272014-06-18 00:27:35 -070054import android.text.SpanWatcher;
mindyped9c2f02012-10-12 10:02:08 -070055import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080056import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080057import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080058import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080059import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080060import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070061import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070062import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080063import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080064import android.view.Menu;
65import android.view.MenuInflater;
66import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080067import android.view.View;
68import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070069import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080070import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070071import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080072import android.widget.ArrayAdapter;
Mindy Pereira433b1982012-04-03 11:53:07 -070073import android.widget.EditText;
Jin Cao36e23872014-07-29 13:41:12 -070074import android.widget.ScrollView;
Mindy Pereira6349a042012-01-04 11:25:01 -080075import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080076import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080077
Mindy Pereirac17d0732011-12-29 10:46:19 -080078import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070079import com.android.common.contacts.DataUsageStatUpdater;
Tony Mantler821e5782014-01-06 15:33:43 -080080import com.android.emailcommon.mail.Address;
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -070081import com.android.ex.chips.BaseRecipientAdapter;
82import com.android.ex.chips.DropdownChipLayouter;
Andy Huang5c5fd572012-04-08 18:19:29 -070083import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080084import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070085import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070086import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070087import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070088import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080089import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080090import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080091import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080092import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080093import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080094import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070095import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080096import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080097import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070098import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080099import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -0800100import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -0700101import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -0700102import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -0700103import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700104import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -0700105import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -0700106import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -0700107import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800108import com.android.mail.utils.ContentProviderTask;
Jin Cao77b4c2c2014-05-20 13:55:53 -0700109import com.android.mail.utils.HtmlUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -0700110import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800111import com.android.mail.utils.LogUtils;
Alan Lau15490232014-03-06 14:53:14 -0800112import com.android.mail.utils.NotificationActionUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800113import com.android.mail.utils.Utils;
Andy Huang9ed742c2014-06-18 02:34:50 -0700114import com.google.android.mail.common.html.parser.HtmlTree;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800115import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800116import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800117import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800118
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700119import java.io.FileNotFoundException;
120import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700121import java.io.UnsupportedEncodingException;
122import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800123import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700124import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800125import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800126import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800127import java.util.HashSet;
128import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700129import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700130import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800131import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800132
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700133public class ComposeActivity extends ActionBarActivity
134 implements OnClickListener, ActionBar.OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700135 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700136 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
Andrew Sappersteinffd61552014-05-14 15:04:23 -0700137 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
Jin Caoc5c550a2014-07-29 11:53:17 -0700138 RecipientEditTextView.RecipientEntryItemClickedListener, View.OnFocusChangeListener {
Scott Kennedya0287a82014-04-07 14:30:13 -0700139 /**
140 * An {@link Intent} action that launches {@link ComposeActivity}, but is handled as if the
141 * {@link Activity} were launched with no special action.
142 */
143 private static final String ACTION_LAUNCH_COMPOSE =
144 "com.android.mail.intent.action.LAUNCH_COMPOSE";
145
Mindy Pereira6349a042012-01-04 11:25:01 -0800146 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700147 public static final int COMPOSE = -1;
148 public static final int REPLY = 0;
149 public static final int REPLY_ALL = 1;
150 public static final int FORWARD = 2;
151 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800152
153 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700154 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800155
Mindy Pereira326689d2012-05-17 10:14:14 -0700156 private static final String EXTRA_SHOW_CC = "showCc";
157 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800158 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800159 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700160
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700161 private static final String UTF8_ENCODING_NAME = "UTF-8";
162
163 private static final String MAIL_TO = "mailto";
164
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700165 private static final String EXTRA_SUBJECT = "subject";
166
167 private static final String EXTRA_BODY = "body";
168
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700169 /**
170 * Expected to be html formatted text.
171 */
172 private static final String EXTRA_QUOTED_TEXT = "quotedText";
173
mindypd27b6ea2012-10-05 09:43:49 -0700174 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700175
Mark Wei62066e42012-09-13 12:07:02 -0700176 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
177
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700178 // Extra that we can get passed from other activities
Tony Mantler184ec732013-10-24 13:13:49 -0700179 @VisibleForTesting
180 protected static final String EXTRA_TO = "to";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700181 private static final String EXTRA_CC = "cc";
182 private static final String EXTRA_BCC = "bcc";
183
Scott Kennedy60847252013-08-15 15:55:42 -0700184 /**
185 * An optional extra containing a {@link ContentValues} of values to be added to
186 * {@link SendOrSaveMessage#mValues}.
187 */
188 public static final String EXTRA_VALUES = "extra-values";
189
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700190 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700191 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
192 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700193
Alan Lau439aa5d2014-05-27 17:57:13 -0700194 private static final String LEGACY_WEAR_EXTRA = "com.google.android.wearable.extras";
195
Andrew Sapperstein09da9422014-05-30 09:48:08 -0700196 /**
197 * Constant value for the threshold to use for auto-complete suggestions
198 * for the to/cc/bcc fields.
199 */
200 private static final int COMPLETION_THRESHOLD = 1;
201
Mindy Pereira82cc5662012-01-09 17:29:30 -0800202 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
203 // Map containing information about requests to create new messages, and the id of the
204 // messages that were the result of those requests.
205 //
206 // This map is used when the activity that initiated the save a of a new message, is killed
207 // before the save has completed (and when we know the id of the newly created message). When
208 // a save is completed, the service that is running in the background, will update the map
209 //
210 // When a new ComposeActivity instance is created, it will attempt to use the information in
211 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
212 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
213 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700214 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800215 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800216 /**
217 * Notifies the {@code Activity} that the caller is an Email
218 * {@code Activity}, so that the back behavior may be modified accordingly.
219 *
220 * @see #onAppUpPressed
221 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700222 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800223
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700224 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700225
Scott Kennedy5680ec22013-01-07 13:15:20 -0800226 /** If set, we will clear notifications for this folder. */
227 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
Alan Laue806c942014-06-06 16:19:15 -0700228 public static final String EXTRA_NOTIFICATION_CONVERSATION = "extra-notification-conversation";
Scott Kennedy5680ec22013-01-07 13:15:20 -0800229
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800230 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700231 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700232 // If this is a reply/forward then this extra will hold a uri we must query
233 // to get the original message.
234 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700235 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700236 // draft message
237 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800238 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700239 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800240 // Request numbers for activities we start
241 private static final int RESULT_PICK_ATTACHMENT = 1;
242 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700243 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700244 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700245
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700246 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
247 private static final String EXTRA_REQUEST_ID = "requestId";
248 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700249 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700250 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700251 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700252 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700253 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700254 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700255 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700256 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700257 private static final String MIME_TYPE_PHOTO = "image/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800258
Andy Huang9f855d62013-05-30 17:15:03 -0700259 private static final String KEY_INNER_SAVED_STATE = "compose_state";
260
Mindy Pereira82cc5662012-01-09 17:29:30 -0800261 /**
262 * A single thread for running tasks in the background.
263 */
Jin Cao5134be52014-05-06 19:18:38 -0700264 private final static Handler SEND_SAVE_TASK_HANDLER;
265 static {
266 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
267 handlerThread.start();
268
269 SEND_SAVE_TASK_HANDLER = new Handler(handlerThread.getLooper());
270 }
271
Jin Cao36e23872014-07-29 13:41:12 -0700272 private ScrollView mScrollView;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800273 private RecipientEditTextView mTo;
274 private RecipientEditTextView mCc;
275 private RecipientEditTextView mBcc;
Jin Cao9d358a12014-07-24 12:15:38 -0700276 private View mCcBccButton;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800277 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800278 private AttachmentsView mAttachmentsView;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700279 protected Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700280 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800281 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800282 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800283 private TextView mSubject;
284
Mindy Pereira326c6602012-01-04 15:32:42 -0800285 private ComposeModeAdapter mComposeModeAdapter;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700286 protected int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800287 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800288 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700289 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800290 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800291 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800292 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700293 @VisibleForTesting
294 protected FromAddressSpinner mFromSpinner;
Andy Huang5f082212014-06-11 22:19:21 -0700295 protected boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800296 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800297 private boolean mTextChanged;
298 private boolean mReplyFromChanged;
299 private MenuItem mSave;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700300 @VisibleForTesting
301 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800302 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
303 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800304 private ReplyFromAccount mDraftAccount;
Tony Mantler581edd42014-02-18 15:41:22 -0800305 private final Object mDraftLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800306
Mindy Pereira326c6602012-01-04 15:32:42 -0800307 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700308 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
309 */
310 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700311 private RecipientTextWatcher mToListener;
312 private RecipientTextWatcher mCcListener;
313 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700314 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700315 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700316 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700317 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700318
mindyp1623f9b2012-11-21 12:41:16 -0800319 // Array of the outstanding send or save tasks. Access is synchronized
320 // with the object itself
321 /* package for testing */
322 @VisibleForTesting
Tony Mantler581edd42014-02-18 15:41:22 -0800323 public final ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
mindyp1623f9b2012-11-21 12:41:16 -0800324 // FIXME: this variable is never read. related to sRequestMessageIdMap.
325 private int mRequestId;
326 private String mSignature;
327 private Account[] mAccounts;
328 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700329 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800330
Andy Huang9ed742c2014-06-18 02:34:50 -0700331 private final HtmlTree.ConverterFactory mSpanConverterFactory =
332 new HtmlTree.ConverterFactory() {
333 @Override
334 public HtmlTree.Converter<Spanned> createInstance() {
335 return getSpanConverter();
336 }
337 };
338
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700339 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800340 * Can be called from a non-UI thread.
341 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800342 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700343 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
344 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800345 }
346
Mindy Pereira6349a042012-01-04 11:25:01 -0800347 /**
348 * Can be called from a non-UI thread.
349 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800350 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700351 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800352 }
353
354 /**
355 * Can be called from a non-UI thread.
356 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700357 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700358 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
359 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700360 }
361
362 /**
363 * Can be called from a non-UI thread.
364 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700365 public static void composeWithExtraValues(Context launcher, Account account,
366 String subject, final ContentValues extraValues) {
367 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
368 }
369
370 /**
371 * Can be called from a non-UI thread.
372 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800373 public static Intent createReplyIntent(final Context launcher, final Account account,
374 final Uri messageUri, final boolean isReplyAll) {
375 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
376 }
377
378 /**
379 * Can be called from a non-UI thread.
380 */
381 public static Intent createForwardIntent(final Context launcher, final Account account,
382 final Uri messageUri) {
383 return createActionIntent(launcher, account, messageUri, FORWARD);
384 }
385
Scott Kennedya0287a82014-04-07 14:30:13 -0700386 private static Intent createActionIntent(final Context context, final Account account,
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800387 final Uri messageUri, final int action) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700388 final Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
389 intent.setPackage(context.getPackageName());
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800390
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700391 updateActionIntent(account, messageUri, action, intent);
392
393 return intent;
394 }
395
396 @VisibleForTesting
397 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800398 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
399 intent.putExtra(EXTRA_ACTION, action);
400 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
401 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
402
403 return intent;
404 }
405
406 /**
407 * Can be called from a non-UI thread.
408 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800409 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700410 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800411 }
412
413 /**
414 * Can be called from a non-UI thread.
415 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800416 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700417 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
418 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800419 }
420
421 /**
422 * Can be called from a non-UI thread.
423 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800424 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700425 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800426 }
427
Alice Yang1ebc2db2013-03-14 21:21:44 -0700428 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
429 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700430 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700431 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700432 }
433
Scott Kennedya0287a82014-04-07 14:30:13 -0700434 private static void launch(Context context, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700435 String toAddress, String body, String quotedText, String subject,
436 final ContentValues extraValues) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700437 Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
438 intent.setPackage(context.getPackageName());
Mindy Pereira6349a042012-01-04 11:25:01 -0800439 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
440 intent.putExtra(EXTRA_ACTION, action);
441 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700442 if (action == EDIT_DRAFT) {
443 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
444 } else {
445 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
446 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700447 if (toAddress != null) {
448 intent.putExtra(EXTRA_TO, toAddress);
449 }
450 if (body != null) {
451 intent.putExtra(EXTRA_BODY, body);
452 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700453 if (quotedText != null) {
454 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
455 }
456 if (subject != null) {
457 intent.putExtra(EXTRA_SUBJECT, subject);
458 }
Scott Kennedy60847252013-08-15 15:55:42 -0700459 if (extraValues != null) {
460 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
461 intent.putExtra(EXTRA_VALUES, extraValues);
462 }
Andy Huange0f03202014-06-13 17:34:49 -0700463 if (action == COMPOSE) {
464 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
465 } else if (message != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700466 intent.setData(Utils.normalizeUri(message.uri));
Andy Huange0f03202014-06-13 17:34:49 -0700467 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700468 context.startActivity(intent);
Mindy Pereira6349a042012-01-04 11:25:01 -0800469 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800470
Scott Kennedya0287a82014-04-07 14:30:13 -0700471 public static void composeMailto(Context context, Account account, Uri mailto) {
472 final Intent intent = new Intent(Intent.ACTION_VIEW, mailto);
473 intent.setPackage(context.getPackageName());
Andy Huang0a2a3462013-12-20 15:56:13 -0800474 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
475 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Andy Huange0f03202014-06-13 17:34:49 -0700476 if (mailto != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700477 intent.setData(Utils.normalizeUri(mailto));
Andy Huange0f03202014-06-13 17:34:49 -0700478 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700479 context.startActivity(intent);
Andy Huang0a2a3462013-12-20 15:56:13 -0800480 }
481
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800482 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700483 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800484 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800485 setContentView(R.layout.compose);
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700486 final ActionBar actionBar = getSupportActionBar();
Paul Westbrook4def3bf2014-07-01 00:38:17 -0700487 if (actionBar != null) {
488 // Hide the app icon.
Paul Westbrook5043cc22014-06-28 05:04:21 -0700489 actionBar.setIcon(null);
Paul Westbrook4def3bf2014-07-01 00:38:17 -0700490 actionBar.setDisplayUseLogoEnabled(false);
Paul Westbrook5043cc22014-06-28 05:04:21 -0700491 }
492
Andy Huang9f855d62013-05-30 17:15:03 -0700493 mInnerSavedState = (savedInstanceState != null) ?
494 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700495 checkValidAccounts();
496 }
497
498 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700499 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800500 findViews();
Tony Mantler581edd42014-02-18 15:41:22 -0800501 final Intent intent = getIntent();
502 final Message message;
503 final ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700504 mShowQuotedText = false;
Tony Mantler581edd42014-02-18 15:41:22 -0800505 final CharSequence quotedText;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700506 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700507 // Check for any of the possibly supplied accounts.;
Tony Mantler581edd42014-02-18 15:41:22 -0800508 final Account account;
Andy Huang9f855d62013-05-30 17:15:03 -0700509 if (hadSavedInstanceStateMessage(savedState)) {
510 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
511 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
Tony Mantler581edd42014-02-18 15:41:22 -0800512 message = savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700513
Andy Huang9f855d62013-05-30 17:15:03 -0700514 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800515 mRefMessage = savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700516 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700517
518 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700519 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700520 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700521 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
522 // Initialize the message from the message in the intent
Tony Mantler581edd42014-02-18 15:41:22 -0800523 message = intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700524 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800525 mRefMessage = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
526 mRefMessageUri = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
527 quotedText = null;
Andy Huang4fe0af82013-08-20 17:24:51 -0700528
529 if (Analytics.isLoggable()) {
530 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
531 Analytics.getInstance().sendEvent(
532 "notification_action", "compose", getActionString(action), 0);
533 }
534 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700535 }
Mark Wei62066e42012-09-13 12:07:02 -0700536 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700537
538 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800539 if (mAccount == null) {
540 return;
541 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700542
Scott Kennedyfe853d32013-06-19 11:47:35 -0700543 initRecipients();
544
Scott Kennedy5680ec22013-01-07 13:15:20 -0800545 // Clear the notification and mark the conversation as seen, if necessary
546 final Folder notificationFolder =
547 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800548
Alan Laue806c942014-06-06 16:19:15 -0700549 if (notificationFolder != null) {
550 final Uri conversationUri = intent.getParcelableExtra(EXTRA_NOTIFICATION_CONVERSATION);
551 Intent actionIntent;
552 if (conversationUri != null) {
553 actionIntent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS_WEAR);
554 actionIntent.putExtra(Utils.EXTRA_CONVERSATION, conversationUri);
555 } else {
556 actionIntent = new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
557 actionIntent.setData(Utils.appendVersionQueryParameter(this,
558 notificationFolder.folderUri.fullUri));
559 }
560 actionIntent.setPackage(getPackageName());
561 actionIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
562 actionIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
563
564 startService(actionIntent);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800565 }
566
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700567 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
568 mLaunchedFromEmail = true;
569 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
570 final Uri dataUri = intent.getData();
571 if (dataUri != null) {
572 final String dataScheme = intent.getData().getScheme();
573 final String accountScheme = mAccount.composeIntentUri.getScheme();
574 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
575 }
576 }
577
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700578 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700579 mShowQuotedText = true;
580 mComposeMode = action;
Alan Lau15490232014-03-06 14:53:14 -0800581
582 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Alan Lau575255c2014-05-16 11:44:27 -0700583 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
Alan Lau439aa5d2014-05-27 17:57:13 -0700584 String wearReply = null;
Alan Lau575255c2014-05-16 11:44:27 -0700585 if (remoteInput != null) {
Alan Lau439aa5d2014-05-27 17:57:13 -0700586 LogUtils.d(LOG_TAG, "Got remote input from new api");
587 CharSequence input = remoteInput.getCharSequence(
Alan Lau575255c2014-05-16 11:44:27 -0700588 NotificationActionUtils.WEAR_REPLY_INPUT);
Alan Lau439aa5d2014-05-27 17:57:13 -0700589 if (input != null) {
590 wearReply = input.toString();
Alan Lau15490232014-03-06 14:53:14 -0800591 }
Alan Lau575255c2014-05-16 11:44:27 -0700592 } else {
Alan Lau439aa5d2014-05-27 17:57:13 -0700593 // TODO: remove after legacy code has been removed.
594 LogUtils.d(LOG_TAG,
595 "No remote input from new api, falling back to compatibility mode");
596 ClipData clipData = intent.getClipData();
597 if (clipData != null
598 && LEGACY_WEAR_EXTRA.equals(clipData.getDescription().getLabel())) {
599 Bundle extras = clipData.getItemAt(0).getIntent().getExtras();
600 if (extras != null) {
601 wearReply = extras.getString(NotificationActionUtils.WEAR_REPLY_INPUT);
602 }
603 }
604 }
605
606 if (!TextUtils.isEmpty(wearReply)) {
607 createWearReplyTask(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION,
608 mComposeMode, wearReply).execute();
609 finish();
610 return;
611 } else {
612 LogUtils.w(LOG_TAG, "remote input string is null");
Alan Lau15490232014-03-06 14:53:14 -0800613 }
614 }
615
Alice Yanga990a712013-03-13 18:37:00 -0700616 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700617 return;
618 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700619 initFromDraftMessage(message);
620 initQuotedTextFromRefMessage(mRefMessage, action);
Alice Yanga990a712013-03-13 18:37:00 -0700621 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700622 // if we should be showing quoted text but mRefMessage is null
623 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700624 if (mShowQuotedText && mRefMessage == null) {
625 if (quotedText != null) {
626 initQuotedText(quotedText, false /* shouldQuoteText */);
627 } else if (mExtraValues != null) {
628 initExtraValues(mExtraValues);
629 return;
630 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700631 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700632 } else if (action == EDIT_DRAFT) {
Tony Mantler581edd42014-02-18 15:41:22 -0800633 if (message == null) {
634 throw new IllegalStateException("Message must not be null to edit draft");
635 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700636 initFromDraftMessage(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700637 // Update the action to the draft type of the previous draft
638 switch (message.draftType) {
639 case UIProvider.DraftType.REPLY:
640 action = REPLY;
641 break;
642 case UIProvider.DraftType.REPLY_ALL:
643 action = REPLY_ALL;
644 break;
645 case UIProvider.DraftType.FORWARD:
646 action = FORWARD;
647 break;
648 case UIProvider.DraftType.COMPOSE:
649 default:
650 action = COMPOSE;
651 break;
652 }
Alice Yanga990a712013-03-13 18:37:00 -0700653 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
654
655 mShowQuotedText = message.appendRefMessageContent;
656 if (message.refMessageUri != null) {
657 // If we're editing an existing draft that was in reference to an existing message,
658 // still need to load that original message since we might need to refer to the
659 // original sender and recipients if user switches "reply <-> reply-all".
660 mRefMessageUri = message.refMessageUri;
661 mComposeMode = action;
662 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
663 return;
664 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700665 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
666 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800667 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700668 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700669 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700670 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700671 if (initFromExtras(intent)) {
672 return;
673 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700674 }
Alice Yanga990a712013-03-13 18:37:00 -0700675
676 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700677 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700678 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700679
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -0700680 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
Alan Lau15490232014-03-06 14:53:14 -0800681 private static AsyncTask<Void, Void, Message> createWearReplyTask(
682 final ComposeActivity composeActivity,
683 final Uri refMessageUri, final String[] projection, final int action,
684 final String wearReply) {
685 return new AsyncTask<Void, Void, Message>() {
686 private Intent mEmptyServiceIntent = new Intent(composeActivity, EmptyService.class);
687
688 @Override
689 protected void onPreExecute() {
690 // Start service so we won't be killed if this app is put in the background.
691 composeActivity.startService(mEmptyServiceIntent);
692 }
693
694 @Override
695 protected Message doInBackground(Void... params) {
696 Cursor cursor = composeActivity.getContentResolver()
697 .query(refMessageUri, projection, null, null, null, null);
698 if (cursor != null) {
699 try {
700 cursor.moveToFirst();
701 return new Message(cursor);
702 } finally {
703 cursor.close();
704 }
705 }
706 return null;
707 }
708
709 @Override
710 protected void onPostExecute(Message message) {
711 composeActivity.stopService(mEmptyServiceIntent);
712
713 composeActivity.mRefMessage = message;
714 composeActivity.initFromRefMessage(action);
715 composeActivity.setBody(wearReply, false);
716 composeActivity.finishSetup(action, composeActivity.getIntent(), null);
717 composeActivity.sendOrSaveWithSanityChecks(false /* save */, true /* show toast */,
718 false /* orientationChanged */, true /* autoSend */);
719 }
720 };
721 }
722
Mindy Pereirab199d172012-08-13 11:04:03 -0700723 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700724 final Account[] allAccounts = AccountUtils.getAccounts(this);
725 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700726 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
727 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700728 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700729 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
730 }
731 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700732 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700733 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700734 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700735 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700736 anySyncing = true;
737 break;
738 }
739 }
740 if (!anySyncing) {
741 // There are accounts, but none are sync'd, which is just like having no accounts.
742 mAccounts = null;
743 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
744 return;
745 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700746 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700747 finishCreate();
748 }
749 }
750
Mindy Pereira47d0e652012-07-23 09:45:07 -0700751 private Account obtainAccount(Intent intent) {
752 Account account = null;
753 Object accountExtra = null;
754 if (intent != null && intent.getExtras() != null) {
755 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
756 if (accountExtra instanceof Account) {
757 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700758 } else if (accountExtra instanceof String) {
759 // This is the Account attached to the widget compose intent.
Tony Mantler26a20752014-02-28 16:44:24 -0800760 account = Account.newInstance((String) accountExtra);
mindyp7ae042e2012-08-27 13:27:37 -0700761 if (account != null) {
762 return account;
763 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700764 }
mindyp5ee9dc42013-01-08 09:54:54 -0800765 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
766 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
767 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700768 }
Tony Mantler581edd42014-02-18 15:41:22 -0800769
770 MailAppProvider provider = MailAppProvider.getInstance();
771 String lastAccountUri = provider.getLastSentFromAccount();
772 if (TextUtils.isEmpty(lastAccountUri)) {
773 lastAccountUri = provider.getLastViewedAccount();
Mindy Pereira47d0e652012-07-23 09:45:07 -0700774 }
Tony Mantler581edd42014-02-18 15:41:22 -0800775 if (!TextUtils.isEmpty(lastAccountUri)) {
776 accountExtra = Uri.parse(lastAccountUri);
777 }
778
Mindy Pereirab199d172012-08-13 11:04:03 -0700779 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700780 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
781 // For backwards compatibility, we need to check account
782 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700783 for (Account a : mAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -0700784 if (a.getEmailAddress().equals(accountExtra)) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700785 account = a;
786 }
787 }
788 } else if (accountExtra instanceof Uri) {
789 // The uri of the last viewed account is what is stored in
790 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700791 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700792 if (a.uri.equals(accountExtra)) {
793 account = a;
794 }
795 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700796 }
797 if (account == null) {
798 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700799 }
800 }
801 return account;
802 }
803
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700804 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800805 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700806 // Don't bother with the intent if we have procured a message from the
807 // intent already.
808 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
809 initAttachmentsFromIntent(intent);
810 }
Alice Yanga990a712013-03-13 18:37:00 -0700811 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700812 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
813 action);
mindypd4a48662012-11-08 17:13:49 -0800814
815 // If this is a draft message, the draft account is whatever account was
816 // used to open the draft message in Compose.
817 if (mDraft != null) {
818 mDraftAccount = mReplyFromAccount;
819 }
820
Mindy Pereira75f66632012-01-11 11:42:02 -0800821 initChangeListeners();
Jin Cao32973b42014-05-06 16:12:11 -0700822
823 // These two should be identical since we check CC and BCC the same way
824 boolean showCc = !TextUtils.isEmpty(mCc.getText()) || (savedInstanceState != null &&
825 savedInstanceState.getBoolean(EXTRA_SHOW_CC));
826 boolean showBcc = !TextUtils.isEmpty(mBcc.getText()) || (savedInstanceState != null &&
827 savedInstanceState.getBoolean(EXTRA_SHOW_BCC));
828 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereira326689d2012-05-17 10:14:14 -0700829 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700830 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800831
Tony Mantler581edd42014-02-18 15:41:22 -0800832 mRespondedInline = mInnerSavedState != null &&
833 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE);
mindyp1623f9b2012-11-21 12:41:16 -0800834 if (mRespondedInline) {
835 mQuotedTextView.setVisibility(View.GONE);
836 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700837 }
838
Scott Kennedyff8553f2013-04-05 20:57:44 -0700839 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700840 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
841 }
842
Mindy Pereira71c9e562012-05-17 11:01:02 -0700843 private void updateHideOrShowQuotedText(boolean showQuotedText) {
844 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700845 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700846 }
847
848 private void setFocus(int action) {
849 if (action == EDIT_DRAFT) {
850 int type = mDraft.draftType;
851 switch (type) {
852 case UIProvider.DraftType.COMPOSE:
853 case UIProvider.DraftType.FORWARD:
854 action = COMPOSE;
855 break;
856 case UIProvider.DraftType.REPLY:
857 case UIProvider.DraftType.REPLY_ALL:
858 default:
859 action = REPLY;
860 break;
861 }
862 }
863 switch (action) {
864 case FORWARD:
865 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800866 if (TextUtils.isEmpty(mTo.getText())) {
867 mTo.requestFocus();
868 break;
869 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700870 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700871 case REPLY:
872 case REPLY_ALL:
873 default:
874 focusBody();
875 break;
876 }
877 }
878
879 /**
880 * Focus the body of the message.
881 */
Tony Mantler6a7ac782014-02-19 15:22:02 -0800882 private void focusBody() {
Mindy Pereira433b1982012-04-03 11:53:07 -0700883 mBodyView.requestFocus();
Tony Mantler6a7ac782014-02-19 15:22:02 -0800884 resetBodySelection();
885 }
Mindy Pereira433b1982012-04-03 11:53:07 -0700886
Tony Mantler6a7ac782014-02-19 15:22:02 -0800887 private void resetBodySelection() {
888 int length = mBodyView.getText().length();
Mindy Pereira433b1982012-04-03 11:53:07 -0700889 int signatureStartPos = getSignatureStartPosition(
890 mSignature, mBodyView.getText().toString());
891 if (signatureStartPos > -1) {
892 // In case the user deleted the newlines...
893 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700894 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700895 // Move cursor to the end.
896 mBodyView.setSelection(length);
897 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800898 }
899
900 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700901 protected void onStart() {
902 super.onStart();
903
904 Analytics.getInstance().activityStart(this);
905 }
906
907 @Override
908 protected void onStop() {
909 super.onStop();
910
911 Analytics.getInstance().activityStop(this);
912 }
913
914 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800915 protected void onResume() {
916 super.onResume();
917 // Update the from spinner as other accounts
918 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800919 if (mFromSpinner != null && mAccount != null) {
Andrew Sappersteina01ddca2014-03-04 10:59:56 -0800920 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800921 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800922 }
923
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800924 @Override
925 protected void onPause() {
926 super.onPause();
927
Mindy Pereiraa2148332012-07-02 13:54:14 -0700928 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700929 // Don't save unnecessary drafts if we are only changing the orientation.
930 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700931 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700932
Andy Huange003b4c2013-08-16 10:32:05 -0700933 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700934 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
935 // because that method can be invoked many times in a single compose session.)
936 logSendOrSave(true /* save */);
937 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700938 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800939 }
940
941 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700942 protected void onActivityResult(int request, int result, Intent data) {
Andy Huang5f082212014-06-11 22:19:21 -0700943 if (request == RESULT_PICK_ATTACHMENT) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700944 mAddingAttachment = false;
Andy Huang5f082212014-06-11 22:19:21 -0700945 if (result == RESULT_OK) {
946 addAttachmentAndUpdateView(data);
947 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700948 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700949 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700950 if (result != RESULT_OK) {
951 finish();
952 } else {
953 // Watch for accounts to show up!
954 // restart the loader to get the updated list of accounts
955 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
956 showWaitFragment(null);
957 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800958 }
959 }
960
961 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700962 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700963 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
964 if (hasAccounts) {
965 clearChangeListeners();
966 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700967 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700968 if (mInnerSavedState != null) {
969 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
970 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
971 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700972 // There should be a focus and it should be an EditText since we
973 // only save these extras if these conditions are true.
974 EditText focusEditText = (EditText) getCurrentFocus();
975 final int length = focusEditText.getText().length();
976 if (selectionStart < length && selectionEnd < length) {
977 focusEditText.setSelection(selectionStart, selectionEnd);
978 }
979 }
980 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700981 if (hasAccounts) {
982 initChangeListeners();
983 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700984 }
985
986 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700987 protected void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800988 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700989 final Bundle inner = new Bundle();
990 saveState(inner);
991 state.putBundle(KEY_INNER_SAVED_STATE, inner);
992 }
993
994 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700995 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
996 if (mAccounts == null || mAccounts.length == 0) {
997 return;
998 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700999 // The framework is happy to save and restore the selection but only if it also saves and
1000 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
1001 // this manually.
1002 View focus = getCurrentFocus();
1003 if (focus != null && focus instanceof EditText) {
1004 EditText focusEditText = (EditText) focus;
1005 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
1006 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
1007 }
Paul Westbrook6273e962012-04-23 10:44:15 -07001008
1009 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -07001010 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -07001011 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
1012 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
1013 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001014 if (selectedReplyFromAccount != null) {
1015 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
1016 .toString());
1017 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
1018 } else {
1019 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
1020 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001021
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001022 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
1023 // We don't have a draft id, and we have a request id,
1024 // save the request id.
1025 state.putInt(EXTRA_REQUEST_ID, mRequestId);
1026 }
1027
1028 // We want to restore the current mode after a pause
1029 // or rotation.
1030 int mode = getMode();
1031 state.putInt(EXTRA_ACTION, mode);
1032
Jin Cao77b4c2c2014-05-20 13:55:53 -07001033 final Message message = createMessage(selectedReplyFromAccount, mRefMessage, mode,
1034 removeComposingSpans(mBodyView.getText()));
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001035 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -08001036 message.id = mDraft.id;
1037 message.serverId = mDraft.serverId;
1038 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001039 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001040 state.putParcelable(EXTRA_MESSAGE, message);
1041
1042 if (mRefMessage != null) {
1043 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001044 } else if (message.appendRefMessageContent) {
1045 // If we have no ref message but should be appending
1046 // ref message content, we have orphaned quoted text. Save it.
1047 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001048 }
Mindy Pereira326689d2012-05-17 10:14:14 -07001049 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
1050 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -08001051 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -08001052 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -07001053 state.putParcelableArrayList(
1054 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -07001055
1056 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001057 }
1058
1059 private int getMode() {
1060 int mode = ComposeActivity.COMPOSE;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001061 final ActionBar actionBar = getSupportActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001062 if (actionBar != null
1063 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001064 mode = actionBar.getSelectedNavigationIndex();
1065 }
1066 return mode;
1067 }
1068
Jin Cao77b4c2c2014-05-20 13:55:53 -07001069 /**
1070 * This function might be called from a background thread, so be sure to move everything that
1071 * can potentially modify the UI to the main thread (e.g. removeComposingSpans for body).
1072 */
Anthony Lee2a3cc132014-04-22 14:15:25 -07001073 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, Message refMessage,
Jin Cao77b4c2c2014-05-20 13:55:53 -07001074 int mode, Spanned body) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001075 Message message = new Message();
1076 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -07001077 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001078 message.uri = null;
1079 message.conversationUri = null;
1080 message.subject = mSubject.getText().toString();
1081 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001082 message.setTo(formatSenders(mTo.getText().toString()));
1083 message.setCc(formatSenders(mCc.getText().toString()));
1084 message.setBcc(formatSenders(mBcc.getText().toString()));
1085 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001086 message.dateReceivedMs = 0;
Jin Cao77b4c2c2014-05-20 13:55:53 -07001087 message.bodyHtml = spannedBodyToHtml(body, true);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001088 message.bodyText = mBodyView.getText().toString();
1089 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -07001090 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001091 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
1092 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
1093 message.hasAttachments = attachments != null && attachments.size() > 0;
1094 message.attachmentListUri = null;
1095 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001096 message.alwaysShowImages = false;
1097 message.attachmentsJson = Attachment.toJSONArray(attachments);
1098 CharSequence quotedText = mQuotedTextView.getQuotedText();
Anthony Lee2a3cc132014-04-22 14:15:25 -07001099 message.quotedTextOffset = -1; // Just a default value.
1100 if (refMessage != null && !TextUtils.isEmpty(quotedText)) {
1101 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1102 // We want the index to point to just the quoted text and not the
1103 // "On December 25, 2014..." part of it.
1104 message.quotedTextOffset =
1105 QuotedTextView.getQuotedTextOffset(quotedText.toString());
1106 } else if (!TextUtils.isEmpty(refMessage.bodyText)) {
1107 // We want to point to the entire quoted text.
1108 message.quotedTextOffset = QuotedTextView.findQuotedTextIndex(quotedText);
1109 }
1110 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001111 message.accountUri = null;
Greg Bullock14fd3042014-08-12 09:21:15 +02001112 message.setFrom(computeFromForAccount(selectedReplyFromAccount));
1113 message.draftType = getDraftType(mode);
1114 return message;
1115 }
1116
1117 protected String computeFromForAccount(ReplyFromAccount selectedReplyFromAccount) {
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001118 final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
1119 : mAccount != null ? mAccount.getEmailAddress() : null;
Tony Mantlerf441d142013-10-22 11:46:00 -07001120 final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
1121 : mAccount != null ? mAccount.getSenderName() : null;
Tony Mantler821e5782014-01-06 15:33:43 -08001122 final Address address = new Address(email, senderName);
Greg Bullock14fd3042014-08-12 09:21:15 +02001123 return address.toHeader();
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001124 }
1125
Scott Kennedyff8553f2013-04-05 20:57:44 -07001126 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -07001127 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
1128 return string.substring(0, string.length() - 1);
1129 }
1130 return string;
1131 }
1132
Mindy Pereira818143e2012-01-11 13:59:49 -08001133 @VisibleForTesting
Andy Huang91ede362014-01-21 19:16:00 -08001134 protected void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -07001135 if (account == null) {
1136 return;
1137 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001138 if (!account.equals(mAccount)) {
1139 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001140 mCachedSettings = mAccount.settings;
1141 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001142 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001143 if (mAccount != null) {
Tony Mantler79b11562013-10-09 15:31:50 -07001144 MailActivity.setNfcMessage(mAccount.getEmailAddress());
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001145 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001146 }
1147
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001148 private void initFromSpinner(Bundle bundle, int action) {
1149 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001150 action = COMPOSE;
1151 }
Andrew Sappersteina01ddca2014-03-04 10:59:56 -08001152 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001153
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001154 if (bundle != null) {
1155 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
1156 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
1157 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
1158 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001159 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001160 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
1161 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001162 }
1163 if (mReplyFromAccount == null) {
1164 if (mDraft != null) {
1165 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
1166 } else if (mRefMessage != null) {
1167 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
1168 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001169 }
1170 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -07001171 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001172 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001173
Mindy Pereira62de1b12012-04-06 12:17:56 -07001174 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001175
Mindy Pereira62de1b12012-04-06 12:17:56 -07001176 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001177 // If there is only 1 account, just show that account.
1178 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001179 // send mail from / save drafts to.
1180 mFromStatic.setVisibility(View.GONE);
Andy Huangca4676f2014-01-16 13:22:20 -08001181 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001182 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001183 } else {
1184 mFromStatic.setVisibility(View.VISIBLE);
Andy Huangca4676f2014-01-16 13:22:20 -08001185 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001186 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001187 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001188 }
1189
Mindy Pereira62de1b12012-04-06 12:17:56 -07001190 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1191 if (refMessage.accountUri != null) {
1192 // This must be from combined inbox.
1193 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1194 for (ReplyFromAccount from : replyFromAccounts) {
1195 if (from.account.uri.equals(refMessage.accountUri)) {
1196 return from;
1197 }
1198 }
1199 return null;
1200 } else {
1201 return getReplyFromAccount(account, refMessage);
1202 }
1203 }
1204
1205 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001206 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001207 * return who the message should be sent from.
1208 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001209 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001210 * @return the address from which to reply.
1211 */
1212 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1213 // First see if we are supposed to use the default address or
1214 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001215 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001216 return getDefaultReplyFromAccount(account);
1217 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001218 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001219 // all the message recipients and find one that matches
1220 // a custom from or account.
1221 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001222 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1223 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001224 return getMatchingRecipient(account, allRecipients);
1225 }
1226 }
1227
1228 /**
1229 * Compare all the recipients of an email to the current account and all
1230 * custom addresses associated with that account. Return the match if there
1231 * is one, or the default account if there isn't.
1232 */
1233 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1234 // Tokenize the list and place in a hashmap.
1235 ReplyFromAccount matchingReplyFrom = null;
1236 Rfc822Token[] tokens;
1237 HashSet<String> recipientsMap = new HashSet<String>();
1238 for (String address : sentTo) {
1239 tokens = Rfc822Tokenizer.tokenize(address);
Tony Mantler581edd42014-02-18 15:41:22 -08001240 for (final Rfc822Token token : tokens) {
1241 recipientsMap.add(token.getAddress());
Mindy Pereira62de1b12012-04-06 12:17:56 -07001242 }
1243 }
1244
1245 int matchingAddressCount = 0;
1246 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001247 customFroms = account.getReplyFroms();
1248 if (customFroms != null) {
1249 for (ReplyFromAccount entry : customFroms) {
1250 if (recipientsMap.contains(entry.address)) {
1251 matchingReplyFrom = entry;
1252 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001253 }
1254 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001255 }
1256 if (matchingAddressCount > 1) {
1257 matchingReplyFrom = getDefaultReplyFromAccount(account);
1258 }
1259 return matchingReplyFrom;
1260 }
1261
Scott Kennedyff8553f2013-04-05 20:57:44 -07001262 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1263 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001264 if (from.isDefault) {
1265 return from;
1266 }
1267 }
Tony Mantlerf441d142013-10-22 11:46:00 -07001268 return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1269 account.getSenderName(), account.getEmailAddress(), true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001270 }
1271
Tony Mantlerf441d142013-10-22 11:46:00 -07001272 private ReplyFromAccount getReplyFromAccountFromDraft(final Account account,
1273 final Message msg) {
1274 final Address[] draftFroms = Address.parse(msg.getFrom());
1275 final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
Mindy Pereira62de1b12012-04-06 12:17:56 -07001276 ReplyFromAccount replyFromAccount = null;
1277 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Tony Mantler79b11562013-10-09 15:31:50 -07001278 if (TextUtils.equals(account.getEmailAddress(), sender)) {
Tony Mantlerf441d142013-10-22 11:46:00 -07001279 replyFromAccount = getDefaultReplyFromAccount(account);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001280 } else {
1281 for (ReplyFromAccount fromAccount : replyFromAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -07001282 if (TextUtils.equals(fromAccount.address, sender)) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001283 replyFromAccount = fromAccount;
1284 break;
1285 }
1286 }
1287 }
1288 return replyFromAccount;
1289 }
1290
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001291 private void findViews() {
Jin Cao36e23872014-07-29 13:41:12 -07001292 mScrollView = (ScrollView) findViewById(R.id.compose);
1293 mScrollView.setVisibility(View.VISIBLE);
Jin Cao9d358a12014-07-24 12:15:38 -07001294 mCcBccButton = findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001295 if (mCcBccButton != null) {
1296 mCcBccButton.setOnClickListener(this);
1297 }
1298 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001299 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira818143e2012-01-11 13:59:49 -08001300 mTo = (RecipientEditTextView) findViewById(R.id.to);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001301 initializeRecipientEditTextView(mTo);
Jin Cao15f09d72014-08-08 13:27:34 -07001302 mTo.setAlternatePopupAnchor(findViewById(R.id.compose_to_dropdown_anchor));
Mindy Pereira818143e2012-01-11 13:59:49 -08001303 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001304 initializeRecipientEditTextView(mCc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001305 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001306 initializeRecipientEditTextView(mBcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001307 // TODO: add special chips text change watchers before adding
1308 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001309 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001310 mSubject.setOnEditorActionListener(this);
Jin Caoc5c550a2014-07-29 11:53:17 -07001311 mSubject.setOnFocusChangeListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001312 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1313 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001314 mBodyView = (EditText) findViewById(R.id.body);
Jin Caoc5c550a2014-07-29 11:53:17 -07001315 mBodyView.setOnFocusChangeListener(this);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001316 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001317 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001318 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001319 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001320 }
1321
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001322 private void initializeRecipientEditTextView(RecipientEditTextView view) {
1323 view.setTokenizer(new Rfc822Tokenizer());
1324 view.setThreshold(COMPLETION_THRESHOLD);
1325 }
1326
mindyp62d3ec72012-08-24 13:04:09 -07001327 @Override
1328 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1329 if (action == EditorInfo.IME_ACTION_DONE) {
1330 focusBody();
1331 return true;
1332 }
1333 return false;
1334 }
1335
Andy Huang91ede362014-01-21 19:16:00 -08001336 /**
1337 * Convert the body text (in {@link Spanned} form) to ready-to-send HTML format as a plain
1338 * String.
1339 *
1340 * @param body the body text including fancy style spans
Jin Cao77b4c2c2014-05-20 13:55:53 -07001341 * @param removedComposing whether the function already removed composingSpans. Necessary
1342 * because we cannot call removeComposingSpans from a background thread.
Andy Huang91ede362014-01-21 19:16:00 -08001343 * @return HTML formatted body that's suitable for sending or saving
1344 */
Jin Cao77b4c2c2014-05-20 13:55:53 -07001345 private String spannedBodyToHtml(Spanned body, boolean removedComposing) {
1346 if (!removedComposing) {
1347 body = removeComposingSpans(body);
1348 }
1349 final HtmlifyBeginResult r = onHtmlifyBegin(body);
Andy Huang91ede362014-01-21 19:16:00 -08001350 return onHtmlifyEnd(Html.toHtml(r.result), r.extras);
1351 }
1352
1353 /**
1354 * A hook for subclasses to convert custom spans in the body text prior to system HTML
1355 * conversion. That HTML conversion is lossy, so anything above and beyond its capability
1356 * has to be handled here.
1357 *
1358 * @param body
1359 * @return a copy of the body text with custom spans replaced with HTML
1360 */
1361 protected HtmlifyBeginResult onHtmlifyBegin(Spanned body) {
1362 return new HtmlifyBeginResult(body, null /* extras */);
1363 }
1364
1365 protected String onHtmlifyEnd(String html, Object extras) {
1366 return html;
1367 }
1368
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001369 protected TextView getBody() {
1370 return mBodyView;
1371 }
1372
1373 @VisibleForTesting
Andy Huang0a2a3462013-12-20 15:56:13 -08001374 public String getBodyHtml() {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001375 return spannedBodyToHtml(mBodyView.getText(), false);
Andy Huang0a2a3462013-12-20 15:56:13 -08001376 }
1377
1378 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001379 public Account getFromAccount() {
1380 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1381 mReplyFromAccount.account : mAccount;
1382 }
1383
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001384 private void clearChangeListeners() {
1385 mSubject.removeTextChangedListener(this);
1386 mBodyView.removeTextChangedListener(this);
1387 mTo.removeTextChangedListener(mToListener);
1388 mCc.removeTextChangedListener(mCcListener);
1389 mBcc.removeTextChangedListener(mBccListener);
1390 mFromSpinner.setOnAccountChangedListener(null);
1391 mAttachmentsView.setAttachmentChangesListener(null);
1392 }
1393
Mindy Pereira75f66632012-01-11 11:42:02 -08001394 // Now that the message has been initialized from any existing draft or
1395 // ref message data, set up listeners for any changes that occur to the
1396 // message.
1397 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001398 // Make sure we only add text changed listeners once!
1399 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001400 mSubject.addTextChangedListener(this);
1401 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001402 if (mToListener == null) {
1403 mToListener = new RecipientTextWatcher(mTo, this);
1404 }
1405 mTo.addTextChangedListener(mToListener);
1406 if (mCcListener == null) {
1407 mCcListener = new RecipientTextWatcher(mCc, this);
1408 }
1409 mCc.addTextChangedListener(mCcListener);
1410 if (mBccListener == null) {
1411 mBccListener = new RecipientTextWatcher(mBcc, this);
1412 }
1413 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001414 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001415 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001416 }
1417
Alice Yanga990a712013-03-13 18:37:00 -07001418 private void initActionBar() {
1419 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001420 final ActionBar actionBar = getSupportActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001421 if (actionBar == null) {
1422 return;
1423 }
Alice Yanga990a712013-03-13 18:37:00 -07001424 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001425 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1426 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001427 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001428 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001429 if (mComposeModeAdapter == null) {
Jin Caof7461632014-08-11 15:21:43 -07001430 mComposeModeAdapter = new ComposeModeAdapter(actionBar.getThemedContext());
Mindy Pereira326c6602012-01-04 15:32:42 -08001431 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001432 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1433 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001434 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001435 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001436 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001437 break;
1438 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001439 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001440 break;
1441 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001442 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001443 break;
1444 }
1445 }
Paul Westbrook4def3bf2014-07-01 00:38:17 -07001446 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
1447 ActionBar.DISPLAY_HOME_AS_UP);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001448 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001449 }
1450
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001451 private void initFromRefMessage(int action) {
1452 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001453
1454 // Check if To: address and email body needs to be prefilled based on extras.
1455 // This is used for reporting rendering feedback.
1456 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1457 Intent intent = getIntent();
1458 if (intent.getExtras() != null) {
1459 String toAddresses = intent.getStringExtra(EXTRA_TO);
1460 if (toAddresses != null) {
1461 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1462 }
1463 String body = intent.getStringExtra(EXTRA_BODY);
1464 if (body != null) {
1465 setBody(body, false /* withSignature */);
1466 }
1467 }
1468 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001469 }
1470
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001471 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001472 setSubject(mRefMessage, action);
1473 // Setup recipients
1474 if (action == FORWARD) {
1475 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001476 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001477 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001478 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001479 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1480 initAttachments(mRefMessage);
1481 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001482 }
1483
Andy Huang9ed742c2014-06-18 02:34:50 -07001484 protected HtmlTree.Converter<Spanned> getSpanConverter() {
1485 return new HtmlUtils.SpannedConverter();
1486 }
1487
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001488 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001489 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001490
1491 mDraft = message;
1492 mDraftId = message.id;
1493 mSubject.setText(message.subject);
1494 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001495 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001496 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001497 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1498 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001499 if (message.hasAttachments) {
1500 List<Attachment> attachments = message.getAttachments();
1501 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001502 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001503 }
1504 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07001505 int quotedTextIndex = message.appendRefMessageContent ? message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001506 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001507 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001508 if (!TextUtils.isEmpty(message.bodyHtml)) {
Jin Cao32f453b2014-07-22 14:21:15 -07001509 String body = message.bodyHtml;
Mindy Pereira002ff522012-05-30 10:31:26 -07001510 if (quotedTextIndex > -1) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07001511 // Find the offset in the html text of the actual quoted text and strip it out.
1512 // Note that the actual quotedTextOffset in the message has not changed as
1513 // this different offset is used only for display purposes. They point to different
1514 // parts of the original message. Please see the comments in QuoteTextView
1515 // to see the differences.
Mindy Pereira752222d2012-07-19 09:58:53 -07001516 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1517 if (quotedTextIndex > -1) {
Jin Cao32f453b2014-07-22 14:21:15 -07001518 body = message.bodyHtml.substring(0, quotedTextIndex);
Mindy Pereira752222d2012-07-19 09:58:53 -07001519 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1520 message.bodyHtml.length());
1521 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001522 }
Jin Cao32f453b2014-07-22 14:21:15 -07001523 new HtmlToSpannedTask().execute(body);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001524 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001525 final String body = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07001526 final CharSequence bodyText;
1527 if (TextUtils.isEmpty(body)) {
1528 bodyText = "";
1529 quotedText = null;
1530 } else {
1531 if (quotedTextIndex > body.length()) {
1532 // Sanity check to guarantee that we will not over index the String.
1533 // If this happens there is a bigger problem. This should never happen hence
1534 // the wtf logging.
1535 quotedTextIndex = -1;
1536 LogUtils.wtf(LOG_TAG, "quotedTextIndex (%d) > body.length() (%d)",
1537 quotedTextIndex, body.length());
1538 }
1539 bodyText = quotedTextIndex > -1 ? body.substring(0, quotedTextIndex) : body;
1540 if (quotedTextIndex > -1) {
1541 quotedText = body.substring(quotedTextIndex);
1542 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001543 }
1544 mBodyView.setText(bodyText);
1545 }
1546 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001547 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001548 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001549 }
1550
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001551 /**
1552 * Fill all the widgets with the content found in the Intent Extra, if any.
1553 * Also apply the same style to all widgets. Note: if initFromExtras is
1554 * called as a result of switching between reply, reply all, and forward per
1555 * the latest revision of Gmail, and the user has already made changes to
1556 * attachments on a previous incarnation of the message (as a reply, reply
1557 * all, or forward), the original attachments from the message will not be
1558 * re-instantiated. The user's changes will be respected. This follows the
1559 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001560 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001561 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001562 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001563 // If we were invoked with a SENDTO intent, the value
1564 // should take precedence
1565 final Uri dataUri = intent.getData();
1566 if (dataUri != null) {
1567 if (MAIL_TO.equals(dataUri.getScheme())) {
1568 initFromMailTo(dataUri.toString());
1569 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001570 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001571 String toText = dataUri.getSchemeSpecificPart();
1572 if (toText != null) {
1573 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001574 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001575 }
1576 }
1577 }
1578 }
1579
1580 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1581 if (extraStrings != null) {
1582 addToAddresses(Arrays.asList(extraStrings));
1583 }
1584 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1585 if (extraStrings != null) {
1586 addCcAddresses(Arrays.asList(extraStrings), null);
1587 }
1588 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1589 if (extraStrings != null) {
1590 addBccAddresses(Arrays.asList(extraStrings));
1591 }
1592
1593 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1594 if (extraString != null) {
1595 mSubject.setText(extraString);
1596 }
1597
1598 for (String extra : ALL_EXTRAS) {
1599 if (intent.hasExtra(extra)) {
1600 String value = intent.getStringExtra(extra);
1601 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001602 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001603 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001604 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001605 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001606 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001607 } else if (EXTRA_SUBJECT.equals(extra)) {
1608 mSubject.setText(value);
1609 } else if (EXTRA_BODY.equals(extra)) {
1610 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001611 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1612 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001613 }
1614 }
1615 }
1616
1617 Bundle extras = intent.getExtras();
1618 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001619 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
Jin Caoa8f34ff2014-07-24 14:43:57 -07001620 setBody((text != null) ? text : "", true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001621
1622 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001623 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001624
1625 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1626 if (mExtraValues != null) {
1627 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1628 initExtraValues(mExtraValues);
1629 return true;
1630 }
1631
1632 return false;
1633 }
1634
1635 protected void initExtraValues(ContentValues extraValues) {
1636 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001637 }
1638
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001639
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001640 @VisibleForTesting
1641 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001642 // TODO: handle the case where there are spaces in the display name as
1643 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1644 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001645 // Since URLDecode.decode changes + into ' ', and + is a valid
1646 // email character, we need to find/ replace these ourselves before
1647 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001648 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001649 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001650 } catch (IllegalArgumentException e) {
1651 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1652 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1653 } else {
1654 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1655 }
1656 return null;
1657 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001658 }
1659
1660 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001661 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1662 * changing '+' into ' '
1663 *
1664 * @param toReplace Input string
1665 * @return The string with all "+" characters replaced with "%2B"
1666 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001667 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001668 return toReplace.replace("+", "%2B");
1669 }
1670
1671 /**
Jin Caod67d7e32014-03-26 16:49:48 -07001672 * Replaces all occurrences of '%' with "%25", to prevent URLDecode.decode from
1673 * crashing on decoded '%' symbols
1674 *
1675 * @param toReplace Input string
1676 * @return The string with all "%" characters replaced with "%25"
1677 */
1678 private static String replacePercent(String toReplace) {
1679 return toReplace.replace("%", "%25");
1680 }
1681
1682 /**
1683 * Helper function to encapsulate encoding/decoding string from Uri.getQueryParameters
1684 * @param content Input string
1685 * @return The string that's properly escaped to be shown in mail subject/content
1686 */
1687 private static String decodeContentFromQueryParam(String content) {
1688 try {
1689 return URLDecoder.decode(replacePlus(replacePercent(content)), UTF8_ENCODING_NAME);
1690 } catch (UnsupportedEncodingException e) {
1691 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), content);
1692 return ""; // Default to empty string so setText/setBody has same behavior as before.
1693 }
1694 }
1695
1696 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001697 * Initialize the compose view from a String representing a mailTo uri.
1698 * @param mailToString The uri as a string.
1699 */
1700 public void initFromMailTo(String mailToString) {
1701 // We need to disguise this string as a URI in order to parse it
1702 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1703 Uri uri = Uri.parse("foo://" + mailToString);
1704 int index = mailToString.indexOf("?");
1705 int length = "mailto".length() + 1;
1706 String to;
1707 try {
1708 // Extract the recipient after mailto:
1709 if (index == -1) {
1710 to = decodeEmailInUri(mailToString.substring(length));
1711 } else {
1712 to = decodeEmailInUri(mailToString.substring(length, index));
1713 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001714 if (!TextUtils.isEmpty(to)) {
1715 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1716 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001717 } catch (UnsupportedEncodingException e) {
1718 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1719 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1720 } else {
1721 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1722 }
1723 }
1724
1725 List<String> cc = uri.getQueryParameters("cc");
1726 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1727
1728 List<String> otherTo = uri.getQueryParameters("to");
1729 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1730
1731 List<String> bcc = uri.getQueryParameters("bcc");
1732 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1733
Jin Caod67d7e32014-03-26 16:49:48 -07001734 // NOTE: Uri.getQueryParameters already decodes % encoded characters
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001735 List<String> subject = uri.getQueryParameters("subject");
1736 if (subject.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001737 mSubject.setText(decodeContentFromQueryParam(subject.get(0)));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001738 }
1739
1740 List<String> body = uri.getQueryParameters("body");
1741 if (body.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001742 setBody(decodeContentFromQueryParam(body.get(0)), true /* with signature */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001743 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001744 }
1745
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001746 @VisibleForTesting
1747 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001748 addAttachments(refMessage.getAttachments());
1749 }
1750
1751 public long addAttachments(List<Attachment> attachments) {
1752 long size = 0;
1753 AttachmentFailureException error = null;
1754 for (Attachment a : attachments) {
1755 try {
1756 size += mAttachmentsView.addAttachment(mAccount, a);
1757 } catch (AttachmentFailureException e) {
1758 error = e;
1759 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001760 }
Mark Wei434f2942012-08-24 11:54:02 -07001761 if (error != null) {
1762 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1763 if (attachments.size() > 1) {
1764 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1765 } else {
1766 showAttachmentTooBigToast(error.getErrorRes());
1767 }
1768 }
1769 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001770 }
1771
1772 /**
1773 * When an attachment is too large to be added to a message, show a toast.
1774 * This method also updates the position of the toast so that it is shown
1775 * clearly above they keyboard if it happens to be open.
1776 */
Mark Wei434f2942012-08-24 11:54:02 -07001777 private void showAttachmentTooBigToast(int errorRes) {
1778 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1779 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1780 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001781 }
1782
Mark Wei434f2942012-08-24 11:54:02 -07001783 private void showErrorToast(String message) {
1784 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1785 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001786 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1787 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1788 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001789 }
1790
Paul Westbrookf97588b2012-03-20 11:11:37 -07001791 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001792 Bundle extras = intent.getExtras();
1793 if (extras == null) {
1794 extras = Bundle.EMPTY;
1795 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001796 final String action = intent.getAction();
1797 if (!mAttachmentsChanged) {
1798 long totalSize = 0;
1799 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1800 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1801 for (String uriString : uris) {
1802 final Uri uri = Uri.parse(uriString);
1803 long size = 0;
1804 try {
Andy Huang91ede362014-01-21 19:16:00 -08001805 if (handleSpecialAttachmentUri(uri)) {
1806 continue;
1807 }
1808
Andy Huange003b4c2013-08-16 10:32:05 -07001809 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1810 size = mAttachmentsView.addAttachment(mAccount, a);
1811
1812 Analytics.getInstance().sendEvent("send_intent_attachment",
1813 Utils.normalizeMimeType(a.getContentType()), null, size);
1814
Paul Westbrookf97588b2012-03-20 11:11:37 -07001815 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001816 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001817 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001818 }
1819 totalSize += size;
1820 }
1821 }
mindyp9a9e8d62012-10-03 12:24:07 -07001822 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1823 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
Andy Huang91ede362014-01-21 19:16:00 -08001824 final ArrayList<Uri> uris = extras
mindyp9a9e8d62012-10-03 12:24:07 -07001825 .getParcelableArrayList(Intent.EXTRA_STREAM);
1826 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
Andy Huang91ede362014-01-21 19:16:00 -08001827 for (Uri uri : uris) {
Andy Huang1a438aa2014-06-03 19:04:06 -07001828 if (uri == null) {
Tony Mantler1877c5f2014-05-12 16:02:16 -07001829 continue;
1830 }
mindyp9a9e8d62012-10-03 12:24:07 -07001831 try {
Andy Huang91ede362014-01-21 19:16:00 -08001832 if (handleSpecialAttachmentUri(uri)) {
1833 continue;
1834 }
1835
1836 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
Andy Huange003b4c2013-08-16 10:32:05 -07001837 attachments.add(a);
1838
1839 Analytics.getInstance().sendEvent("send_intent_attachment",
1840 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1841
mindyp9a9e8d62012-10-03 12:24:07 -07001842 } catch (AttachmentFailureException e) {
1843 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1844 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1845 getApplicationContext(),
1846 mAccount.settings.getMaxAttachmentSize());
1847 showErrorToast(getString
1848 (R.string.generic_attachment_problem, maxSize));
1849 }
1850 }
1851 totalSize += addAttachments(attachments);
1852 } else {
Tony Mantler581edd42014-02-18 15:41:22 -08001853 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
Andy Huang1a438aa2014-06-03 19:04:06 -07001854 if (uri != null) {
Tony Mantler1877c5f2014-05-12 16:02:16 -07001855 long size = 0;
1856 try {
Andy Huang1a438aa2014-06-03 19:04:06 -07001857 if (!handleSpecialAttachmentUri(uri)) {
1858 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1859 size = mAttachmentsView.addAttachment(mAccount, a);
Andy Huange003b4c2013-08-16 10:32:05 -07001860
Andy Huang1a438aa2014-06-03 19:04:06 -07001861 Analytics.getInstance().sendEvent("send_intent_attachment",
1862 Utils.normalizeMimeType(a.getContentType()), null, size);
1863 }
Andy Huange003b4c2013-08-16 10:32:05 -07001864
Tony Mantler1877c5f2014-05-12 16:02:16 -07001865 } catch (AttachmentFailureException e) {
1866 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1867 showAttachmentTooBigToast(e.getErrorRes());
1868 }
1869 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001870 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001871 }
1872 }
1873
1874 if (totalSize > 0) {
1875 mAttachmentsChanged = true;
1876 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001877
1878 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1879 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001880 }
1881 }
1882 }
1883
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001884 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001885 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1886 mShowQuotedText = true;
1887 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001888
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001889 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1890 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001891 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1892 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001893 }
1894
1895 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001896 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001897 boolean ccVisible = mCcBccView.isCcVisible();
1898 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001899 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001900 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001901 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001902 } else {
Jin Cao9d358a12014-07-24 12:15:38 -07001903 mCcBccButton.setVisibility(View.GONE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001904 }
1905 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001906 }
1907
Mindy Pereira013194c2012-01-06 15:09:33 -08001908 /**
1909 * Add attachment and update the compose area appropriately.
Mindy Pereira013194c2012-01-06 15:09:33 -08001910 */
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001911 private void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07001912 if (data == null) {
1913 return;
1914 }
1915
1916 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1917 final ClipData clipData = data.getClipData();
1918 if (clipData != null) {
1919 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1920 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1921 }
1922 return;
1923 }
1924 }
1925
1926 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07001927 }
1928
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001929 private void addAttachmentAndUpdateView(Uri contentUri) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001930 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001931 return;
1932 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001933 try {
Andy Huang91ede362014-01-21 19:16:00 -08001934
1935 if (handleSpecialAttachmentUri(contentUri)) {
1936 return;
1937 }
1938
Andy Huang5c5fd572012-04-08 18:19:29 -07001939 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1940 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001941 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001942 showErrorToast(getResources().getString(
1943 e.getErrorRes(),
1944 AttachmentUtils.convertToHumanReadableSize(
1945 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001946 }
1947 }
1948
Andy Huang91ede362014-01-21 19:16:00 -08001949 /**
1950 * Allow subclasses to implement custom handling of attachments.
1951 *
1952 * @param contentUri a passed-in URI from a pick intent
1953 * @return true iff handled
1954 */
1955 protected boolean handleSpecialAttachmentUri(final Uri contentUri) {
1956 return false;
1957 }
1958
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001959 private void addAttachmentAndUpdateView(Attachment attachment) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001960 try {
Mark Wei434f2942012-08-24 11:54:02 -07001961 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001962 if (size > 0) {
1963 mAttachmentsChanged = true;
1964 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001965 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001966 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001967 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001968 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001969 }
1970 }
1971
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001972 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001973 // Don't populate the address if this is a forward.
1974 if (action == ComposeActivity.FORWARD) {
1975 return;
1976 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001977 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001978 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001979
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001980 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1981 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001982 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001983 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001984 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001985 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001986 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1987 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
Andy Huange2af8872014-01-16 12:36:27 -08001988 final String[] replyToAddresses = getReplyToAddresses(
1989 refMessage.getReplyToAddressesUnescaped(), fromAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001990
1991 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1992 // Cc list is the union of the To and Cc recipients of the original
1993 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001994 // already on the To list.
1995 if (action == ComposeActivity.REPLY) {
Tony Mantler24f116f2014-01-16 10:20:50 -08001996 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001997 addToAddresses(toAddresses);
1998 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001999 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002000 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08002001 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07002002 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07002003 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002004 addCcAddresses(ccAddresses, toAddresses);
2005 }
2006 }
2007
Andy Huange2af8872014-01-16 12:36:27 -08002008 // If there is no reply to address, the reply to address is the sender.
2009 private static String[] getReplyToAddresses(String[] replyTo, String from) {
2010 boolean hasReplyTo = false;
2011 for (final String replyToAddress : replyTo) {
2012 if (!TextUtils.isEmpty(replyToAddress)) {
2013 hasReplyTo = true;
2014 }
2015 }
2016 return hasReplyTo ? replyTo : new String[] {from};
2017 }
2018
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002019 private void addToAddresses(Collection<String> addresses) {
2020 addAddressesToList(addresses, mTo);
2021 }
2022
2023 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002024 addCcAddressesToList(tokenizeAddressList(addresses),
2025 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002026 }
2027
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07002028 private void addBccAddresses(Collection<String> addresses) {
2029 addAddressesToList(addresses, mBcc);
2030 }
2031
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002032 @VisibleForTesting
2033 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
2034 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
2035 String address;
2036
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002037 if (compareToList == null) {
Tony Mantler581edd42014-02-18 15:41:22 -08002038 for (final Rfc822Token[] tokens : addresses) {
2039 for (final Rfc822Token token : tokens) {
2040 address = token.toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002041 list.append(address + END_TOKEN);
2042 }
2043 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002044 } else {
2045 HashSet<String> compareTo = convertToHashSet(compareToList);
Tony Mantler581edd42014-02-18 15:41:22 -08002046 for (final Rfc822Token[] tokens : addresses) {
2047 for (final Rfc822Token token : tokens) {
2048 address = token.toString();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002049 // Check if this is a duplicate:
Tony Mantler581edd42014-02-18 15:41:22 -08002050 if (!compareTo.contains(token.getAddress())) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002051 // Get the address here
2052 list.append(address + END_TOKEN);
2053 }
2054 }
2055 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002056 }
2057 }
2058
Scott Kennedyff8553f2013-04-05 20:57:44 -07002059 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
2060 final HashSet<String> hash = new HashSet<String>();
2061 for (final Rfc822Token[] tokens : list) {
Tony Mantler581edd42014-02-18 15:41:22 -08002062 for (final Rfc822Token token : tokens) {
2063 hash.add(token.getAddress());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002064 }
2065 }
2066 return hash;
2067 }
2068
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002069 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
2070 @VisibleForTesting
2071 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
2072
2073 for (String address: addresses) {
2074 tokenized.add(Rfc822Tokenizer.tokenize(address));
2075 }
2076 return tokenized;
2077 }
2078
2079 @VisibleForTesting
2080 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
2081 for (String address : addresses) {
2082 addAddressToList(address, list);
2083 }
2084 }
2085
Scott Kennedyff8553f2013-04-05 20:57:44 -07002086 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002087 if (address == null || list == null)
2088 return;
2089
Scott Kennedyff8553f2013-04-05 20:57:44 -07002090 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002091
Tony Mantler581edd42014-02-18 15:41:22 -08002092 for (final Rfc822Token token : tokens) {
2093 list.append(token + END_TOKEN);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002094 }
2095 }
2096
2097 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07002098 protected Collection<String> initToRecipients(final String fullSenderAddress,
Tony Mantler24f116f2014-01-16 10:20:50 -08002099 final String[] replyToAddresses, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002100 // The To recipient is the reply-to address specified in the original
2101 // message, unless it is:
2102 // the current user OR a custom from of the current user, in which case
2103 // it's the To recipient list of the original message.
2104 // OR missing, in which case use the sender of the original message
2105 Set<String> toAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002106 for (final String replyToAddress : replyToAddresses) {
2107 if (!TextUtils.isEmpty(replyToAddress)
2108 && !recipientMatchesThisAccount(replyToAddress)) {
2109 toAddresses.add(replyToAddress);
2110 }
2111 }
2112 if (toAddresses.size() == 0) {
mindyp65b06f52012-11-21 10:35:08 -08002113 // In this case, the user is replying to a message in which their
Tony Mantler24f116f2014-01-16 10:20:50 -08002114 // current account or some of their custom from addresses are the only
2115 // recipients and they sent the original message.
mindyp65b06f52012-11-21 10:35:08 -08002116 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
2117 && recipientMatchesThisAccount(inToAddresses[0])) {
2118 toAddresses.add(inToAddresses[0]);
2119 return toAddresses;
2120 }
2121 // This happens if the user replies to a message they originally
2122 // wrote. In this case, "reply" really means "re-send," so we
2123 // target the original recipients. This works as expected even
2124 // if the user sent the original message to themselves.
2125 for (String address : inToAddresses) {
2126 if (!recipientMatchesThisAccount(address)) {
2127 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08002128 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07002129 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002130 }
2131 return toAddresses;
2132 }
2133
Scott Kennedyff8553f2013-04-05 20:57:44 -07002134 private void addRecipients(final Set<String> recipients, final String[] addresses) {
2135 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002136 // Do not add this account, or any of its custom from addresses, to
2137 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08002138 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08002139 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002140 recipients.add(email.replace("\"\"", ""));
2141 }
2142 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002143 }
2144
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002145 /**
2146 * A recipient matches this account if it has the same address as the
2147 * currently selected account OR one of the custom from addresses associated
2148 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002149 * @param recipientAddress address we are comparing with the currently selected account
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002150 */
mindyp5ee5d692012-11-19 16:02:16 -08002151 protected boolean recipientMatchesThisAccount(String recipientAddress) {
2152 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08002153 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002154 }
2155
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002156 /**
2157 * Returns a formatted subject string with the appropriate prefix for the action type.
2158 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
2159 */
Andrew Sapperstein7e04f142014-06-11 13:43:07 -07002160 public static String buildFormattedSubject(Resources res, String subject, int action) {
Tony Mantler41c3a252014-06-30 11:00:43 -07002161 final String prefix;
2162 final String correctedSubject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002163 if (action == ComposeActivity.COMPOSE) {
2164 prefix = "";
2165 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002166 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002167 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002168 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002169 }
2170
Tony Mantler41c3a252014-06-30 11:00:43 -07002171 if (TextUtils.isEmpty(subject)) {
2172 correctedSubject = prefix;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002173 } else {
Tony Mantler41c3a252014-06-30 11:00:43 -07002174 // Don't duplicate the prefix
2175 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
2176 correctedSubject = subject;
2177 } else {
2178 correctedSubject = String.format(
2179 res.getString(R.string.formatted_subject), prefix, subject);
2180 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002181 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002182
2183 return correctedSubject;
2184 }
2185
2186 private void setSubject(Message refMessage, int action) {
2187 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002188 }
2189
Mindy Pereira818143e2012-01-11 13:59:49 -08002190 private void initRecipients() {
2191 setupRecipients(mTo);
2192 setupRecipients(mCc);
2193 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002194 }
2195
Mindy Pereira818143e2012-01-11 13:59:49 -08002196 private void setupRecipients(RecipientEditTextView view) {
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002197 final DropdownChipLayouter layouter = getDropdownChipLayouter();
2198 if (layouter != null) {
2199 view.setDropdownChipLayouter(layouter);
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002200 }
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002201 view.setAdapter(getRecipientAdapter());
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002202 view.setRecipientEntryItemClickedListener(this);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002203 if (mValidator == null) {
Tony Mantler79b11562013-10-09 15:31:50 -07002204 final String accountName = mAccount.getEmailAddress();
Mindy Pereira33fe9082012-01-09 16:24:30 -08002205 int offset = accountName.indexOf("@") + 1;
2206 String account = accountName;
Tony Mantler79b11562013-10-09 15:31:50 -07002207 if (offset > 0) {
2208 account = account.substring(offset);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002209 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002210 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002211 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002212 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002213 }
2214
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002215 /**
2216 * Derived classes should override if they wish to provide their own autocomplete behavior.
2217 */
2218 public BaseRecipientAdapter getRecipientAdapter() {
2219 return new RecipientAdapter(this, mAccount);
2220 }
2221
2222 /**
2223 * Derived classes should override this to provide their own dropdown behavior.
2224 * If the result is null, the default {@link com.android.ex.chips.DropdownChipLayouter}
2225 * is used.
2226 */
2227 public DropdownChipLayouter getDropdownChipLayouter() {
2228 return null;
2229 }
2230
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002231 @Override
2232 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002233 final int id = v.getId();
2234 if (id == R.id.add_cc_bcc) {
2235 // Verify that cc/ bcc aren't showing.
2236 // Animate in cc/bcc.
2237 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002238 }
2239 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002240
2241 @Override
Jin Caoc5c550a2014-07-29 11:53:17 -07002242 public void onFocusChange (View v, boolean hasFocus) {
2243 final int id = v.getId();
2244 if (hasFocus && (id == R.id.subject || id == R.id.body)) {
2245 // Collapse cc/bcc iff both are empty
2246 final boolean showCcBccFields = !TextUtils.isEmpty(mCc.getText()) ||
2247 !TextUtils.isEmpty(mBcc.getText());
2248 mCcBccView.show(false /* animate */, showCcBccFields, showCcBccFields);
Jin Cao36e23872014-07-29 13:41:12 -07002249 mCcBccButton.setVisibility(showCcBccFields ? View.GONE : View.VISIBLE);
2250
2251 // On phones autoscroll down so that Cc aligns to the top if we are showing cc/bcc.
2252 if (getResources().getBoolean(R.bool.auto_scroll_cc) && showCcBccFields) {
2253 final int[] coords = new int[2];
2254 mCc.getLocationOnScreen(coords);
2255
2256 // Subtract status bar and action bar height from y-coord.
2257 final Rect rect = new Rect();
2258 getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07002259 final int deltaY = coords[1] - getSupportActionBar().getHeight() - rect.top;
Jin Cao36e23872014-07-29 13:41:12 -07002260
2261 // Only scroll down
2262 if (deltaY > 0) {
2263 mScrollView.smoothScrollBy(0, deltaY);
2264 }
2265 }
Jin Caoc5c550a2014-07-29 11:53:17 -07002266 }
2267 }
2268
2269 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002270 public boolean onCreateOptionsMenu(Menu menu) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002271 final boolean superCreated = super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07002272 // Don't render any menu items when there are no accounts.
2273 if (mAccounts == null || mAccounts.length == 0) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002274 return superCreated;
Mindy Pereirab199d172012-08-13 11:04:03 -07002275 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002276 MenuInflater inflater = getMenuInflater();
2277 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08002278
2279 /*
2280 * Start save in the correct enabled state.
2281 * 1) If a user launches compose from within gmail, save is disabled
2282 * until they add something, at which point, save is enabled, auto save
2283 * on exit; if the user empties everything, save is disabled, exiting does not
2284 * auto-save
2285 * 2) if a user replies/ reply all/ forwards from within gmail, save is
2286 * disabled until they change something, at which point, save is
2287 * enabled, auto save on exit; if the user empties everything, save is
2288 * disabled, exiting does not auto-save.
2289 * 3) If a user launches compose from another application and something
2290 * gets populated (attachments, recipients, body, subject, etc), save is
2291 * enabled, auto save on exit; if the user empties everything, save is
2292 * disabled, exiting does not auto-save
2293 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08002294 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08002295 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07002296 enableSave(mInnerSavedState != null ?
2297 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08002298 : (Intent.ACTION_SEND.equals(action)
2299 || Intent.ACTION_SEND_MULTIPLE.equals(action)
2300 || Intent.ACTION_SENDTO.equals(action)
2301 || shouldSave()));
2302
Greg Bullockd47a7042014-08-13 16:02:59 +02002303 final MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2304 final MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2305 final MenuItem attachFromServiceItem = menu.findItem(R.id.attach_from_service_stub1);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07002306 if (helpItem != null) {
2307 helpItem.setVisible(mAccount != null
2308 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2309 }
2310 if (sendFeedbackItem != null) {
2311 sendFeedbackItem.setVisible(mAccount != null
2312 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2313 }
Greg Bullockd47a7042014-08-13 16:02:59 +02002314 if (attachFromServiceItem != null) {
2315 attachFromServiceItem.setVisible(shouldEnableAttachFromServiceMenu(mAccount));
2316 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002317
Andrew Sapperstein8809f9f2013-10-11 16:13:35 -07002318 // Show attach picture on pre-K devices.
2319 menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002320
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002321 return true;
2322 }
2323
2324 @Override
2325 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002326 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002327
Andy Huangf8c59b02014-03-19 20:00:53 -07002328 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id,
2329 "compose", 0);
Andy Huangdc97bf42013-08-15 16:52:45 -07002330
Mindy Pereira75f66632012-01-11 11:42:02 -08002331 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002332 if (id == R.id.add_file_attachment) {
2333 doAttach(MIME_TYPE_ALL);
2334 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002335 doAttach(MIME_TYPE_PHOTO);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002336 } else if (id == R.id.save) {
2337 doSave(true);
2338 } else if (id == R.id.send) {
2339 doSend();
2340 } else if (id == R.id.discard) {
2341 doDiscard();
2342 } else if (id == R.id.settings) {
2343 Utils.showSettings(this, mAccount);
2344 } else if (id == android.R.id.home) {
2345 onAppUpPressed();
2346 } else if (id == R.id.help_info_menu_item) {
2347 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002348 } else {
2349 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002350 }
Tony Mantler581edd42014-02-18 15:41:22 -08002351 return handled || super.onOptionsItemSelected(item);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002352 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002353
Mindy Pereirab199d172012-08-13 11:04:03 -07002354 @Override
2355 public void onBackPressed() {
2356 // If we are showing the wait fragment, just exit.
2357 if (getWaitFragment() != null) {
2358 finish();
2359 } else {
2360 super.onBackPressed();
2361 }
2362 }
2363
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002364 /**
2365 * Carries out the "up" action in the action bar.
2366 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002367 private void onAppUpPressed() {
2368 if (mLaunchedFromEmail) {
2369 // If this was started from Gmail, simply treat app up as the system back button, so
2370 // that the last view is restored.
2371 onBackPressed();
2372 return;
2373 }
2374
2375 // Fire the main activity to ensure it launches the "top" screen of mail.
2376 // Since the main Activity is singleTask, it should revive that task if it was already
2377 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002378 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002379 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2380 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2381 startActivity(mailIntent);
2382 finish();
2383 }
2384
Mindy Pereira33fe9082012-01-09 16:24:30 -08002385 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002386 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002387 logSendOrSave(false /* save */);
2388 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002389 }
2390
Mindy Pereira48e31b02012-05-30 13:12:24 -07002391 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002392 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002393 }
2394
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002395 @Override
2396 public void onRecipientEntryItemClicked(int charactersTyped, int position) {
2397 // Send analytics of characters typed and position in dropdown selected.
2398 Analytics.getInstance().sendEvent(
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002399 "suggest_click", Integer.toString(charactersTyped), Integer.toString(position), 0);
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002400 }
2401
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002402 @VisibleForTesting
2403 public interface SendOrSaveCallback {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002404 void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
2405 void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2406 Message getMessage();
2407 void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2408 void incrementRecipientsTimesContacted(List<String> recipients);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002409 }
2410
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002411 @VisibleForTesting
2412 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002413 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002414 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002415 public final SendOrSaveCallback mSendOrSaveCallback;
2416 @VisibleForTesting
2417 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002418 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002419
2420 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002421 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002422 mContext = context;
2423 mSendOrSaveCallback = callback;
2424 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002425 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002426 }
2427
2428 @Override
2429 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002430 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002431
Mindy Pereira92551d02012-04-05 11:31:12 -07002432 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002433 Message message = mSendOrSaveCallback.getMessage();
2434 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002435 // If a previous draft has been saved, in an account that is different
2436 // than what the user wants to send from, remove the old draft, and treat this
2437 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002438 if (mExistingDraftAccount != null
2439 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002440 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2441 ContentResolver resolver = mContext.getContentResolver();
2442 ContentValues values = new ContentValues();
2443 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002444 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2445 new ContentProviderTask.UpdateTask()
2446 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2447 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002448 } else {
2449 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002450 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002451 // reset messageId to 0, so a new message will be created
2452 messageId = UIProvider.INVALID_MESSAGE_ID;
2453 }
2454 }
2455
2456 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002457 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002458
2459 if (!sendOrSaveMessage.mSave) {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002460 incrementRecipientsTimesContacted(
Andrew Sapperstein22a3a312014-06-24 18:35:49 -07002461 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO),
2462 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC),
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002463 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2464 }
2465 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2466 }
2467
Andrew Sapperstein22a3a312014-06-24 18:35:49 -07002468 private void incrementRecipientsTimesContacted(
2469 final String toAddresses, final String ccAddresses, final String bccAddresses) {
2470 final List<String> recipients = Lists.newArrayList();
2471 addAddressesToRecipientList(recipients, toAddresses);
2472 addAddressesToRecipientList(recipients, ccAddresses);
2473 addAddressesToRecipientList(recipients, bccAddresses);
2474 mSendOrSaveCallback.incrementRecipientsTimesContacted(recipients);
2475 }
2476
2477 private void addAddressesToRecipientList(
2478 final List<String> recipients, final String addressString) {
2479 if (recipients == null) {
2480 throw new IllegalArgumentException("recipientList cannot be null");
2481 }
Tony Mantler9f324232013-08-08 14:24:30 -07002482 if (TextUtils.isEmpty(addressString)) {
2483 return;
2484 }
2485 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
Tony Mantler581edd42014-02-18 15:41:22 -08002486 for (final Rfc822Token token : tokens) {
2487 recipients.add(token.getAddress());
Tony Mantler9f324232013-08-08 14:24:30 -07002488 }
Tony Mantler9f324232013-08-08 14:24:30 -07002489 }
2490
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002491 /**
2492 * Send or Save a message.
2493 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002494 private void sendOrSaveMessage(final long messageIdToSave,
2495 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002496 final ContentResolver resolver = mContext.getContentResolver();
2497 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2498
2499 final String accountMethod = sendOrSaveMessage.mSave ?
2500 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2501 UIProvider.AccountCallMethods.SEND_MESSAGE;
2502
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002503 try {
2504 if (updateExistingMessage) {
2505 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002506
Paul Westbrook013a23c2013-02-22 10:37:41 -08002507 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002508 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002509 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002510 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002511 final Bundle result = callAccountSendSaveMethod(resolver,
2512 selectedAccount.account, accountMethod, sendOrSaveMessage);
2513 if (result != null) {
2514 // If a non-null value was returned, then the provider handled the call
2515 // method
2516 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002517 }
2518 if (sendOrSaveMessage.mSave && messageUri != null) {
2519 final Cursor messageCursor = resolver.query(messageUri,
2520 UIProvider.MESSAGE_PROJECTION, null, null, null);
2521 if (messageCursor != null) {
2522 try {
2523 if (messageCursor.moveToFirst()) {
2524 // Broadcast notification that a new message has
2525 // been allocated
2526 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2527 new Message(messageCursor));
2528 }
2529 } finally {
2530 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002531 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002532 }
2533 }
2534 }
2535 } finally {
2536 // Close any opened file descriptors
2537 closeOpenedAttachmentFds(sendOrSaveMessage);
2538 }
2539 }
2540
Scott Kennedyff8553f2013-04-05 20:57:44 -07002541 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002542 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2543 if (openedFds != null) {
2544 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002545 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002546 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2547 if (fd != null) {
2548 try {
2549 fd.close();
2550 } catch (IOException e) {
2551 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002552 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002553 }
2554 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002555 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002556 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002557
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002558 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002559 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002560 *
2561 * If this was successful, this method will return an non-null Bundle instance
2562 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002563 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2564 final Account account, final String method,
2565 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002566 // Copy all of the values from the content values to the bundle
2567 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2568 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2569
2570 for (Entry<String, Object> entry : valueSet) {
2571 final Object entryValue = entry.getValue();
2572 final String key = entry.getKey();
2573 if (entryValue instanceof String) {
2574 methodExtras.putString(key, (String)entryValue);
2575 } else if (entryValue instanceof Boolean) {
2576 methodExtras.putBoolean(key, (Boolean)entryValue);
2577 } else if (entryValue instanceof Integer) {
2578 methodExtras.putInt(key, (Integer)entryValue);
2579 } else if (entryValue instanceof Long) {
2580 methodExtras.putLong(key, (Long)entryValue);
2581 } else {
2582 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2583 entryValue.getClass().getName());
2584 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002585 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002586
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002587 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2588 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2589 if (fdMap != null) {
2590 methodExtras.putParcelable(
2591 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2592 }
2593
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002594 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002595 }
2596 }
2597
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002598 /**
2599 * Reports recipients that have been contacted in order to improve auto-complete
2600 * suggestions. Default behavior updates usage statistics in ContactsProvider.
2601 * @param recipients addresses
2602 */
2603 protected void incrementRecipientsTimesContacted(List<String> recipients) {
2604 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(this);
2605 statsUpdater.updateWithAddress(recipients);
2606 }
2607
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002608 @VisibleForTesting
2609 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002610 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002611 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002612 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002613 @VisibleForTesting
2614 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002615 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002616 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002617
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002618 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2619 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002620 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002621 mValues = values;
2622 mRefMessageId = refMessageId;
2623 mSave = save;
2624 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002625
2626 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002627 }
2628
2629 int requestId() {
2630 return mRequestId;
2631 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002632
2633 Bundle attachmentFds() {
2634 return mAttachmentFds;
2635 }
2636
2637 /**
2638 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2639 * called before the ComposeActivity finishes.
2640 * Note: The caller is responsible for closing these file descriptors.
2641 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002642 private static Bundle initializeAttachmentFds(final Context context,
2643 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002644 if (attachments == null || attachments.size() == 0) {
2645 return null;
2646 }
2647
2648 final Bundle result = new Bundle(attachments.size());
2649 final ContentResolver resolver = context.getContentResolver();
2650
2651 for (Attachment attachment : attachments) {
2652 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2653 continue;
2654 }
2655
2656 ParcelFileDescriptor fileDescriptor;
2657 try {
2658 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2659 } catch (FileNotFoundException e) {
2660 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2661 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002662 } catch (SecurityException e) {
2663 // We have encountered a security exception when attempting to open the file
2664 // specified by the content uri. If the attachment has been cached, this
2665 // isn't a problem, as even through the original permission may have been
2666 // revoked, we have cached the file. This will happen when saving/sending
2667 // a previously saved draft.
2668 // TODO(markwei): Expose whether the attachment has been cached through the
2669 // attachment object. This would allow us to limit when the log is made, as
2670 // if the attachment has been cached, this really isn't an error
2671 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2672 // Just set the file descriptor to null, as the underlying provider needs
2673 // to handle the file descriptor not being set.
2674 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002675 }
2676
2677 if (fileDescriptor != null) {
2678 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2679 }
2680 }
2681
2682 return result;
2683 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002684 }
2685
2686 /**
2687 * Get the to recipients.
2688 */
2689 public String[] getToAddresses() {
2690 return getAddressesFromList(mTo);
2691 }
2692
2693 /**
2694 * Get the cc recipients.
2695 */
2696 public String[] getCcAddresses() {
2697 return getAddressesFromList(mCc);
2698 }
2699
2700 /**
2701 * Get the bcc recipients.
2702 */
2703 public String[] getBccAddresses() {
2704 return getAddressesFromList(mBcc);
2705 }
2706
2707 public String[] getAddressesFromList(RecipientEditTextView list) {
2708 if (list == null) {
2709 return new String[0];
2710 }
2711 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2712 int count = tokens.length;
2713 String[] result = new String[count];
2714 for (int i = 0; i < count; i++) {
2715 result[i] = tokens[i].toString();
2716 }
2717 return result;
2718 }
2719
2720 /**
2721 * Check for invalid email addresses.
2722 * @param to String array of email addresses to check.
2723 * @param wrongEmailsOut Emails addresses that were invalid.
2724 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002725 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002726 if (mValidator == null) {
2727 return;
2728 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002729 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002730 if (!mValidator.isValid(email)) {
2731 wrongEmailsOut.add(email);
2732 }
2733 }
2734 }
2735
Tony Mantler2558b502013-07-09 10:53:34 -07002736 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002737 // Public no-args constructor needed for fragment re-instantiation
2738 public RecipientErrorDialogFragment() {}
2739
Tony Mantler2558b502013-07-09 10:53:34 -07002740 public static RecipientErrorDialogFragment newInstance(final String message) {
2741 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2742 final Bundle args = new Bundle(1);
2743 args.putString("message", message);
2744 frag.setArguments(args);
2745 return frag;
2746 }
2747
2748 @Override
2749 public Dialog onCreateDialog(Bundle savedInstanceState) {
2750 final String message = getArguments().getString("message");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002751 return new AlertDialog.Builder(getActivity())
2752 .setMessage(message)
Tony Mantler2558b502013-07-09 10:53:34 -07002753 .setPositiveButton(
2754 R.string.ok, new Dialog.OnClickListener() {
2755 @Override
2756 public void onClick(DialogInterface dialog, int which) {
2757 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2758 }
2759 }).create();
2760 }
2761 }
2762
2763 private void finishRecipientErrorDialog() {
2764 // after the user dismisses the recipient error
2765 // dialog we want to make sure to refocus the
2766 // recipient to field so they can fix the issue
2767 // easily
2768 if (mTo != null) {
2769 mTo.requestFocus();
2770 }
2771 }
2772
Mindy Pereira82cc5662012-01-09 17:29:30 -08002773 /**
2774 * Show an error because the user has entered an invalid recipient.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002775 */
Tony Mantler2558b502013-07-09 10:53:34 -07002776 private void showRecipientErrorDialog(final String message) {
2777 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2778 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002779 }
2780
2781 /**
2782 * Update the state of the UI based on whether or not the current draft
2783 * needs to be saved and the message is not empty.
2784 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002785 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002786 if (mSave != null) {
2787 mSave.setEnabled((shouldSave() && !isBlank()));
2788 }
2789 }
2790
2791 /**
2792 * Returns true if we need to save the current draft.
2793 */
2794 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002795 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002796 // The message should only be saved if:
2797 // It hasn't been sent AND
2798 // Some text has been added to the message OR
2799 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002800 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002801 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002802 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002803 }
2804 }
2805
2806 /**
Greg Bullockd47a7042014-08-13 16:02:59 +02002807 * Returns whether the "Attach from Drive" menu item should be visible.
2808 */
2809 protected boolean shouldEnableAttachFromServiceMenu(Account mAccount) {
2810 return false;
2811 }
2812
2813 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002814 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002815 * @return boolean
2816 */
2817 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002818 // Need to check for null since isBlank() can be called from onPause()
2819 // before findViews() is called
2820 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2821 mAttachmentsView == null) {
2822 LogUtils.w(LOG_TAG, "null views in isBlank check");
2823 return true;
2824 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002825 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002826 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2827 mBodyView.getText().toString()) == 0)
2828 && mTo.length() == 0
2829 && mCc.length() == 0 && mBcc.length() == 0
2830 && mAttachmentsView.getAttachments().size() == 0;
2831 }
2832
2833 @VisibleForTesting
2834 protected int getSignatureStartPosition(String signature, String bodyText) {
2835 int startPos = -1;
2836
2837 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2838 return startPos;
2839 }
2840
2841 int bodyLength = bodyText.length();
2842 int signatureLength = signature.length();
2843 String printableVersion = convertToPrintableSignature(signature);
2844 int printableLength = printableVersion.length();
2845
2846 if (bodyLength >= printableLength
2847 && bodyText.substring(bodyLength - printableLength)
2848 .equals(printableVersion)) {
2849 startPos = bodyLength - printableLength;
2850 } else if (bodyLength >= signatureLength
2851 && bodyText.substring(bodyLength - signatureLength)
2852 .equals(signature)) {
2853 startPos = bodyLength - signatureLength;
2854 }
2855 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002856 }
2857
2858 /**
2859 * Allows any changes made by the user to be ignored. Called when the user
2860 * decides to discard a draft.
2861 */
2862 private void discardChanges() {
2863 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002864 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002865 mReplyFromChanged = false;
2866 }
2867
2868 /**
Tony Mantler581edd42014-02-18 15:41:22 -08002869 * @param save True to save, false to send
2870 * @param showToast True to show a toast once the message is sent/saved
Mindy Pereira181df782012-03-01 13:32:44 -08002871 */
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002872 protected void sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002873 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002874 if (mAccounts == null || mAccount == null) {
2875 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002876 if (autoSend) {
2877 finish();
2878 }
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002879 return;
Mark Wei009b3712012-10-18 18:07:50 -07002880 }
2881
Scott Kennedyff8553f2013-04-05 20:57:44 -07002882 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002883 if (orientationChanged) {
2884 to = cc = bcc = new String[0];
2885 } else {
2886 to = getToAddresses();
2887 cc = getCcAddresses();
2888 bcc = getBccAddresses();
2889 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002890
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002891 final ArrayList<String> recipients = buildEmailAddressList(to);
2892 recipients.addAll(buildEmailAddressList(cc));
2893 recipients.addAll(buildEmailAddressList(bcc));
2894
Mindy Pereira181df782012-03-01 13:32:44 -08002895 // Don't let the user send to nobody (but it's okay to save a message
2896 // with no recipients)
2897 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2898 showRecipientErrorDialog(getString(R.string.recipient_needed));
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002899 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002900 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002901
Mindy Pereira181df782012-03-01 13:32:44 -08002902 List<String> wrongEmails = new ArrayList<String>();
2903 if (!save) {
2904 checkInvalidEmails(to, wrongEmails);
2905 checkInvalidEmails(cc, wrongEmails);
2906 checkInvalidEmails(bcc, wrongEmails);
2907 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002908
Mindy Pereira181df782012-03-01 13:32:44 -08002909 // Don't let the user send an email with invalid recipients
2910 if (wrongEmails.size() > 0) {
2911 String errorText = String.format(getString(R.string.invalid_recipient),
2912 wrongEmails.get(0));
2913 showRecipientErrorDialog(errorText);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002914 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002915 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002916
Mindy Pereira181df782012-03-01 13:32:44 -08002917 if (!save) {
Alan Lau3d519042014-06-05 11:13:06 -07002918 if (autoSend) {
2919 // Skip all further checks during autosend. This flow is used by Android Wear
2920 // and Google Now.
2921 sendOrSave(save, showToast);
2922 return;
2923 }
2924
2925 // Show a warning before sending only if there are no attachments, body, or subject.
Mindy Pereira181df782012-03-01 13:32:44 -08002926 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2927 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002928 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002929
Mindy Pereira181df782012-03-01 13:32:44 -08002930 // A warning about an empty body may not be warranted when
2931 // forwarding mails, since a common use case is to forward
2932 // quoted text and not append any more text.
2933 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002934
Mindy Pereira181df782012-03-01 13:32:44 -08002935 // When we bring up a dialog warning the user about a send,
2936 // assume that they accept sending the message. If they do not,
2937 // the dialog listener is required to enable sending again.
2938 if (warnAboutEmptySubject) {
Tony Mantler581edd42014-02-18 15:41:22 -08002939 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002940 showToast, recipients);
2941 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002942 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002943
Mindy Pereira181df782012-03-01 13:32:44 -08002944 if (warnAboutEmptyBody) {
Tony Mantler581edd42014-02-18 15:41:22 -08002945 showSendConfirmDialog(R.string.confirm_send_message_with_no_body,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002946 showToast, recipients);
2947 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002948 }
2949 }
Alan Lau3d519042014-06-05 11:13:06 -07002950 // Ask for confirmation to send.
2951 if (showSendConfirmation()) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002952 showSendConfirmDialog(R.string.confirm_send_message, showToast, recipients);
2953 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002954 }
2955 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002956
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002957 performAdditionalSendOrSaveSanityChecks(save, showToast, recipients);
Mindy Pereira181df782012-03-01 13:32:44 -08002958 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002959
Mindy Pereira181df782012-03-01 13:32:44 -08002960 /**
2961 * Returns a boolean indicating whether warnings should be shown for empty
2962 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002963 *
Mindy Pereira181df782012-03-01 13:32:44 -08002964 * @return True if a warning should be shown for empty text fields
2965 */
2966 protected boolean showEmptyTextWarnings() {
2967 return mAttachmentsView.getAttachments().size() == 0;
2968 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002969
Mindy Pereira181df782012-03-01 13:32:44 -08002970 /**
2971 * Returns a boolean indicating whether the user should confirm each send
2972 *
2973 * @return True if a warning should be on each send
2974 */
2975 protected boolean showSendConfirmation() {
Tony Mantler581edd42014-02-18 15:41:22 -08002976 return mCachedSettings != null && mCachedSettings.confirmSend;
Mindy Pereira181df782012-03-01 13:32:44 -08002977 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002978
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002979 public static class SendConfirmDialogFragment extends DialogFragment
2980 implements DialogInterface.OnClickListener {
2981
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002982 private static final String MESSAGE_ID = "messageId";
2983 private static final String SHOW_TOAST = "showToast";
2984 private static final String RECIPIENTS = "recipients";
2985
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002986 private boolean mShowToast;
2987
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002988 private ArrayList<String> mRecipients;
2989
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002990 // Public no-args constructor needed for fragment re-instantiation
2991 public SendConfirmDialogFragment() {}
2992
Tony Mantler2558b502013-07-09 10:53:34 -07002993 public static SendConfirmDialogFragment newInstance(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002994 final boolean showToast, final ArrayList<String> recipients) {
Tony Mantler2558b502013-07-09 10:53:34 -07002995 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2996 final Bundle args = new Bundle(3);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002997 args.putInt(MESSAGE_ID, messageId);
2998 args.putBoolean(SHOW_TOAST, showToast);
2999 args.putStringArrayList(RECIPIENTS, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003000 frag.setArguments(args);
3001 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08003002 }
Tony Mantler2558b502013-07-09 10:53:34 -07003003
3004 @Override
3005 public Dialog onCreateDialog(Bundle savedInstanceState) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003006 final int messageId = getArguments().getInt(MESSAGE_ID);
3007 mShowToast = getArguments().getBoolean(SHOW_TOAST);
3008 mRecipients = getArguments().getStringArrayList(RECIPIENTS);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003009
3010 final int confirmTextId = (messageId == R.string.confirm_send_message) ?
3011 R.string.ok : R.string.send;
Tony Mantler2558b502013-07-09 10:53:34 -07003012
3013 return new AlertDialog.Builder(getActivity())
3014 .setMessage(messageId)
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003015 .setPositiveButton(confirmTextId, this)
Paul Westbrook7d1c5c42013-10-01 23:40:04 -07003016 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003017 .create();
3018 }
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003019
3020 @Override
3021 public void onClick(DialogInterface dialog, int which) {
3022 if (which == DialogInterface.BUTTON_POSITIVE) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003023 ((ComposeActivity) getActivity()).finishSendConfirmDialog(mShowToast, mRecipients);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003024 }
3025 }
Tony Mantler2558b502013-07-09 10:53:34 -07003026 }
3027
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003028 private void finishSendConfirmDialog(
3029 final boolean showToast, final ArrayList<String> recipients) {
3030 performAdditionalSendOrSaveSanityChecks(false /* save */, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003031 }
3032
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003033 // The list of recipients are used by the additional sendOrSave checks.
3034 // However, the send confirm dialog may be shown before performing
3035 // the additional checks. As a result, we need to plumb the recipient
3036 // list through the send confirm dialog so that
3037 // performAdditionalSendOrSaveChecks can be performed properly.
Tony Mantler581edd42014-02-18 15:41:22 -08003038 private void showSendConfirmDialog(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003039 final boolean showToast, final ArrayList<String> recipients) {
3040 final DialogFragment frag = SendConfirmDialogFragment.newInstance(
3041 messageId, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003042 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08003043 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003044
Mindy Pereira181df782012-03-01 13:32:44 -08003045 /**
3046 * Returns whether the ComposeArea believes there is any text in the body of
3047 * the composition. TODO: When ComposeArea controls the Body as well, add
3048 * that here.
3049 */
3050 public boolean isBodyEmpty() {
3051 return !mQuotedTextView.isTextIncluded();
3052 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003053
Mindy Pereira181df782012-03-01 13:32:44 -08003054 /**
3055 * Test to see if the subject is empty.
3056 *
3057 * @return boolean.
3058 */
3059 // TODO: this will likely go away when composeArea.focus() is implemented
3060 // after all the widget control is moved over.
3061 public boolean isSubjectEmpty() {
3062 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
3063 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003064
Andy Huang0a2a3462013-12-20 15:56:13 -08003065 @VisibleForTesting
3066 public String getSubject() {
3067 return mSubject.getText().toString();
3068 }
3069
Andy Huang91ede362014-01-21 19:16:00 -08003070 private int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Jin Cao77b4c2c2014-05-20 13:55:53 -07003071 Message message, final Message refMessage, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08003072 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07003073 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08003074 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003075
Paul Westbrookb4931c62013-01-14 17:51:18 -08003076 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07003077
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003078 MessageModification.putToAddresses(values, message.getToAddresses());
3079 MessageModification.putCcAddresses(values, message.getCcAddresses());
3080 MessageModification.putBccAddresses(values, message.getBccAddresses());
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003081 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07003082
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003083 MessageModification.putSubject(values, message.subject);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003084
Jin Cao77b4c2c2014-05-20 13:55:53 -07003085 // bodyHtml already have the composing spans removed.
3086 final String htmlBody = message.bodyHtml;
Jin Caoa9f5a8e2014-07-22 13:48:45 -07003087 final String textBody = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07003088 // fullbody will contain the actual body plus the quoted text.
3089 final String fullBody;
3090 final String quotedString;
3091 final boolean hasQuotedText = !TextUtils.isEmpty(quotedText);
3092 if (hasQuotedText) {
3093 // The quoted text is HTML at this point.
3094 quotedString = quotedText.toString();
3095 fullBody = htmlBody + quotedString;
3096 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
3097 MessageModification.putAppendRefMessageContent(values, true /* include quoted */);
3098 } else {
3099 fullBody = htmlBody;
3100 quotedString = null;
Mindy Pereira29ef1b82012-01-13 11:26:21 -08003101 }
Jin Caoa9f5a8e2014-07-22 13:48:45 -07003102 // Only take refMessage into account if either one of its html/text is not empty.
3103 if (refMessage != null && !(TextUtils.isEmpty(refMessage.bodyHtml) &&
3104 TextUtils.isEmpty(refMessage.bodyText))) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07003105 // The code below might need to be revisited. The quoted text position is different
3106 // between text/html and text/plain parts and they should be stored seperately and
3107 // the right version should be used in the UI. text/html should have preference
3108 // if both exist. Issues like this made me file b/14256940 to make sure that we
3109 // properly handle the existing of both text/html and text/plain parts and to verify
3110 // that we are not making some assumptions that break if there is no text/html part.
3111 int quotedTextPos = -1;
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003112 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
3113 MessageModification.putBodyHtml(values, fullBody.toString());
Anthony Lee2a3cc132014-04-22 14:15:25 -07003114 if (hasQuotedText) {
3115 quotedTextPos = htmlBody.length() +
3116 QuotedTextView.getQuotedTextOffset(quotedString);
3117 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003118 }
3119 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08003120 MessageModification.putBody(values,
Tony Mantler581edd42014-02-18 15:41:22 -08003121 Utils.convertHtmlToPlainText(fullBody.toString()));
Anthony Lee2a3cc132014-04-22 14:15:25 -07003122 if (hasQuotedText && (quotedTextPos == -1)) {
3123 quotedTextPos = textBody.length();
3124 }
3125 }
3126 if (quotedTextPos != -1) {
3127 // The quoted text pos is the text/html version first and the text/plan version
3128 // if there is no text/html part. The reason for this is because preference
3129 // is given to text/html in the compose window if it exists. In the future, we
3130 // should calculate the index for both since the user could choose to compose
3131 // explicitly in text/plain.
3132 MessageModification.putQuoteStartPos(values, quotedTextPos);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003133 }
3134 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07003135 MessageModification.putBodyHtml(values, fullBody.toString());
Tony Mantler581edd42014-02-18 15:41:22 -08003136 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString()));
Mindy Pereirac2031972012-04-03 09:38:35 -07003137 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07003138 int draftType = getDraftType(composeMode);
3139 MessageModification.putDraftType(values, draftType);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003140 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07003141 if (!TextUtils.isEmpty(refMessageId)) {
3142 MessageModification.putRefMessageId(values, refMessageId);
3143 }
Scott Kennedy60847252013-08-15 15:55:42 -07003144 if (extraValues != null) {
3145 values.putAll(extraValues);
3146 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07003147 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
3148 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08003149 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
3150 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003151
Mindy Pereira181df782012-03-01 13:32:44 -08003152 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08003153 // Do the send/save action on the specified handler to avoid possible
3154 // ANRs
3155 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003156
Mindy Pereira181df782012-03-01 13:32:44 -08003157 return sendOrSaveMessage.requestId();
3158 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003159
Paul Westbrookb4931c62013-01-14 17:51:18 -08003160 /**
3161 * Removes any composing spans from the specified string. This will create a new
3162 * SpannableString instance, as to not modify the behavior of the EditText view.
3163 */
3164 private static SpannableString removeComposingSpans(Spanned body) {
3165 final SpannableString messageBody = new SpannableString(body);
3166 BaseInputConnection.removeComposingSpans(messageBody);
Andy Huangff017272014-06-18 00:27:35 -07003167
3168 // Remove watcher spans while we're at it, so any off-UI thread manipulation of these
3169 // spans doesn't trigger unexpected side-effects. This copy is essentially 100% detached
3170 // from the EditText.
3171 //
3172 // (must remove SpanWatchers first to avoid triggering them as we remove other spans)
3173 removeSpansOfType(messageBody, SpanWatcher.class);
3174 removeSpansOfType(messageBody, TextWatcher.class);
3175
Paul Westbrookb4931c62013-01-14 17:51:18 -08003176 return messageBody;
3177 }
3178
Andy Huangff017272014-06-18 00:27:35 -07003179 private static void removeSpansOfType(SpannableString str, Class<?> cls) {
3180 for (Object span : str.getSpans(0, str.length(), cls)) {
3181 str.removeSpan(span);
3182 }
3183 }
3184
Mindy Pereira002ff522012-05-30 10:31:26 -07003185 private static int getDraftType(int mode) {
3186 int draftType = -1;
3187 switch (mode) {
3188 case ComposeActivity.COMPOSE:
3189 draftType = DraftType.COMPOSE;
3190 break;
3191 case ComposeActivity.REPLY:
3192 draftType = DraftType.REPLY;
3193 break;
3194 case ComposeActivity.REPLY_ALL:
3195 draftType = DraftType.REPLY_ALL;
3196 break;
3197 case ComposeActivity.FORWARD:
3198 draftType = DraftType.FORWARD;
3199 break;
3200 }
3201 return draftType;
3202 }
3203
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003204 /**
3205 * Derived classes should override this step to perform additional checks before
3206 * send or save. The default implementation simply calls {@link #sendOrSave(boolean, boolean)}.
3207 */
3208 protected void performAdditionalSendOrSaveSanityChecks(
3209 final boolean save, final boolean showToast, ArrayList<String> recipients) {
3210 sendOrSave(save, showToast);
3211 }
3212
3213 protected void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08003214 // Check if user is a monkey. Monkeys can compose and hit send
3215 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07003216 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08003217 return;
3218 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003219
Jin Cao77b4c2c2014-05-20 13:55:53 -07003220 final SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003221 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08003222 private int mRestoredRequestId;
3223
Marc Blank0bbc8582012-04-23 15:07:57 -07003224 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08003225 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08003226 synchronized (mActiveTasks) {
3227 int numTasks = mActiveTasks.size();
3228 if (numTasks == 0) {
3229 // Start service so we won't be killed if this app is
3230 // put in the background.
3231 startService(new Intent(ComposeActivity.this, EmptyService.class));
3232 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003233
Mindy Pereira181df782012-03-01 13:32:44 -08003234 mActiveTasks.add(sendOrSaveTask);
3235 }
3236 if (sTestSendOrSaveCallback != null) {
3237 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
3238 }
3239 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003240
Marc Blank0bbc8582012-04-23 15:07:57 -07003241 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003242 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
3243 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08003244 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08003245 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08003246 mDraftId = message.id;
3247 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003248 if (sRequestMessageIdMap != null) {
3249 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
3250 }
Mindy Pereira181df782012-03-01 13:32:44 -08003251 // Cache request message map, in case the process is killed
3252 saveRequestMap();
3253 }
3254 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003255 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08003256 }
3257 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003258
Marc Blank0bbc8582012-04-23 15:07:57 -07003259 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003260 public Message getMessage() {
3261 synchronized (mDraftLock) {
3262 return mDraft;
3263 }
3264 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003265
Marc Blank0bbc8582012-04-23 15:07:57 -07003266 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003267 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07003268 // Update the last sent from account.
3269 if (mAccount != null) {
3270 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
3271 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003272 if (success) {
3273 // Successfully sent or saved so reset change markers
3274 discardChanges();
3275 } else {
3276 // A failure happened with saving/sending the draft
3277 // TODO(pwestbro): add a better string that should be used
3278 // when failing to send or save
3279 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
3280 .show();
3281 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003282
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003283 int numTasks;
3284 synchronized (mActiveTasks) {
3285 // Remove the task from the list of active tasks
3286 mActiveTasks.remove(task);
3287 numTasks = mActiveTasks.size();
3288 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003289
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003290 if (numTasks == 0) {
3291 // Stop service so we can be killed.
3292 stopService(new Intent(ComposeActivity.this, EmptyService.class));
3293 }
3294 if (sTestSendOrSaveCallback != null) {
3295 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
3296 }
3297 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07003298
3299 @Override
3300 public void incrementRecipientsTimesContacted(final List<String> recipients) {
3301 ComposeActivity.this.incrementRecipientsTimesContacted(recipients);
3302 }
Mindy Pereira181df782012-03-01 13:32:44 -08003303 };
Tony Mantler1e05a1e2013-08-12 16:44:26 -07003304 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003305
Jin Cao77b4c2c2014-05-20 13:55:53 -07003306 final Spanned body = removeComposingSpans(mBodyView.getText());
3307 SEND_SAVE_TASK_HANDLER.post(new Runnable() {
3308 @Override
3309 public void run() {
3310 final Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode(), body);
3311 mRequestId = sendOrSaveInternal(ComposeActivity.this, mReplyFromAccount, msg,
3312 mRefMessage, mQuotedTextView.getQuotedTextIfIncluded(), callback,
3313 SEND_SAVE_TASK_HANDLER, save, mComposeMode, mDraftAccount, mExtraValues);
3314 }
3315 });
Mindy Pereira82cc5662012-01-09 17:29:30 -08003316
Mindy Pereira181df782012-03-01 13:32:44 -08003317 // Don't display the toast if the user is just changing the orientation,
3318 // but we still need to save the draft to the cursor because this is how we restore
3319 // the attachments when the configuration change completes.
3320 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
3321 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
3322 Toast.LENGTH_LONG).show();
3323 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003324
Mindy Pereira181df782012-03-01 13:32:44 -08003325 // Need to update variables here because the send or save completes
3326 // asynchronously even though the toast shows right away.
3327 discardChanges();
3328 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003329
Mindy Pereira181df782012-03-01 13:32:44 -08003330 // If we are sending, finish the activity
3331 if (!save) {
3332 finish();
3333 }
3334 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003335
Mindy Pereira181df782012-03-01 13:32:44 -08003336 /**
3337 * Save the state of the request messageid map. This allows for the Gmail
3338 * process to be killed, but and still allow for ComposeActivity instances
3339 * to be recreated correctly.
3340 */
3341 private void saveRequestMap() {
3342 // TODO: store the request map in user preferences.
3343 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003344
Tony Mantler581edd42014-02-18 15:41:22 -08003345 @SuppressLint("NewApi")
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003346 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08003347 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
3348 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07003349 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07003350 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003351 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08003352 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08003353 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
3354 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08003355 }
3356
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003357 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003358 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003359 if (mCcBccButton != null) {
Jin Cao9d358a12014-07-24 12:15:38 -07003360 mCcBccButton.setVisibility(View.GONE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003361 }
3362 }
3363
Andy Huang4fe0af82013-08-20 17:24:51 -07003364 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003365 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07003366 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003367 case COMPOSE:
3368 msgType = "new_message";
3369 break;
3370 case REPLY:
3371 msgType = "reply";
3372 break;
3373 case REPLY_ALL:
3374 msgType = "reply_all";
3375 break;
3376 case FORWARD:
3377 msgType = "forward";
3378 break;
3379 default:
3380 msgType = "unknown";
3381 break;
3382 }
Andy Huang4fe0af82013-08-20 17:24:51 -07003383 return msgType;
3384 }
3385
3386 private void logSendOrSave(boolean save) {
3387 if (!Analytics.isLoggable() || mAttachmentsView == null) {
3388 return;
3389 }
3390
3391 final String category = (save) ? "message_save" : "message_send";
3392 final int attachmentCount = getAttachments().size();
3393 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07003394 final String label;
3395 final long value;
3396 if (mComposeMode == COMPOSE) {
3397 label = Integer.toString(attachmentCount);
3398 value = attachmentCount;
3399 } else {
3400 label = null;
3401 value = 0;
3402 }
3403 Analytics.getInstance().sendEvent(category, msgType, label, value);
3404 }
3405
Mindy Pereira326c6602012-01-04 15:32:42 -08003406 @Override
3407 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003408 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003409 if (position == ComposeActivity.REPLY) {
3410 mComposeMode = ComposeActivity.REPLY;
3411 } else if (position == ComposeActivity.REPLY_ALL) {
3412 mComposeMode = ComposeActivity.REPLY_ALL;
3413 } else if (position == ComposeActivity.FORWARD) {
3414 mComposeMode = ComposeActivity.FORWARD;
3415 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003416 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003417 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003418 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003419 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003420 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003421 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003422 boolean showCc = false;
3423 boolean showBcc = false;
3424 if (mDraft != null) {
3425 // Following desktop behavior, if the user has added a BCC
3426 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003427 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003428 // Use the draft to determine what to populate.
3429 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003430 showCc = showBcc
3431 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003432 }
3433 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003434 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003435 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003436 }
Jin Caoc5c550a2014-07-29 11:53:17 -07003437 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003438 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003439 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003440 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003441 return true;
3442 }
3443
Mindy Pereirab3112a22012-06-20 12:10:03 -07003444 @VisibleForTesting
3445 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003446 // When switching between reply, reply all, forward,
3447 // follow the behavior of webview.
3448 // The contents of the following fields are cleared
3449 // so that they can be populated directly from the
3450 // ref message:
3451 // 1) Any recipient fields
3452 // 2) The subject
3453 mTo.setText("");
3454 mCc.setText("");
3455 mBcc.setText("");
3456 // Any edits to the subject are replaced with the original subject.
3457 mSubject.setText("");
3458
3459 // Any changes to the contents of the following fields are kept:
3460 // 1) Body
3461 // 2) Attachments
3462 // If the user made changes to attachments, keep their changes.
3463 if (!mAttachmentsChanged) {
3464 mAttachmentsView.deleteAllAttachments();
3465 }
3466 }
3467
Mindy Pereira326c6602012-01-04 15:32:42 -08003468 private class ComposeModeAdapter extends ArrayAdapter<String> {
3469
Jin Caof7461632014-08-11 15:21:43 -07003470 private Context mContext;
Mindy Pereira326c6602012-01-04 15:32:42 -08003471 private LayoutInflater mInflater;
3472
3473 public ComposeModeAdapter(Context context) {
3474 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3475 .getStringArray(R.array.compose_modes));
Jin Caof7461632014-08-11 15:21:43 -07003476 mContext = context;
Mindy Pereira326c6602012-01-04 15:32:42 -08003477 }
3478
3479 private LayoutInflater getInflater() {
3480 if (mInflater == null) {
Jin Caof7461632014-08-11 15:21:43 -07003481 mInflater = LayoutInflater.from(mContext);
Mindy Pereira326c6602012-01-04 15:32:42 -08003482 }
3483 return mInflater;
3484 }
3485
3486 @Override
3487 public View getView(int position, View convertView, ViewGroup parent) {
3488 if (convertView == null) {
3489 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3490 }
3491 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3492 return super.getView(position, convertView, parent);
3493 }
3494 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003495
3496 @Override
3497 public void onRespondInline(String text) {
3498 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003499 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003500 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003501 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003502 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003503 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003504 }
3505
3506 /**
3507 * Append text to the body of the message. If there is no existing body
3508 * text, just sets the body to text.
3509 *
Tony Mantler581edd42014-02-18 15:41:22 -08003510 * @param text Text to append
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003511 * @param withSignature True to append a signature.
3512 */
3513 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003514 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003515 if (bodyText != null && bodyText.length() > 0) {
3516 bodyText.append(text);
3517 } else {
3518 setBody(text, withSignature);
3519 }
3520 }
3521
3522 /**
3523 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003524 *
Tony Mantler581edd42014-02-18 15:41:22 -08003525 * @param text text to set
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003526 * @param withSignature True to append a signature.
3527 */
3528 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003529 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003530 if (withSignature) {
3531 appendSignature();
3532 }
3533 }
3534
3535 private void appendSignature() {
Tony Mantler6a7ac782014-02-19 15:22:02 -08003536 final String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3537 final int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
mindyp27083062012-11-15 09:02:01 -08003538 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003539 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003540 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003541 // Appending a signature does not count as changing text.
3542 mBodyView.removeTextChangedListener(this);
3543 mBodyView.append(convertToPrintableSignature(mSignature));
3544 mBodyView.addTextChangedListener(this);
3545 }
Tony Mantler6a7ac782014-02-19 15:22:02 -08003546 resetBodySelection();
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003547 }
3548 }
3549
3550 private String convertToPrintableSignature(String signature) {
3551 String signatureResource = getResources().getString(R.string.signature);
3552 if (signature == null) {
3553 signature = "";
3554 }
3555 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003556 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003557
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003558 @Override
3559 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003560 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3561 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003562 // Clear a signature, if there was one.
3563 mBodyView.removeTextChangedListener(this);
3564 String oldSignature = mSignature;
3565 String bodyText = getBody().getText().toString();
3566 if (!TextUtils.isEmpty(oldSignature)) {
3567 int pos = getSignatureStartPosition(oldSignature, bodyText);
3568 if (pos > -1) {
3569 mBodyView.setText(bodyText.substring(0, pos));
3570 }
3571 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003572 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003573 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003574 // TODO: handle discarding attachments when switching accounts.
3575 // Only enable save for this draft if there is any other content
3576 // in the message.
3577 if (!isBlank()) {
3578 enableSave(true);
3579 }
3580 mReplyFromChanged = true;
3581 initRecipients();
Greg Bullockd47a7042014-08-13 16:02:59 +02003582
3583 invalidateOptionsMenu();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003584 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003585 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003586
3587 public void enableSave(boolean enabled) {
3588 if (mSave != null) {
3589 mSave.setEnabled(enabled);
3590 }
3591 }
3592
Tony Mantler2558b502013-07-09 10:53:34 -07003593 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003594 // Public no-args constructor needed for fragment re-instantiation
3595 public DiscardConfirmDialogFragment() {}
3596
Tony Mantler2558b502013-07-09 10:53:34 -07003597 @Override
3598 public Dialog onCreateDialog(Bundle savedInstanceState) {
3599 return new AlertDialog.Builder(getActivity())
3600 .setMessage(R.string.confirm_discard_text)
3601 .setPositiveButton(R.string.discard,
3602 new DialogInterface.OnClickListener() {
3603 @Override
3604 public void onClick(DialogInterface dialog, int which) {
3605 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3606 }
3607 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003608 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003609 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003610 }
3611 }
3612
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003613 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003614 final DialogFragment frag = new DiscardConfirmDialogFragment();
3615 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003616 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003617 /**
3618 * Effectively discard the current message.
3619 *
3620 * This method is either invoked from the menu or from the dialog
3621 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003622 */
Tony Mantler2558b502013-07-09 10:53:34 -07003623 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003624 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003625 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3626 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003627 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003628 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003629 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3630 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003631 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003632 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003633 // This is not strictly necessary (since we should not try to
3634 // save the draft after calling this) but it ensures that if we
3635 // do save again for some reason we make a new draft rather than
3636 // trying to resave an expunged draft.
3637 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3638 }
3639 }
3640
Tony Mantler2558b502013-07-09 10:53:34 -07003641 // Display a toast to let the user know
3642 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003643
3644 // This prevents the draft from being saved in onPause().
3645 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003646 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003647 finish();
3648 }
3649
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003650 private void saveIfNeeded() {
3651 if (mAccount == null) {
3652 // We have not chosen an account yet so there's no way that we can save. This is ok,
3653 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3654 // user has not interacted with us yet and there is no real state to save.
3655 return;
3656 }
3657
3658 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003659 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003660 }
3661 }
3662
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003663 @Override
3664 public void onAttachmentDeleted() {
3665 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003666 // If we are showing any attachments, make sure we have an upper
3667 // divider.
3668 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003669 updateSaveUi();
3670 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003671
mindyp40882432012-09-06 11:07:40 -07003672 @Override
3673 public void onAttachmentAdded() {
3674 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3675 mAttachmentsView.focusLastAttachment();
3676 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003677
3678 /**
3679 * This is called any time one of our text fields changes.
3680 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003681 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003682 public void afterTextChanged(Editable s) {
3683 mTextChanged = true;
3684 updateSaveUi();
3685 }
3686
3687 @Override
3688 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3689 // Do nothing.
3690 }
3691
Marc Blank0bbc8582012-04-23 15:07:57 -07003692 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003693 public void onTextChanged(CharSequence s, int start, int before, int count) {
3694 // Do nothing.
3695 }
3696
3697
3698 // There is a big difference between the text associated with an address changing
3699 // to add the display name or to format properly and a recipient being added or deleted.
3700 // Make sure we only notify of changes when a recipient has been added or deleted.
3701 private class RecipientTextWatcher implements TextWatcher {
3702 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3703
3704 private RecipientEditTextView mView;
3705
3706 private TextWatcher mListener;
3707
3708 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3709 mView = view;
3710 mListener = listener;
3711 }
3712
3713 @Override
3714 public void afterTextChanged(Editable s) {
3715 if (hasChanged()) {
3716 mListener.afterTextChanged(s);
3717 }
3718 }
3719
3720 private boolean hasChanged() {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003721 final ArrayList<String> currRecips = buildEmailAddressList(getAddressesFromList(mView));
3722 int totalCount = currRecips.size();
Mindy Pereira75f66632012-01-11 11:42:02 -08003723 int totalPrevCount = 0;
3724 for (Entry<String, Integer> entry : mContent.entrySet()) {
3725 totalPrevCount += entry.getValue();
3726 }
3727 if (totalCount != totalPrevCount) {
3728 return true;
3729 }
3730
3731 for (String recip : currRecips) {
3732 if (!mContent.containsKey(recip)) {
3733 return true;
3734 } else {
3735 int count = mContent.get(recip) - 1;
3736 if (count < 0) {
3737 return true;
3738 } else {
3739 mContent.put(recip, count);
3740 }
3741 }
3742 }
3743 return false;
3744 }
3745
Mindy Pereira75f66632012-01-11 11:42:02 -08003746 @Override
3747 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003748 final ArrayList<String> recips = buildEmailAddressList(getAddressesFromList(mView));
Mindy Pereira75f66632012-01-11 11:42:02 -08003749 for (String recip : recips) {
3750 if (!mContent.containsKey(recip)) {
3751 mContent.put(recip, 1);
3752 } else {
3753 mContent.put(recip, (mContent.get(recip)) + 1);
3754 }
3755 }
3756 }
3757
3758 @Override
3759 public void onTextChanged(CharSequence s, int start, int before, int count) {
3760 // Do nothing.
3761 }
3762 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003763
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003764 /**
3765 * Returns a list of email addresses from the recipients. List only contains
3766 * email addresses strips additional info like the recipient's name.
3767 */
3768 private static ArrayList<String> buildEmailAddressList(String[] recips) {
3769 // Tokenize them all and put them in the list.
3770 final ArrayList<String> recipAddresses = Lists.newArrayListWithCapacity(recips.length);
3771 for (int i = 0; i < recips.length; i++) {
3772 recipAddresses.add(Rfc822Tokenizer.tokenize(recips[i])[0].getAddress());
3773 }
3774 return recipAddresses;
3775 }
3776
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003777 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3778 if (sTestSendOrSaveCallback != null && testCallback != null) {
3779 throw new IllegalStateException("Attempting to register more than one test callback");
3780 }
3781 sTestSendOrSaveCallback = testCallback;
3782 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003783
3784 @VisibleForTesting
3785 protected ArrayList<Attachment> getAttachments() {
3786 return mAttachmentsView.getAttachments();
3787 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003788
3789 @Override
3790 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3791 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003792 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3793 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3794 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003795 case REFERENCE_MESSAGE_LOADER:
3796 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3797 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003798 case LOADER_ACCOUNT_CURSOR:
3799 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3800 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003801 }
3802 return null;
3803 }
3804
3805 @Override
3806 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003807 int id = loader.getId();
3808 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003809 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003810 if (data != null && data.moveToFirst()) {
3811 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003812 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003813 initFromRefMessage(mComposeMode);
3814 finishSetup(mComposeMode, intent, null);
3815 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003816 String to = intent.getStringExtra(EXTRA_TO);
3817 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003818 mRefMessage.setTo(null);
3819 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003820 clearChangeListeners();
3821 mTo.append(to);
3822 initChangeListeners();
3823 }
3824 }
3825 } else {
3826 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003827 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003828 break;
Alice Yanga990a712013-03-13 18:37:00 -07003829 case REFERENCE_MESSAGE_LOADER:
3830 // Only populate mRefMessage and leave other fields untouched.
3831 if (data != null && data.moveToFirst()) {
3832 mRefMessage = new Message(data);
3833 }
Andy Huang9f855d62013-05-30 17:15:03 -07003834 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003835 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003836 case LOADER_ACCOUNT_CURSOR:
3837 if (data != null && data.moveToFirst()) {
3838 // there are accounts now!
3839 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003840 final ArrayList<Account> accounts = new ArrayList<Account>();
3841 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003842 do {
Ray Chen4b0c0122014-07-11 15:24:54 +02003843 account = Account.builder().buildFrom(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003844 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003845 initializedAccounts.add(account);
3846 }
3847 accounts.add(account);
3848 } while (data.moveToNext());
3849 if (initializedAccounts.size() > 0) {
3850 findViewById(R.id.wait).setVisibility(View.GONE);
3851 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3852 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003853 mAccounts = initializedAccounts.toArray(
3854 new Account[initializedAccounts.size()]);
3855
Mindy Pereirab199d172012-08-13 11:04:03 -07003856 finishCreate();
3857 invalidateOptionsMenu();
3858 } else {
3859 // Show "waiting"
3860 account = accounts.size() > 0 ? accounts.get(0) : null;
3861 showWaitFragment(account);
3862 }
3863 }
3864 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003865 }
3866 }
3867
Mindy Pereirab199d172012-08-13 11:04:03 -07003868 private void showWaitFragment(Account account) {
3869 WaitFragment fragment = getWaitFragment();
3870 if (fragment != null) {
3871 fragment.updateAccount(account);
3872 } else {
3873 findViewById(R.id.wait).setVisibility(View.VISIBLE);
Andy Huangc96efcc2014-04-09 15:30:42 -07003874 replaceFragment(WaitFragment.newInstance(account, false /* expectingMessages */),
Mindy Pereirab199d172012-08-13 11:04:03 -07003875 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3876 }
3877 }
3878
3879 private WaitFragment getWaitFragment() {
3880 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3881 }
3882
3883 private int replaceFragment(Fragment fragment, int transition, String tag) {
3884 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003885 fragmentTransaction.setTransition(transition);
3886 fragmentTransaction.replace(R.id.wait, fragment, tag);
3887 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3888 return transactionId;
3889 }
3890
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003891 @Override
3892 public void onLoaderReset(Loader<Cursor> arg0) {
3893 // Do nothing.
3894 }
Jin Cao77b4c2c2014-05-20 13:55:53 -07003895
3896 /**
3897 * Background task to convert the message's html to Spanned.
3898 */
3899 private class HtmlToSpannedTask extends AsyncTask<String, Void, Spanned> {
3900
3901 @Override
3902 protected Spanned doInBackground(String... input) {
Andy Huang9ed742c2014-06-18 02:34:50 -07003903 return HtmlUtils.htmlToSpan(input[0], mSpanConverterFactory);
Jin Cao77b4c2c2014-05-20 13:55:53 -07003904 }
3905
3906 @Override
3907 protected void onPostExecute(Spanned spanned) {
3908 mBodyView.removeTextChangedListener(ComposeActivity.this);
3909 mBodyView.setText(spanned);
3910 mTextChanged = false;
3911 mBodyView.addTextChangedListener(ComposeActivity.this);
3912 }
3913 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003914}