blob: b50bf67600e0eefa54c5ba9cf85281472133d0b5 [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;
Jin Caoadea2c82014-08-28 12:25:52 -070045import android.os.Environment;
Mindy Pereira82cc5662012-01-09 17:29:30 -080046import android.os.Handler;
47import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070048import android.os.ParcelFileDescriptor;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.provider.BaseColumns;
Alan Lau439aa5d2014-05-27 17:57:13 -070050import android.support.v4.app.RemoteInput;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -070051import android.support.v7.app.ActionBar;
52import android.support.v7.app.ActionBarActivity;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080053import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080054import android.text.Html;
Andy Huangff017272014-06-18 00:27:35 -070055import android.text.SpanWatcher;
mindyped9c2f02012-10-12 10:02:08 -070056import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080057import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080058import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080059import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080060import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080061import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070062import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070063import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080064import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080065import android.view.Menu;
66import android.view.MenuInflater;
67import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080068import android.view.View;
69import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070070import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080071import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070072import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080073import android.widget.ArrayAdapter;
Mindy Pereira433b1982012-04-03 11:53:07 -070074import android.widget.EditText;
Jin Cao36e23872014-07-29 13:41:12 -070075import android.widget.ScrollView;
Mindy Pereira6349a042012-01-04 11:25:01 -080076import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080077import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080078
Mindy Pereirac17d0732011-12-29 10:46:19 -080079import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070080import com.android.common.contacts.DataUsageStatUpdater;
Tony Mantler821e5782014-01-06 15:33:43 -080081import com.android.emailcommon.mail.Address;
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -070082import com.android.ex.chips.BaseRecipientAdapter;
83import com.android.ex.chips.DropdownChipLayouter;
Andy Huang5c5fd572012-04-08 18:19:29 -070084import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080085import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070086import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070087import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070088import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070089import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080090import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080091import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080092import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080093import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080094import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080095import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070096import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080097import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080098import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070099import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800100import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -0800101import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -0700102import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -0700103import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -0700104import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700105import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -0700106import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -0700107import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -0700108import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800109import com.android.mail.utils.ContentProviderTask;
Jin Cao77b4c2c2014-05-20 13:55:53 -0700110import com.android.mail.utils.HtmlUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -0700111import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800112import com.android.mail.utils.LogUtils;
Alan Lau15490232014-03-06 14:53:14 -0800113import com.android.mail.utils.NotificationActionUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800114import com.android.mail.utils.Utils;
Andy Huang9ed742c2014-06-18 02:34:50 -0700115import com.google.android.mail.common.html.parser.HtmlTree;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800116import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800117import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800118import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800119
Jin Caoadea2c82014-08-28 12:25:52 -0700120import java.io.File;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700121import java.io.FileNotFoundException;
122import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700123import java.io.UnsupportedEncodingException;
124import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800125import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700126import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800127import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800128import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800129import java.util.HashSet;
130import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700131import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700132import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800133import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800134
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700135public class ComposeActivity extends ActionBarActivity
136 implements OnClickListener, ActionBar.OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700137 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700138 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
Andrew Sappersteinffd61552014-05-14 15:04:23 -0700139 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
Jin Caoc5c550a2014-07-29 11:53:17 -0700140 RecipientEditTextView.RecipientEntryItemClickedListener, View.OnFocusChangeListener {
Scott Kennedya0287a82014-04-07 14:30:13 -0700141 /**
142 * An {@link Intent} action that launches {@link ComposeActivity}, but is handled as if the
143 * {@link Activity} were launched with no special action.
144 */
145 private static final String ACTION_LAUNCH_COMPOSE =
146 "com.android.mail.intent.action.LAUNCH_COMPOSE";
147
Mindy Pereira6349a042012-01-04 11:25:01 -0800148 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700149 public static final int COMPOSE = -1;
150 public static final int REPLY = 0;
151 public static final int REPLY_ALL = 1;
152 public static final int FORWARD = 2;
153 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800154
155 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700156 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800157
Mindy Pereira326689d2012-05-17 10:14:14 -0700158 private static final String EXTRA_SHOW_CC = "showCc";
159 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800160 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800161 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700162
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700163 private static final String UTF8_ENCODING_NAME = "UTF-8";
164
165 private static final String MAIL_TO = "mailto";
166
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700167 private static final String EXTRA_SUBJECT = "subject";
168
169 private static final String EXTRA_BODY = "body";
170
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700171 /**
172 * Expected to be html formatted text.
173 */
174 private static final String EXTRA_QUOTED_TEXT = "quotedText";
175
mindypd27b6ea2012-10-05 09:43:49 -0700176 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700177
Mark Wei62066e42012-09-13 12:07:02 -0700178 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
179
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700180 // Extra that we can get passed from other activities
Tony Mantler184ec732013-10-24 13:13:49 -0700181 @VisibleForTesting
182 protected static final String EXTRA_TO = "to";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700183 private static final String EXTRA_CC = "cc";
184 private static final String EXTRA_BCC = "bcc";
185
Scott Kennedy60847252013-08-15 15:55:42 -0700186 /**
187 * An optional extra containing a {@link ContentValues} of values to be added to
188 * {@link SendOrSaveMessage#mValues}.
189 */
190 public static final String EXTRA_VALUES = "extra-values";
191
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700192 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700193 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
194 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700195
Alan Lau439aa5d2014-05-27 17:57:13 -0700196 private static final String LEGACY_WEAR_EXTRA = "com.google.android.wearable.extras";
197
Andrew Sapperstein09da9422014-05-30 09:48:08 -0700198 /**
199 * Constant value for the threshold to use for auto-complete suggestions
200 * for the to/cc/bcc fields.
201 */
202 private static final int COMPLETION_THRESHOLD = 1;
203
Mindy Pereira82cc5662012-01-09 17:29:30 -0800204 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
205 // Map containing information about requests to create new messages, and the id of the
206 // messages that were the result of those requests.
207 //
208 // This map is used when the activity that initiated the save a of a new message, is killed
209 // before the save has completed (and when we know the id of the newly created message). When
210 // a save is completed, the service that is running in the background, will update the map
211 //
212 // When a new ComposeActivity instance is created, it will attempt to use the information in
213 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
214 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
215 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700216 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800217 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800218 /**
219 * Notifies the {@code Activity} that the caller is an Email
220 * {@code Activity}, so that the back behavior may be modified accordingly.
221 *
222 * @see #onAppUpPressed
223 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700224 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800225
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700226 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700227
Scott Kennedy5680ec22013-01-07 13:15:20 -0800228 /** If set, we will clear notifications for this folder. */
229 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
Alan Laue806c942014-06-06 16:19:15 -0700230 public static final String EXTRA_NOTIFICATION_CONVERSATION = "extra-notification-conversation";
Scott Kennedy5680ec22013-01-07 13:15:20 -0800231
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800232 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700233 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700234 // If this is a reply/forward then this extra will hold a uri we must query
235 // to get the original message.
236 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700237 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700238 // draft message
239 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800240 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700241 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800242 // Request numbers for activities we start
243 private static final int RESULT_PICK_ATTACHMENT = 1;
244 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700245 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700246 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700247
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700248 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
249 private static final String EXTRA_REQUEST_ID = "requestId";
250 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700251 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700252 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700253 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700254 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700255 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700256 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700257 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700258 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700259 private static final String MIME_TYPE_PHOTO = "image/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800260
Andy Huang9f855d62013-05-30 17:15:03 -0700261 private static final String KEY_INNER_SAVED_STATE = "compose_state";
262
Jin Caoadea2c82014-08-28 12:25:52 -0700263 // A single thread for running tasks in the background.
264 private static final Handler SEND_SAVE_TASK_HANDLER;
265 // String representing the uri of the data directory (used for attachment uri checking).
266 private static final String DATA_DIRECTORY_ROOT;
267
268 // Static initializations
Jin Cao5134be52014-05-06 19:18:38 -0700269 static {
270 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
271 handlerThread.start();
Jin Cao5134be52014-05-06 19:18:38 -0700272 SEND_SAVE_TASK_HANDLER = new Handler(handlerThread.getLooper());
Jin Caoadea2c82014-08-28 12:25:52 -0700273
274 DATA_DIRECTORY_ROOT = Environment.getDataDirectory().toString();
Jin Cao5134be52014-05-06 19:18:38 -0700275 }
276
Jin Cao36e23872014-07-29 13:41:12 -0700277 private ScrollView mScrollView;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800278 private RecipientEditTextView mTo;
279 private RecipientEditTextView mCc;
280 private RecipientEditTextView mBcc;
Jin Cao9d358a12014-07-24 12:15:38 -0700281 private View mCcBccButton;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800282 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800283 private AttachmentsView mAttachmentsView;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700284 protected Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700285 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800286 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800287 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800288 private TextView mSubject;
289
Mindy Pereira326c6602012-01-04 15:32:42 -0800290 private ComposeModeAdapter mComposeModeAdapter;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700291 protected int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800292 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800293 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700294 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800295 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800296 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800297 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700298 @VisibleForTesting
299 protected FromAddressSpinner mFromSpinner;
Andy Huang5f082212014-06-11 22:19:21 -0700300 protected boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800301 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800302 private boolean mTextChanged;
303 private boolean mReplyFromChanged;
304 private MenuItem mSave;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700305 @VisibleForTesting
306 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800307 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
308 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800309 private ReplyFromAccount mDraftAccount;
Tony Mantler581edd42014-02-18 15:41:22 -0800310 private final Object mDraftLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800311
Mindy Pereira326c6602012-01-04 15:32:42 -0800312 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700313 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
314 */
315 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700316 private RecipientTextWatcher mToListener;
317 private RecipientTextWatcher mCcListener;
318 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700319 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700320 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700321 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700322 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700323
mindyp1623f9b2012-11-21 12:41:16 -0800324 // Array of the outstanding send or save tasks. Access is synchronized
325 // with the object itself
326 /* package for testing */
327 @VisibleForTesting
Tony Mantler581edd42014-02-18 15:41:22 -0800328 public final ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
mindyp1623f9b2012-11-21 12:41:16 -0800329 // FIXME: this variable is never read. related to sRequestMessageIdMap.
330 private int mRequestId;
331 private String mSignature;
332 private Account[] mAccounts;
333 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700334 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800335
Andy Huang9ed742c2014-06-18 02:34:50 -0700336 private final HtmlTree.ConverterFactory mSpanConverterFactory =
337 new HtmlTree.ConverterFactory() {
338 @Override
339 public HtmlTree.Converter<Spanned> createInstance() {
340 return getSpanConverter();
341 }
342 };
343
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700344 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800345 * Can be called from a non-UI thread.
346 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800347 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700348 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
349 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800350 }
351
Mindy Pereira6349a042012-01-04 11:25:01 -0800352 /**
353 * Can be called from a non-UI thread.
354 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800355 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700356 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800357 }
358
359 /**
360 * Can be called from a non-UI thread.
361 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700362 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700363 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
364 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700365 }
366
367 /**
368 * Can be called from a non-UI thread.
369 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700370 public static void composeWithExtraValues(Context launcher, Account account,
371 String subject, final ContentValues extraValues) {
372 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
373 }
374
375 /**
376 * Can be called from a non-UI thread.
377 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800378 public static Intent createReplyIntent(final Context launcher, final Account account,
379 final Uri messageUri, final boolean isReplyAll) {
380 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
381 }
382
383 /**
384 * Can be called from a non-UI thread.
385 */
386 public static Intent createForwardIntent(final Context launcher, final Account account,
387 final Uri messageUri) {
388 return createActionIntent(launcher, account, messageUri, FORWARD);
389 }
390
Scott Kennedya0287a82014-04-07 14:30:13 -0700391 private static Intent createActionIntent(final Context context, final Account account,
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800392 final Uri messageUri, final int action) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700393 final Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
394 intent.setPackage(context.getPackageName());
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800395
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700396 updateActionIntent(account, messageUri, action, intent);
397
398 return intent;
399 }
400
401 @VisibleForTesting
402 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800403 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
404 intent.putExtra(EXTRA_ACTION, action);
405 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
406 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
407
408 return intent;
409 }
410
411 /**
412 * Can be called from a non-UI thread.
413 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800414 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700415 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800416 }
417
418 /**
419 * Can be called from a non-UI thread.
420 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800421 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700422 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
423 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800424 }
425
426 /**
427 * Can be called from a non-UI thread.
428 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800429 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700430 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800431 }
432
Alice Yang1ebc2db2013-03-14 21:21:44 -0700433 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
434 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700435 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700436 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700437 }
438
Scott Kennedya0287a82014-04-07 14:30:13 -0700439 private static void launch(Context context, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700440 String toAddress, String body, String quotedText, String subject,
441 final ContentValues extraValues) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700442 Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
443 intent.setPackage(context.getPackageName());
Mindy Pereira6349a042012-01-04 11:25:01 -0800444 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
445 intent.putExtra(EXTRA_ACTION, action);
446 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700447 if (action == EDIT_DRAFT) {
448 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
449 } else {
450 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
451 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700452 if (toAddress != null) {
453 intent.putExtra(EXTRA_TO, toAddress);
454 }
455 if (body != null) {
456 intent.putExtra(EXTRA_BODY, body);
457 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700458 if (quotedText != null) {
459 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
460 }
461 if (subject != null) {
462 intent.putExtra(EXTRA_SUBJECT, subject);
463 }
Scott Kennedy60847252013-08-15 15:55:42 -0700464 if (extraValues != null) {
465 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
466 intent.putExtra(EXTRA_VALUES, extraValues);
467 }
Andy Huange0f03202014-06-13 17:34:49 -0700468 if (action == COMPOSE) {
469 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
470 } else if (message != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700471 intent.setData(Utils.normalizeUri(message.uri));
Andy Huange0f03202014-06-13 17:34:49 -0700472 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700473 context.startActivity(intent);
Mindy Pereira6349a042012-01-04 11:25:01 -0800474 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800475
Scott Kennedya0287a82014-04-07 14:30:13 -0700476 public static void composeMailto(Context context, Account account, Uri mailto) {
477 final Intent intent = new Intent(Intent.ACTION_VIEW, mailto);
478 intent.setPackage(context.getPackageName());
Andy Huang0a2a3462013-12-20 15:56:13 -0800479 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
480 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Andy Huange0f03202014-06-13 17:34:49 -0700481 if (mailto != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700482 intent.setData(Utils.normalizeUri(mailto));
Andy Huange0f03202014-06-13 17:34:49 -0700483 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700484 context.startActivity(intent);
Andy Huang0a2a3462013-12-20 15:56:13 -0800485 }
486
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800487 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700488 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800489 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800490 setContentView(R.layout.compose);
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700491 final ActionBar actionBar = getSupportActionBar();
Paul Westbrook4def3bf2014-07-01 00:38:17 -0700492 if (actionBar != null) {
493 // Hide the app icon.
Paul Westbrook5043cc22014-06-28 05:04:21 -0700494 actionBar.setIcon(null);
Paul Westbrook4def3bf2014-07-01 00:38:17 -0700495 actionBar.setDisplayUseLogoEnabled(false);
Paul Westbrook5043cc22014-06-28 05:04:21 -0700496 }
497
Andy Huang9f855d62013-05-30 17:15:03 -0700498 mInnerSavedState = (savedInstanceState != null) ?
499 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700500 checkValidAccounts();
501 }
502
503 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700504 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800505 findViews();
Tony Mantler581edd42014-02-18 15:41:22 -0800506 final Intent intent = getIntent();
507 final Message message;
508 final ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700509 mShowQuotedText = false;
Tony Mantler581edd42014-02-18 15:41:22 -0800510 final CharSequence quotedText;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700511 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700512 // Check for any of the possibly supplied accounts.;
Tony Mantler581edd42014-02-18 15:41:22 -0800513 final Account account;
Andy Huang9f855d62013-05-30 17:15:03 -0700514 if (hadSavedInstanceStateMessage(savedState)) {
515 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
516 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
Tony Mantler581edd42014-02-18 15:41:22 -0800517 message = savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700518
Andy Huang9f855d62013-05-30 17:15:03 -0700519 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800520 mRefMessage = savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700521 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700522
523 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700524 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700525 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700526 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
527 // Initialize the message from the message in the intent
Tony Mantler581edd42014-02-18 15:41:22 -0800528 message = intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700529 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800530 mRefMessage = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
531 mRefMessageUri = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
532 quotedText = null;
Andy Huang4fe0af82013-08-20 17:24:51 -0700533
534 if (Analytics.isLoggable()) {
535 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
536 Analytics.getInstance().sendEvent(
537 "notification_action", "compose", getActionString(action), 0);
538 }
539 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700540 }
Mark Wei62066e42012-09-13 12:07:02 -0700541 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700542
543 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800544 if (mAccount == null) {
545 return;
546 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700547
Scott Kennedyfe853d32013-06-19 11:47:35 -0700548 initRecipients();
549
Scott Kennedy5680ec22013-01-07 13:15:20 -0800550 // Clear the notification and mark the conversation as seen, if necessary
551 final Folder notificationFolder =
552 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800553
Alan Laue806c942014-06-06 16:19:15 -0700554 if (notificationFolder != null) {
555 final Uri conversationUri = intent.getParcelableExtra(EXTRA_NOTIFICATION_CONVERSATION);
556 Intent actionIntent;
557 if (conversationUri != null) {
558 actionIntent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS_WEAR);
559 actionIntent.putExtra(Utils.EXTRA_CONVERSATION, conversationUri);
560 } else {
561 actionIntent = new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
562 actionIntent.setData(Utils.appendVersionQueryParameter(this,
563 notificationFolder.folderUri.fullUri));
564 }
565 actionIntent.setPackage(getPackageName());
566 actionIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
567 actionIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
568
569 startService(actionIntent);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800570 }
571
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700572 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
573 mLaunchedFromEmail = true;
574 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
575 final Uri dataUri = intent.getData();
576 if (dataUri != null) {
577 final String dataScheme = intent.getData().getScheme();
578 final String accountScheme = mAccount.composeIntentUri.getScheme();
579 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
580 }
581 }
582
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700583 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700584 mShowQuotedText = true;
585 mComposeMode = action;
Alan Lau15490232014-03-06 14:53:14 -0800586
587 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Alan Lau575255c2014-05-16 11:44:27 -0700588 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
Alan Lau439aa5d2014-05-27 17:57:13 -0700589 String wearReply = null;
Alan Lau575255c2014-05-16 11:44:27 -0700590 if (remoteInput != null) {
Alan Lau439aa5d2014-05-27 17:57:13 -0700591 LogUtils.d(LOG_TAG, "Got remote input from new api");
592 CharSequence input = remoteInput.getCharSequence(
Alan Lau575255c2014-05-16 11:44:27 -0700593 NotificationActionUtils.WEAR_REPLY_INPUT);
Alan Lau439aa5d2014-05-27 17:57:13 -0700594 if (input != null) {
595 wearReply = input.toString();
Alan Lau15490232014-03-06 14:53:14 -0800596 }
Alan Lau575255c2014-05-16 11:44:27 -0700597 } else {
Alan Lau439aa5d2014-05-27 17:57:13 -0700598 // TODO: remove after legacy code has been removed.
599 LogUtils.d(LOG_TAG,
600 "No remote input from new api, falling back to compatibility mode");
601 ClipData clipData = intent.getClipData();
602 if (clipData != null
603 && LEGACY_WEAR_EXTRA.equals(clipData.getDescription().getLabel())) {
604 Bundle extras = clipData.getItemAt(0).getIntent().getExtras();
605 if (extras != null) {
606 wearReply = extras.getString(NotificationActionUtils.WEAR_REPLY_INPUT);
607 }
608 }
609 }
610
611 if (!TextUtils.isEmpty(wearReply)) {
612 createWearReplyTask(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION,
613 mComposeMode, wearReply).execute();
614 finish();
615 return;
616 } else {
617 LogUtils.w(LOG_TAG, "remote input string is null");
Alan Lau15490232014-03-06 14:53:14 -0800618 }
619 }
620
Alice Yanga990a712013-03-13 18:37:00 -0700621 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700622 return;
623 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700624 initFromDraftMessage(message);
625 initQuotedTextFromRefMessage(mRefMessage, action);
Alice Yanga990a712013-03-13 18:37:00 -0700626 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700627 // if we should be showing quoted text but mRefMessage is null
628 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700629 if (mShowQuotedText && mRefMessage == null) {
630 if (quotedText != null) {
631 initQuotedText(quotedText, false /* shouldQuoteText */);
632 } else if (mExtraValues != null) {
633 initExtraValues(mExtraValues);
634 return;
635 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700636 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700637 } else if (action == EDIT_DRAFT) {
Tony Mantler581edd42014-02-18 15:41:22 -0800638 if (message == null) {
639 throw new IllegalStateException("Message must not be null to edit draft");
640 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700641 initFromDraftMessage(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700642 // Update the action to the draft type of the previous draft
643 switch (message.draftType) {
644 case UIProvider.DraftType.REPLY:
645 action = REPLY;
646 break;
647 case UIProvider.DraftType.REPLY_ALL:
648 action = REPLY_ALL;
649 break;
650 case UIProvider.DraftType.FORWARD:
651 action = FORWARD;
652 break;
653 case UIProvider.DraftType.COMPOSE:
654 default:
655 action = COMPOSE;
656 break;
657 }
Alice Yanga990a712013-03-13 18:37:00 -0700658 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
659
660 mShowQuotedText = message.appendRefMessageContent;
661 if (message.refMessageUri != null) {
662 // If we're editing an existing draft that was in reference to an existing message,
663 // still need to load that original message since we might need to refer to the
664 // original sender and recipients if user switches "reply <-> reply-all".
665 mRefMessageUri = message.refMessageUri;
666 mComposeMode = action;
667 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
668 return;
669 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700670 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
671 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800672 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700673 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700674 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700675 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700676 if (initFromExtras(intent)) {
677 return;
678 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700679 }
Alice Yanga990a712013-03-13 18:37:00 -0700680
681 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700682 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700683 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700684
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -0700685 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
Alan Lau15490232014-03-06 14:53:14 -0800686 private static AsyncTask<Void, Void, Message> createWearReplyTask(
687 final ComposeActivity composeActivity,
688 final Uri refMessageUri, final String[] projection, final int action,
689 final String wearReply) {
690 return new AsyncTask<Void, Void, Message>() {
691 private Intent mEmptyServiceIntent = new Intent(composeActivity, EmptyService.class);
692
693 @Override
694 protected void onPreExecute() {
695 // Start service so we won't be killed if this app is put in the background.
696 composeActivity.startService(mEmptyServiceIntent);
697 }
698
699 @Override
700 protected Message doInBackground(Void... params) {
701 Cursor cursor = composeActivity.getContentResolver()
702 .query(refMessageUri, projection, null, null, null, null);
703 if (cursor != null) {
704 try {
705 cursor.moveToFirst();
706 return new Message(cursor);
707 } finally {
708 cursor.close();
709 }
710 }
711 return null;
712 }
713
714 @Override
715 protected void onPostExecute(Message message) {
716 composeActivity.stopService(mEmptyServiceIntent);
717
718 composeActivity.mRefMessage = message;
719 composeActivity.initFromRefMessage(action);
720 composeActivity.setBody(wearReply, false);
721 composeActivity.finishSetup(action, composeActivity.getIntent(), null);
722 composeActivity.sendOrSaveWithSanityChecks(false /* save */, true /* show toast */,
723 false /* orientationChanged */, true /* autoSend */);
724 }
725 };
726 }
727
Mindy Pereirab199d172012-08-13 11:04:03 -0700728 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700729 final Account[] allAccounts = AccountUtils.getAccounts(this);
730 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700731 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
732 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700733 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700734 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
735 }
736 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700737 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700738 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700739 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700740 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700741 anySyncing = true;
742 break;
743 }
744 }
745 if (!anySyncing) {
746 // There are accounts, but none are sync'd, which is just like having no accounts.
747 mAccounts = null;
748 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
749 return;
750 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700751 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700752 finishCreate();
753 }
754 }
755
Mindy Pereira47d0e652012-07-23 09:45:07 -0700756 private Account obtainAccount(Intent intent) {
757 Account account = null;
758 Object accountExtra = null;
759 if (intent != null && intent.getExtras() != null) {
760 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
761 if (accountExtra instanceof Account) {
762 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700763 } else if (accountExtra instanceof String) {
764 // This is the Account attached to the widget compose intent.
Tony Mantler26a20752014-02-28 16:44:24 -0800765 account = Account.newInstance((String) accountExtra);
mindyp7ae042e2012-08-27 13:27:37 -0700766 if (account != null) {
767 return account;
768 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700769 }
mindyp5ee9dc42013-01-08 09:54:54 -0800770 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
771 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
772 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700773 }
Tony Mantler581edd42014-02-18 15:41:22 -0800774
775 MailAppProvider provider = MailAppProvider.getInstance();
776 String lastAccountUri = provider.getLastSentFromAccount();
777 if (TextUtils.isEmpty(lastAccountUri)) {
778 lastAccountUri = provider.getLastViewedAccount();
Mindy Pereira47d0e652012-07-23 09:45:07 -0700779 }
Tony Mantler581edd42014-02-18 15:41:22 -0800780 if (!TextUtils.isEmpty(lastAccountUri)) {
781 accountExtra = Uri.parse(lastAccountUri);
782 }
783
Mindy Pereirab199d172012-08-13 11:04:03 -0700784 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700785 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
786 // For backwards compatibility, we need to check account
787 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700788 for (Account a : mAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -0700789 if (a.getEmailAddress().equals(accountExtra)) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700790 account = a;
791 }
792 }
793 } else if (accountExtra instanceof Uri) {
794 // The uri of the last viewed account is what is stored in
795 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700796 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700797 if (a.uri.equals(accountExtra)) {
798 account = a;
799 }
800 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700801 }
802 if (account == null) {
803 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700804 }
805 }
806 return account;
807 }
808
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700809 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800810 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700811 // Don't bother with the intent if we have procured a message from the
812 // intent already.
813 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
814 initAttachmentsFromIntent(intent);
815 }
Alice Yanga990a712013-03-13 18:37:00 -0700816 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700817 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
818 action);
mindypd4a48662012-11-08 17:13:49 -0800819
820 // If this is a draft message, the draft account is whatever account was
821 // used to open the draft message in Compose.
822 if (mDraft != null) {
823 mDraftAccount = mReplyFromAccount;
824 }
825
Mindy Pereira75f66632012-01-11 11:42:02 -0800826 initChangeListeners();
Jin Cao32973b42014-05-06 16:12:11 -0700827
828 // These two should be identical since we check CC and BCC the same way
829 boolean showCc = !TextUtils.isEmpty(mCc.getText()) || (savedInstanceState != null &&
830 savedInstanceState.getBoolean(EXTRA_SHOW_CC));
831 boolean showBcc = !TextUtils.isEmpty(mBcc.getText()) || (savedInstanceState != null &&
832 savedInstanceState.getBoolean(EXTRA_SHOW_BCC));
833 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereira326689d2012-05-17 10:14:14 -0700834 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700835 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800836
Tony Mantler581edd42014-02-18 15:41:22 -0800837 mRespondedInline = mInnerSavedState != null &&
838 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE);
mindyp1623f9b2012-11-21 12:41:16 -0800839 if (mRespondedInline) {
840 mQuotedTextView.setVisibility(View.GONE);
841 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700842 }
843
Scott Kennedyff8553f2013-04-05 20:57:44 -0700844 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700845 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
846 }
847
Mindy Pereira71c9e562012-05-17 11:01:02 -0700848 private void updateHideOrShowQuotedText(boolean showQuotedText) {
849 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700850 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700851 }
852
853 private void setFocus(int action) {
854 if (action == EDIT_DRAFT) {
855 int type = mDraft.draftType;
856 switch (type) {
857 case UIProvider.DraftType.COMPOSE:
858 case UIProvider.DraftType.FORWARD:
859 action = COMPOSE;
860 break;
861 case UIProvider.DraftType.REPLY:
862 case UIProvider.DraftType.REPLY_ALL:
863 default:
864 action = REPLY;
865 break;
866 }
867 }
868 switch (action) {
869 case FORWARD:
870 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800871 if (TextUtils.isEmpty(mTo.getText())) {
872 mTo.requestFocus();
873 break;
874 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700875 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700876 case REPLY:
877 case REPLY_ALL:
878 default:
879 focusBody();
880 break;
881 }
882 }
883
884 /**
885 * Focus the body of the message.
886 */
Tony Mantler6a7ac782014-02-19 15:22:02 -0800887 private void focusBody() {
Mindy Pereira433b1982012-04-03 11:53:07 -0700888 mBodyView.requestFocus();
Tony Mantler6a7ac782014-02-19 15:22:02 -0800889 resetBodySelection();
890 }
Mindy Pereira433b1982012-04-03 11:53:07 -0700891
Tony Mantler6a7ac782014-02-19 15:22:02 -0800892 private void resetBodySelection() {
893 int length = mBodyView.getText().length();
Mindy Pereira433b1982012-04-03 11:53:07 -0700894 int signatureStartPos = getSignatureStartPosition(
895 mSignature, mBodyView.getText().toString());
896 if (signatureStartPos > -1) {
897 // In case the user deleted the newlines...
898 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700899 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700900 // Move cursor to the end.
901 mBodyView.setSelection(length);
902 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800903 }
904
905 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700906 protected void onStart() {
907 super.onStart();
908
909 Analytics.getInstance().activityStart(this);
910 }
911
912 @Override
913 protected void onStop() {
914 super.onStop();
915
916 Analytics.getInstance().activityStop(this);
917 }
918
919 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800920 protected void onResume() {
921 super.onResume();
922 // Update the from spinner as other accounts
923 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800924 if (mFromSpinner != null && mAccount != null) {
Andrew Sappersteina01ddca2014-03-04 10:59:56 -0800925 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800926 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800927 }
928
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800929 @Override
930 protected void onPause() {
931 super.onPause();
932
Mindy Pereiraa2148332012-07-02 13:54:14 -0700933 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700934 // Don't save unnecessary drafts if we are only changing the orientation.
935 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700936 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700937
Andy Huange003b4c2013-08-16 10:32:05 -0700938 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700939 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
940 // because that method can be invoked many times in a single compose session.)
941 logSendOrSave(true /* save */);
942 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700943 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800944 }
945
946 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700947 protected void onActivityResult(int request, int result, Intent data) {
Andy Huang5f082212014-06-11 22:19:21 -0700948 if (request == RESULT_PICK_ATTACHMENT) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700949 mAddingAttachment = false;
Andy Huang5f082212014-06-11 22:19:21 -0700950 if (result == RESULT_OK) {
951 addAttachmentAndUpdateView(data);
952 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700953 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700954 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700955 if (result != RESULT_OK) {
956 finish();
957 } else {
958 // Watch for accounts to show up!
959 // restart the loader to get the updated list of accounts
960 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
961 showWaitFragment(null);
962 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800963 }
964 }
965
966 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700967 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700968 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
969 if (hasAccounts) {
970 clearChangeListeners();
971 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700972 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700973 if (mInnerSavedState != null) {
974 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
975 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
976 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700977 // There should be a focus and it should be an EditText since we
978 // only save these extras if these conditions are true.
979 EditText focusEditText = (EditText) getCurrentFocus();
980 final int length = focusEditText.getText().length();
981 if (selectionStart < length && selectionEnd < length) {
982 focusEditText.setSelection(selectionStart, selectionEnd);
983 }
984 }
985 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700986 if (hasAccounts) {
987 initChangeListeners();
988 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700989 }
990
991 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700992 protected void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800993 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700994 final Bundle inner = new Bundle();
995 saveState(inner);
996 state.putBundle(KEY_INNER_SAVED_STATE, inner);
997 }
998
999 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -07001000 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
1001 if (mAccounts == null || mAccounts.length == 0) {
1002 return;
1003 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001004 // The framework is happy to save and restore the selection but only if it also saves and
1005 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
1006 // this manually.
1007 View focus = getCurrentFocus();
1008 if (focus != null && focus instanceof EditText) {
1009 EditText focusEditText = (EditText) focus;
1010 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
1011 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
1012 }
Paul Westbrook6273e962012-04-23 10:44:15 -07001013
1014 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -07001015 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -07001016 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
1017 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
1018 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001019 if (selectedReplyFromAccount != null) {
1020 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
1021 .toString());
1022 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
1023 } else {
1024 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
1025 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001026
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001027 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
1028 // We don't have a draft id, and we have a request id,
1029 // save the request id.
1030 state.putInt(EXTRA_REQUEST_ID, mRequestId);
1031 }
1032
1033 // We want to restore the current mode after a pause
1034 // or rotation.
1035 int mode = getMode();
1036 state.putInt(EXTRA_ACTION, mode);
1037
Jin Cao77b4c2c2014-05-20 13:55:53 -07001038 final Message message = createMessage(selectedReplyFromAccount, mRefMessage, mode,
1039 removeComposingSpans(mBodyView.getText()));
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001040 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -08001041 message.id = mDraft.id;
1042 message.serverId = mDraft.serverId;
1043 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001044 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001045 state.putParcelable(EXTRA_MESSAGE, message);
1046
1047 if (mRefMessage != null) {
1048 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001049 } else if (message.appendRefMessageContent) {
1050 // If we have no ref message but should be appending
1051 // ref message content, we have orphaned quoted text. Save it.
1052 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001053 }
Mindy Pereira326689d2012-05-17 10:14:14 -07001054 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
1055 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -08001056 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -08001057 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -07001058 state.putParcelableArrayList(
1059 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -07001060
1061 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001062 }
1063
1064 private int getMode() {
1065 int mode = ComposeActivity.COMPOSE;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001066 final ActionBar actionBar = getSupportActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001067 if (actionBar != null
1068 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001069 mode = actionBar.getSelectedNavigationIndex();
1070 }
1071 return mode;
1072 }
1073
Jin Cao77b4c2c2014-05-20 13:55:53 -07001074 /**
1075 * This function might be called from a background thread, so be sure to move everything that
1076 * can potentially modify the UI to the main thread (e.g. removeComposingSpans for body).
1077 */
Anthony Lee2a3cc132014-04-22 14:15:25 -07001078 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, Message refMessage,
Jin Cao77b4c2c2014-05-20 13:55:53 -07001079 int mode, Spanned body) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001080 Message message = new Message();
1081 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -07001082 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001083 message.uri = null;
1084 message.conversationUri = null;
1085 message.subject = mSubject.getText().toString();
1086 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001087 message.setTo(formatSenders(mTo.getText().toString()));
1088 message.setCc(formatSenders(mCc.getText().toString()));
1089 message.setBcc(formatSenders(mBcc.getText().toString()));
1090 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001091 message.dateReceivedMs = 0;
Jin Cao77b4c2c2014-05-20 13:55:53 -07001092 message.bodyHtml = spannedBodyToHtml(body, true);
Jin Cao7800d292014-08-24 11:31:15 -07001093 message.bodyText = body.toString();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001094 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -07001095 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001096 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
1097 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
1098 message.hasAttachments = attachments != null && attachments.size() > 0;
1099 message.attachmentListUri = null;
1100 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001101 message.alwaysShowImages = false;
1102 message.attachmentsJson = Attachment.toJSONArray(attachments);
1103 CharSequence quotedText = mQuotedTextView.getQuotedText();
Anthony Lee2a3cc132014-04-22 14:15:25 -07001104 message.quotedTextOffset = -1; // Just a default value.
1105 if (refMessage != null && !TextUtils.isEmpty(quotedText)) {
1106 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1107 // We want the index to point to just the quoted text and not the
1108 // "On December 25, 2014..." part of it.
1109 message.quotedTextOffset =
1110 QuotedTextView.getQuotedTextOffset(quotedText.toString());
1111 } else if (!TextUtils.isEmpty(refMessage.bodyText)) {
1112 // We want to point to the entire quoted text.
1113 message.quotedTextOffset = QuotedTextView.findQuotedTextIndex(quotedText);
1114 }
1115 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001116 message.accountUri = null;
Greg Bullock14fd3042014-08-12 09:21:15 +02001117 message.setFrom(computeFromForAccount(selectedReplyFromAccount));
1118 message.draftType = getDraftType(mode);
1119 return message;
1120 }
1121
1122 protected String computeFromForAccount(ReplyFromAccount selectedReplyFromAccount) {
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001123 final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
1124 : mAccount != null ? mAccount.getEmailAddress() : null;
Tony Mantlerf441d142013-10-22 11:46:00 -07001125 final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
1126 : mAccount != null ? mAccount.getSenderName() : null;
Tony Mantler821e5782014-01-06 15:33:43 -08001127 final Address address = new Address(email, senderName);
Greg Bullock14fd3042014-08-12 09:21:15 +02001128 return address.toHeader();
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001129 }
1130
Scott Kennedyff8553f2013-04-05 20:57:44 -07001131 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -07001132 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
1133 return string.substring(0, string.length() - 1);
1134 }
1135 return string;
1136 }
1137
Mindy Pereira818143e2012-01-11 13:59:49 -08001138 @VisibleForTesting
Andy Huang91ede362014-01-21 19:16:00 -08001139 protected void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -07001140 if (account == null) {
1141 return;
1142 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001143 if (!account.equals(mAccount)) {
1144 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001145 mCachedSettings = mAccount.settings;
1146 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001147 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001148 if (mAccount != null) {
Tony Mantler79b11562013-10-09 15:31:50 -07001149 MailActivity.setNfcMessage(mAccount.getEmailAddress());
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001150 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001151 }
1152
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001153 private void initFromSpinner(Bundle bundle, int action) {
1154 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001155 action = COMPOSE;
1156 }
Andrew Sappersteina01ddca2014-03-04 10:59:56 -08001157 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001158
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001159 if (bundle != null) {
1160 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
1161 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
1162 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
1163 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001164 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001165 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
1166 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001167 }
1168 if (mReplyFromAccount == null) {
1169 if (mDraft != null) {
1170 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
1171 } else if (mRefMessage != null) {
1172 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
1173 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001174 }
1175 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -07001176 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001177 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001178
Mindy Pereira62de1b12012-04-06 12:17:56 -07001179 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001180
Mindy Pereira62de1b12012-04-06 12:17:56 -07001181 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001182 // If there is only 1 account, just show that account.
1183 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001184 // send mail from / save drafts to.
1185 mFromStatic.setVisibility(View.GONE);
Andy Huangca4676f2014-01-16 13:22:20 -08001186 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001187 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001188 } else {
1189 mFromStatic.setVisibility(View.VISIBLE);
Andy Huangca4676f2014-01-16 13:22:20 -08001190 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001191 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001192 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001193 }
1194
Mindy Pereira62de1b12012-04-06 12:17:56 -07001195 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1196 if (refMessage.accountUri != null) {
1197 // This must be from combined inbox.
1198 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1199 for (ReplyFromAccount from : replyFromAccounts) {
1200 if (from.account.uri.equals(refMessage.accountUri)) {
1201 return from;
1202 }
1203 }
1204 return null;
1205 } else {
1206 return getReplyFromAccount(account, refMessage);
1207 }
1208 }
1209
1210 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001211 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001212 * return who the message should be sent from.
1213 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001214 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001215 * @return the address from which to reply.
1216 */
1217 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1218 // First see if we are supposed to use the default address or
1219 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001220 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001221 return getDefaultReplyFromAccount(account);
1222 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001223 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001224 // all the message recipients and find one that matches
1225 // a custom from or account.
1226 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001227 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1228 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001229 return getMatchingRecipient(account, allRecipients);
1230 }
1231 }
1232
1233 /**
1234 * Compare all the recipients of an email to the current account and all
1235 * custom addresses associated with that account. Return the match if there
1236 * is one, or the default account if there isn't.
1237 */
1238 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1239 // Tokenize the list and place in a hashmap.
1240 ReplyFromAccount matchingReplyFrom = null;
1241 Rfc822Token[] tokens;
1242 HashSet<String> recipientsMap = new HashSet<String>();
1243 for (String address : sentTo) {
1244 tokens = Rfc822Tokenizer.tokenize(address);
Tony Mantler581edd42014-02-18 15:41:22 -08001245 for (final Rfc822Token token : tokens) {
1246 recipientsMap.add(token.getAddress());
Mindy Pereira62de1b12012-04-06 12:17:56 -07001247 }
1248 }
1249
1250 int matchingAddressCount = 0;
1251 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001252 customFroms = account.getReplyFroms();
1253 if (customFroms != null) {
1254 for (ReplyFromAccount entry : customFroms) {
1255 if (recipientsMap.contains(entry.address)) {
1256 matchingReplyFrom = entry;
1257 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001258 }
1259 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001260 }
1261 if (matchingAddressCount > 1) {
1262 matchingReplyFrom = getDefaultReplyFromAccount(account);
1263 }
1264 return matchingReplyFrom;
1265 }
1266
Scott Kennedyff8553f2013-04-05 20:57:44 -07001267 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1268 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001269 if (from.isDefault) {
1270 return from;
1271 }
1272 }
Tony Mantlerf441d142013-10-22 11:46:00 -07001273 return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1274 account.getSenderName(), account.getEmailAddress(), true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001275 }
1276
Tony Mantlerf441d142013-10-22 11:46:00 -07001277 private ReplyFromAccount getReplyFromAccountFromDraft(final Account account,
1278 final Message msg) {
1279 final Address[] draftFroms = Address.parse(msg.getFrom());
1280 final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
Mindy Pereira62de1b12012-04-06 12:17:56 -07001281 ReplyFromAccount replyFromAccount = null;
1282 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Tony Mantler79b11562013-10-09 15:31:50 -07001283 if (TextUtils.equals(account.getEmailAddress(), sender)) {
Tony Mantlerf441d142013-10-22 11:46:00 -07001284 replyFromAccount = getDefaultReplyFromAccount(account);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001285 } else {
1286 for (ReplyFromAccount fromAccount : replyFromAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -07001287 if (TextUtils.equals(fromAccount.address, sender)) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001288 replyFromAccount = fromAccount;
1289 break;
1290 }
1291 }
1292 }
1293 return replyFromAccount;
1294 }
1295
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001296 private void findViews() {
Jin Cao36e23872014-07-29 13:41:12 -07001297 mScrollView = (ScrollView) findViewById(R.id.compose);
1298 mScrollView.setVisibility(View.VISIBLE);
Jin Cao9d358a12014-07-24 12:15:38 -07001299 mCcBccButton = findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001300 if (mCcBccButton != null) {
1301 mCcBccButton.setOnClickListener(this);
1302 }
1303 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001304 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira818143e2012-01-11 13:59:49 -08001305 mTo = (RecipientEditTextView) findViewById(R.id.to);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001306 initializeRecipientEditTextView(mTo);
Jin Cao15f09d72014-08-08 13:27:34 -07001307 mTo.setAlternatePopupAnchor(findViewById(R.id.compose_to_dropdown_anchor));
Mindy Pereira818143e2012-01-11 13:59:49 -08001308 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001309 initializeRecipientEditTextView(mCc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001310 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001311 initializeRecipientEditTextView(mBcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001312 // TODO: add special chips text change watchers before adding
1313 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001314 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001315 mSubject.setOnEditorActionListener(this);
Jin Caoc5c550a2014-07-29 11:53:17 -07001316 mSubject.setOnFocusChangeListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001317 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1318 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001319 mBodyView = (EditText) findViewById(R.id.body);
Jin Caoc5c550a2014-07-29 11:53:17 -07001320 mBodyView.setOnFocusChangeListener(this);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001321 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001322 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001323 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001324 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Jin Cao95b8a7b2014-09-02 16:45:42 -07001325
1326 // Bottom placeholder to forward click events to the body
1327 findViewById(R.id.composearea_tap_trap_bottom).setOnClickListener(new OnClickListener() {
1328 @Override
1329 public void onClick(View v) {
1330 mBodyView.requestFocus();
1331 mBodyView.setSelection(mBodyView.getText().length());
1332 }
1333 });
Mindy Pereira6349a042012-01-04 11:25:01 -08001334 }
1335
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001336 private void initializeRecipientEditTextView(RecipientEditTextView view) {
1337 view.setTokenizer(new Rfc822Tokenizer());
1338 view.setThreshold(COMPLETION_THRESHOLD);
1339 }
1340
mindyp62d3ec72012-08-24 13:04:09 -07001341 @Override
1342 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1343 if (action == EditorInfo.IME_ACTION_DONE) {
1344 focusBody();
1345 return true;
1346 }
1347 return false;
1348 }
1349
Andy Huang91ede362014-01-21 19:16:00 -08001350 /**
1351 * Convert the body text (in {@link Spanned} form) to ready-to-send HTML format as a plain
1352 * String.
1353 *
1354 * @param body the body text including fancy style spans
Jin Cao77b4c2c2014-05-20 13:55:53 -07001355 * @param removedComposing whether the function already removed composingSpans. Necessary
1356 * because we cannot call removeComposingSpans from a background thread.
Andy Huang91ede362014-01-21 19:16:00 -08001357 * @return HTML formatted body that's suitable for sending or saving
1358 */
Jin Cao77b4c2c2014-05-20 13:55:53 -07001359 private String spannedBodyToHtml(Spanned body, boolean removedComposing) {
1360 if (!removedComposing) {
1361 body = removeComposingSpans(body);
1362 }
1363 final HtmlifyBeginResult r = onHtmlifyBegin(body);
Andy Huang91ede362014-01-21 19:16:00 -08001364 return onHtmlifyEnd(Html.toHtml(r.result), r.extras);
1365 }
1366
1367 /**
1368 * A hook for subclasses to convert custom spans in the body text prior to system HTML
1369 * conversion. That HTML conversion is lossy, so anything above and beyond its capability
1370 * has to be handled here.
1371 *
1372 * @param body
1373 * @return a copy of the body text with custom spans replaced with HTML
1374 */
1375 protected HtmlifyBeginResult onHtmlifyBegin(Spanned body) {
1376 return new HtmlifyBeginResult(body, null /* extras */);
1377 }
1378
1379 protected String onHtmlifyEnd(String html, Object extras) {
1380 return html;
1381 }
1382
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001383 protected TextView getBody() {
1384 return mBodyView;
1385 }
1386
1387 @VisibleForTesting
Andy Huang0a2a3462013-12-20 15:56:13 -08001388 public String getBodyHtml() {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001389 return spannedBodyToHtml(mBodyView.getText(), false);
Andy Huang0a2a3462013-12-20 15:56:13 -08001390 }
1391
1392 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001393 public Account getFromAccount() {
1394 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1395 mReplyFromAccount.account : mAccount;
1396 }
1397
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001398 private void clearChangeListeners() {
1399 mSubject.removeTextChangedListener(this);
1400 mBodyView.removeTextChangedListener(this);
1401 mTo.removeTextChangedListener(mToListener);
1402 mCc.removeTextChangedListener(mCcListener);
1403 mBcc.removeTextChangedListener(mBccListener);
1404 mFromSpinner.setOnAccountChangedListener(null);
1405 mAttachmentsView.setAttachmentChangesListener(null);
1406 }
1407
Mindy Pereira75f66632012-01-11 11:42:02 -08001408 // Now that the message has been initialized from any existing draft or
1409 // ref message data, set up listeners for any changes that occur to the
1410 // message.
1411 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001412 // Make sure we only add text changed listeners once!
1413 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001414 mSubject.addTextChangedListener(this);
1415 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001416 if (mToListener == null) {
1417 mToListener = new RecipientTextWatcher(mTo, this);
1418 }
1419 mTo.addTextChangedListener(mToListener);
1420 if (mCcListener == null) {
1421 mCcListener = new RecipientTextWatcher(mCc, this);
1422 }
1423 mCc.addTextChangedListener(mCcListener);
1424 if (mBccListener == null) {
1425 mBccListener = new RecipientTextWatcher(mBcc, this);
1426 }
1427 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001428 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001429 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001430 }
1431
Alice Yanga990a712013-03-13 18:37:00 -07001432 private void initActionBar() {
1433 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001434 final ActionBar actionBar = getSupportActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001435 if (actionBar == null) {
1436 return;
1437 }
Alice Yanga990a712013-03-13 18:37:00 -07001438 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001439 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
Jin Caof0a0b4c2014-08-20 15:35:50 -07001440 actionBar.setTitle(R.string.compose_title);
Mindy Pereira326c6602012-01-04 15:32:42 -08001441 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001442 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001443 if (mComposeModeAdapter == null) {
Jin Caof7461632014-08-11 15:21:43 -07001444 mComposeModeAdapter = new ComposeModeAdapter(actionBar.getThemedContext());
Mindy Pereira326c6602012-01-04 15:32:42 -08001445 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001446 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1447 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001448 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001449 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001450 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001451 break;
1452 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001453 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001454 break;
1455 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001456 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001457 break;
1458 }
1459 }
Paul Westbrook4def3bf2014-07-01 00:38:17 -07001460 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
1461 ActionBar.DISPLAY_HOME_AS_UP);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001462 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001463 }
1464
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001465 private void initFromRefMessage(int action) {
1466 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001467
1468 // Check if To: address and email body needs to be prefilled based on extras.
1469 // This is used for reporting rendering feedback.
1470 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1471 Intent intent = getIntent();
1472 if (intent.getExtras() != null) {
1473 String toAddresses = intent.getStringExtra(EXTRA_TO);
1474 if (toAddresses != null) {
1475 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1476 }
1477 String body = intent.getStringExtra(EXTRA_BODY);
1478 if (body != null) {
1479 setBody(body, false /* withSignature */);
1480 }
1481 }
1482 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001483 }
1484
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001485 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001486 setSubject(mRefMessage, action);
1487 // Setup recipients
1488 if (action == FORWARD) {
1489 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001490 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001491 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001492 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001493 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1494 initAttachments(mRefMessage);
1495 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001496 }
1497
Andy Huang9ed742c2014-06-18 02:34:50 -07001498 protected HtmlTree.Converter<Spanned> getSpanConverter() {
1499 return new HtmlUtils.SpannedConverter();
1500 }
1501
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001502 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001503 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001504
1505 mDraft = message;
1506 mDraftId = message.id;
1507 mSubject.setText(message.subject);
1508 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001509 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001510 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001511 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1512 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001513 if (message.hasAttachments) {
1514 List<Attachment> attachments = message.getAttachments();
1515 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001516 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001517 }
1518 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07001519 int quotedTextIndex = message.appendRefMessageContent ? message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001520 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001521 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001522 if (!TextUtils.isEmpty(message.bodyHtml)) {
Jin Cao32f453b2014-07-22 14:21:15 -07001523 String body = message.bodyHtml;
Mindy Pereira002ff522012-05-30 10:31:26 -07001524 if (quotedTextIndex > -1) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07001525 // Find the offset in the html text of the actual quoted text and strip it out.
1526 // Note that the actual quotedTextOffset in the message has not changed as
1527 // this different offset is used only for display purposes. They point to different
1528 // parts of the original message. Please see the comments in QuoteTextView
1529 // to see the differences.
Mindy Pereira752222d2012-07-19 09:58:53 -07001530 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1531 if (quotedTextIndex > -1) {
Jin Cao32f453b2014-07-22 14:21:15 -07001532 body = message.bodyHtml.substring(0, quotedTextIndex);
Mindy Pereira752222d2012-07-19 09:58:53 -07001533 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1534 message.bodyHtml.length());
1535 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001536 }
Jin Cao32f453b2014-07-22 14:21:15 -07001537 new HtmlToSpannedTask().execute(body);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001538 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001539 final String body = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07001540 final CharSequence bodyText;
1541 if (TextUtils.isEmpty(body)) {
1542 bodyText = "";
1543 quotedText = null;
1544 } else {
1545 if (quotedTextIndex > body.length()) {
1546 // Sanity check to guarantee that we will not over index the String.
1547 // If this happens there is a bigger problem. This should never happen hence
1548 // the wtf logging.
1549 quotedTextIndex = -1;
1550 LogUtils.wtf(LOG_TAG, "quotedTextIndex (%d) > body.length() (%d)",
1551 quotedTextIndex, body.length());
1552 }
1553 bodyText = quotedTextIndex > -1 ? body.substring(0, quotedTextIndex) : body;
1554 if (quotedTextIndex > -1) {
1555 quotedText = body.substring(quotedTextIndex);
1556 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001557 }
1558 mBodyView.setText(bodyText);
1559 }
1560 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001561 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001562 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001563 }
1564
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001565 /**
1566 * Fill all the widgets with the content found in the Intent Extra, if any.
1567 * Also apply the same style to all widgets. Note: if initFromExtras is
1568 * called as a result of switching between reply, reply all, and forward per
1569 * the latest revision of Gmail, and the user has already made changes to
1570 * attachments on a previous incarnation of the message (as a reply, reply
1571 * all, or forward), the original attachments from the message will not be
1572 * re-instantiated. The user's changes will be respected. This follows the
1573 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001574 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001575 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001576 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001577 // If we were invoked with a SENDTO intent, the value
1578 // should take precedence
1579 final Uri dataUri = intent.getData();
1580 if (dataUri != null) {
1581 if (MAIL_TO.equals(dataUri.getScheme())) {
1582 initFromMailTo(dataUri.toString());
1583 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001584 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001585 String toText = dataUri.getSchemeSpecificPart();
1586 if (toText != null) {
1587 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001588 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001589 }
1590 }
1591 }
1592 }
1593
1594 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1595 if (extraStrings != null) {
1596 addToAddresses(Arrays.asList(extraStrings));
1597 }
1598 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1599 if (extraStrings != null) {
1600 addCcAddresses(Arrays.asList(extraStrings), null);
1601 }
1602 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1603 if (extraStrings != null) {
1604 addBccAddresses(Arrays.asList(extraStrings));
1605 }
1606
1607 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1608 if (extraString != null) {
1609 mSubject.setText(extraString);
1610 }
1611
1612 for (String extra : ALL_EXTRAS) {
1613 if (intent.hasExtra(extra)) {
1614 String value = intent.getStringExtra(extra);
1615 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001616 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001617 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001618 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001619 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001620 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001621 } else if (EXTRA_SUBJECT.equals(extra)) {
1622 mSubject.setText(value);
1623 } else if (EXTRA_BODY.equals(extra)) {
1624 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001625 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1626 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001627 }
1628 }
1629 }
1630
1631 Bundle extras = intent.getExtras();
1632 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001633 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
Jin Caoa8f34ff2014-07-24 14:43:57 -07001634 setBody((text != null) ? text : "", true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001635
1636 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001637 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001638
1639 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1640 if (mExtraValues != null) {
1641 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1642 initExtraValues(mExtraValues);
1643 return true;
1644 }
1645
1646 return false;
1647 }
1648
1649 protected void initExtraValues(ContentValues extraValues) {
1650 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001651 }
1652
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001653
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001654 @VisibleForTesting
1655 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001656 // TODO: handle the case where there are spaces in the display name as
1657 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1658 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001659 // Since URLDecode.decode changes + into ' ', and + is a valid
1660 // email character, we need to find/ replace these ourselves before
1661 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001662 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001663 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001664 } catch (IllegalArgumentException e) {
1665 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1666 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1667 } else {
1668 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1669 }
1670 return null;
1671 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001672 }
1673
1674 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001675 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1676 * changing '+' into ' '
1677 *
1678 * @param toReplace Input string
1679 * @return The string with all "+" characters replaced with "%2B"
1680 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001681 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001682 return toReplace.replace("+", "%2B");
1683 }
1684
1685 /**
Jin Caod67d7e32014-03-26 16:49:48 -07001686 * Replaces all occurrences of '%' with "%25", to prevent URLDecode.decode from
1687 * crashing on decoded '%' symbols
1688 *
1689 * @param toReplace Input string
1690 * @return The string with all "%" characters replaced with "%25"
1691 */
1692 private static String replacePercent(String toReplace) {
1693 return toReplace.replace("%", "%25");
1694 }
1695
1696 /**
1697 * Helper function to encapsulate encoding/decoding string from Uri.getQueryParameters
1698 * @param content Input string
1699 * @return The string that's properly escaped to be shown in mail subject/content
1700 */
1701 private static String decodeContentFromQueryParam(String content) {
1702 try {
1703 return URLDecoder.decode(replacePlus(replacePercent(content)), UTF8_ENCODING_NAME);
1704 } catch (UnsupportedEncodingException e) {
1705 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), content);
1706 return ""; // Default to empty string so setText/setBody has same behavior as before.
1707 }
1708 }
1709
1710 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001711 * Initialize the compose view from a String representing a mailTo uri.
1712 * @param mailToString The uri as a string.
1713 */
1714 public void initFromMailTo(String mailToString) {
1715 // We need to disguise this string as a URI in order to parse it
1716 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1717 Uri uri = Uri.parse("foo://" + mailToString);
1718 int index = mailToString.indexOf("?");
1719 int length = "mailto".length() + 1;
1720 String to;
1721 try {
1722 // Extract the recipient after mailto:
1723 if (index == -1) {
1724 to = decodeEmailInUri(mailToString.substring(length));
1725 } else {
1726 to = decodeEmailInUri(mailToString.substring(length, index));
1727 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001728 if (!TextUtils.isEmpty(to)) {
1729 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1730 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001731 } catch (UnsupportedEncodingException e) {
1732 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1733 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1734 } else {
1735 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1736 }
1737 }
1738
1739 List<String> cc = uri.getQueryParameters("cc");
1740 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1741
1742 List<String> otherTo = uri.getQueryParameters("to");
1743 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1744
1745 List<String> bcc = uri.getQueryParameters("bcc");
1746 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1747
Jin Caod67d7e32014-03-26 16:49:48 -07001748 // NOTE: Uri.getQueryParameters already decodes % encoded characters
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001749 List<String> subject = uri.getQueryParameters("subject");
1750 if (subject.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001751 mSubject.setText(decodeContentFromQueryParam(subject.get(0)));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001752 }
1753
1754 List<String> body = uri.getQueryParameters("body");
1755 if (body.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001756 setBody(decodeContentFromQueryParam(body.get(0)), true /* with signature */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001757 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001758 }
1759
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001760 @VisibleForTesting
1761 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001762 addAttachments(refMessage.getAttachments());
1763 }
1764
1765 public long addAttachments(List<Attachment> attachments) {
1766 long size = 0;
1767 AttachmentFailureException error = null;
1768 for (Attachment a : attachments) {
1769 try {
1770 size += mAttachmentsView.addAttachment(mAccount, a);
1771 } catch (AttachmentFailureException e) {
1772 error = e;
1773 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001774 }
Mark Wei434f2942012-08-24 11:54:02 -07001775 if (error != null) {
1776 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1777 if (attachments.size() > 1) {
1778 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1779 } else {
1780 showAttachmentTooBigToast(error.getErrorRes());
1781 }
1782 }
1783 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001784 }
1785
1786 /**
1787 * When an attachment is too large to be added to a message, show a toast.
1788 * This method also updates the position of the toast so that it is shown
1789 * clearly above they keyboard if it happens to be open.
1790 */
Mark Wei434f2942012-08-24 11:54:02 -07001791 private void showAttachmentTooBigToast(int errorRes) {
1792 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1793 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1794 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001795 }
1796
Mark Wei434f2942012-08-24 11:54:02 -07001797 private void showErrorToast(String message) {
1798 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1799 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001800 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1801 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1802 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001803 }
1804
Paul Westbrookf97588b2012-03-20 11:11:37 -07001805 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001806 Bundle extras = intent.getExtras();
1807 if (extras == null) {
1808 extras = Bundle.EMPTY;
1809 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001810 final String action = intent.getAction();
1811 if (!mAttachmentsChanged) {
1812 long totalSize = 0;
1813 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
Jin Caoadea2c82014-08-28 12:25:52 -07001814 final String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1815 final ArrayList<Uri> parsedUris = Lists.newArrayListWithCapacity(uris.length);
1816 for (String uri : uris) {
1817 parsedUris.add(Uri.parse(uri));
Paul Westbrookf97588b2012-03-20 11:11:37 -07001818 }
Jin Caoadea2c82014-08-28 12:25:52 -07001819 totalSize += handleAttachmentUrisFromIntent(parsedUris);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001820 }
mindyp9a9e8d62012-10-03 12:24:07 -07001821 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1822 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
Andy Huang91ede362014-01-21 19:16:00 -08001823 final ArrayList<Uri> uris = extras
mindyp9a9e8d62012-10-03 12:24:07 -07001824 .getParcelableArrayList(Intent.EXTRA_STREAM);
Jin Caoadea2c82014-08-28 12:25:52 -07001825 totalSize += handleAttachmentUrisFromIntent(uris);
mindyp9a9e8d62012-10-03 12:24:07 -07001826 } else {
Tony Mantler581edd42014-02-18 15:41:22 -08001827 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
Jin Caoadea2c82014-08-28 12:25:52 -07001828 final ArrayList<Uri> uris = Lists.newArrayList(uri);
1829 totalSize += handleAttachmentUrisFromIntent(uris);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001830 }
1831 }
1832
1833 if (totalSize > 0) {
1834 mAttachmentsChanged = true;
1835 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001836
1837 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1838 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001839 }
1840 }
1841 }
1842
Jin Caoadea2c82014-08-28 12:25:52 -07001843 /**
1844 * Helper function to handle a list of uris to attach.
1845 * @return the total size of all successfully attached files.
1846 */
1847 private long handleAttachmentUrisFromIntent(List<Uri> uris) {
1848 ArrayList<Attachment> attachments = Lists.newArrayList();
1849 for (Uri uri : uris) {
1850 try {
1851 if (uri != null) {
1852 if ("file".equals(uri.getScheme())) {
1853 final File f = new File(uri.getPath());
1854 // We should not be attaching any files from the data directory.
1855 if (f.getCanonicalPath().startsWith(DATA_DIRECTORY_ROOT)) {
1856 showErrorToast(getString(R.string.attachment_permission_denied));
1857 continue;
1858 }
1859 }
1860 if (!handleSpecialAttachmentUri(uri)) {
1861 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1862 attachments.add(a);
1863
1864 Analytics.getInstance().sendEvent("send_intent_attachment",
1865 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1866 }
1867 }
1868 } catch (AttachmentFailureException e) {
1869 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1870 showAttachmentTooBigToast(e.getErrorRes());
1871 } catch (IOException | SecurityException e) {
1872 showErrorToast(getString(R.string.attachment_permission_denied));
1873 }
1874 }
1875 return addAttachments(attachments);
1876 }
1877
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001878 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001879 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1880 mShowQuotedText = true;
1881 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001882
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001883 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1884 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001885 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1886 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001887 }
1888
1889 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001890 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001891 boolean ccVisible = mCcBccView.isCcVisible();
1892 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001893 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001894 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001895 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001896 } else {
Jin Cao9d358a12014-07-24 12:15:38 -07001897 mCcBccButton.setVisibility(View.GONE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001898 }
1899 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001900 }
1901
Mindy Pereira013194c2012-01-06 15:09:33 -08001902 /**
1903 * Add attachment and update the compose area appropriately.
Mindy Pereira013194c2012-01-06 15:09:33 -08001904 */
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001905 private void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07001906 if (data == null) {
1907 return;
1908 }
1909
1910 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1911 final ClipData clipData = data.getClipData();
1912 if (clipData != null) {
1913 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1914 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1915 }
1916 return;
1917 }
1918 }
1919
1920 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07001921 }
1922
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001923 private void addAttachmentAndUpdateView(Uri contentUri) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001924 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001925 return;
1926 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001927 try {
Andy Huang91ede362014-01-21 19:16:00 -08001928
1929 if (handleSpecialAttachmentUri(contentUri)) {
1930 return;
1931 }
1932
Andy Huang5c5fd572012-04-08 18:19:29 -07001933 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1934 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001935 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001936 showErrorToast(getResources().getString(
1937 e.getErrorRes(),
1938 AttachmentUtils.convertToHumanReadableSize(
1939 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001940 }
1941 }
1942
Andy Huang91ede362014-01-21 19:16:00 -08001943 /**
1944 * Allow subclasses to implement custom handling of attachments.
1945 *
1946 * @param contentUri a passed-in URI from a pick intent
1947 * @return true iff handled
1948 */
1949 protected boolean handleSpecialAttachmentUri(final Uri contentUri) {
1950 return false;
1951 }
1952
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001953 private void addAttachmentAndUpdateView(Attachment attachment) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001954 try {
Mark Wei434f2942012-08-24 11:54:02 -07001955 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001956 if (size > 0) {
1957 mAttachmentsChanged = true;
1958 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001959 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001960 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001961 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001962 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001963 }
1964 }
1965
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001966 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001967 // Don't populate the address if this is a forward.
1968 if (action == ComposeActivity.FORWARD) {
1969 return;
1970 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001971 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001972 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001973
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001974 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1975 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001976 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001977 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001978 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001979 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001980 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1981 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
Andy Huange2af8872014-01-16 12:36:27 -08001982 final String[] replyToAddresses = getReplyToAddresses(
1983 refMessage.getReplyToAddressesUnescaped(), fromAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001984
1985 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1986 // Cc list is the union of the To and Cc recipients of the original
1987 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001988 // already on the To list.
1989 if (action == ComposeActivity.REPLY) {
Tony Mantler24f116f2014-01-16 10:20:50 -08001990 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001991 addToAddresses(toAddresses);
1992 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001993 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08001994 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001995 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001996 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001997 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001998 addCcAddresses(ccAddresses, toAddresses);
1999 }
2000 }
2001
Andy Huange2af8872014-01-16 12:36:27 -08002002 // If there is no reply to address, the reply to address is the sender.
2003 private static String[] getReplyToAddresses(String[] replyTo, String from) {
2004 boolean hasReplyTo = false;
2005 for (final String replyToAddress : replyTo) {
2006 if (!TextUtils.isEmpty(replyToAddress)) {
2007 hasReplyTo = true;
2008 }
2009 }
2010 return hasReplyTo ? replyTo : new String[] {from};
2011 }
2012
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002013 private void addToAddresses(Collection<String> addresses) {
2014 addAddressesToList(addresses, mTo);
2015 }
2016
2017 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002018 addCcAddressesToList(tokenizeAddressList(addresses),
2019 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002020 }
2021
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07002022 private void addBccAddresses(Collection<String> addresses) {
2023 addAddressesToList(addresses, mBcc);
2024 }
2025
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002026 @VisibleForTesting
2027 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
2028 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
2029 String address;
2030
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002031 if (compareToList == null) {
Tony Mantler581edd42014-02-18 15:41:22 -08002032 for (final Rfc822Token[] tokens : addresses) {
2033 for (final Rfc822Token token : tokens) {
2034 address = token.toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002035 list.append(address + END_TOKEN);
2036 }
2037 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002038 } else {
2039 HashSet<String> compareTo = convertToHashSet(compareToList);
Tony Mantler581edd42014-02-18 15:41:22 -08002040 for (final Rfc822Token[] tokens : addresses) {
2041 for (final Rfc822Token token : tokens) {
2042 address = token.toString();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002043 // Check if this is a duplicate:
Tony Mantler581edd42014-02-18 15:41:22 -08002044 if (!compareTo.contains(token.getAddress())) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002045 // Get the address here
2046 list.append(address + END_TOKEN);
2047 }
2048 }
2049 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002050 }
2051 }
2052
Scott Kennedyff8553f2013-04-05 20:57:44 -07002053 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
2054 final HashSet<String> hash = new HashSet<String>();
2055 for (final Rfc822Token[] tokens : list) {
Tony Mantler581edd42014-02-18 15:41:22 -08002056 for (final Rfc822Token token : tokens) {
2057 hash.add(token.getAddress());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002058 }
2059 }
2060 return hash;
2061 }
2062
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002063 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
2064 @VisibleForTesting
2065 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
2066
2067 for (String address: addresses) {
2068 tokenized.add(Rfc822Tokenizer.tokenize(address));
2069 }
2070 return tokenized;
2071 }
2072
2073 @VisibleForTesting
2074 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
2075 for (String address : addresses) {
2076 addAddressToList(address, list);
2077 }
2078 }
2079
Scott Kennedyff8553f2013-04-05 20:57:44 -07002080 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002081 if (address == null || list == null)
2082 return;
2083
Scott Kennedyff8553f2013-04-05 20:57:44 -07002084 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002085
Tony Mantler581edd42014-02-18 15:41:22 -08002086 for (final Rfc822Token token : tokens) {
2087 list.append(token + END_TOKEN);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002088 }
2089 }
2090
2091 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07002092 protected Collection<String> initToRecipients(final String fullSenderAddress,
Tony Mantler24f116f2014-01-16 10:20:50 -08002093 final String[] replyToAddresses, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002094 // The To recipient is the reply-to address specified in the original
2095 // message, unless it is:
2096 // the current user OR a custom from of the current user, in which case
2097 // it's the To recipient list of the original message.
2098 // OR missing, in which case use the sender of the original message
2099 Set<String> toAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002100 for (final String replyToAddress : replyToAddresses) {
2101 if (!TextUtils.isEmpty(replyToAddress)
2102 && !recipientMatchesThisAccount(replyToAddress)) {
2103 toAddresses.add(replyToAddress);
2104 }
2105 }
2106 if (toAddresses.size() == 0) {
mindyp65b06f52012-11-21 10:35:08 -08002107 // In this case, the user is replying to a message in which their
Tony Mantler24f116f2014-01-16 10:20:50 -08002108 // current account or some of their custom from addresses are the only
2109 // recipients and they sent the original message.
mindyp65b06f52012-11-21 10:35:08 -08002110 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
2111 && recipientMatchesThisAccount(inToAddresses[0])) {
2112 toAddresses.add(inToAddresses[0]);
2113 return toAddresses;
2114 }
2115 // This happens if the user replies to a message they originally
2116 // wrote. In this case, "reply" really means "re-send," so we
2117 // target the original recipients. This works as expected even
2118 // if the user sent the original message to themselves.
2119 for (String address : inToAddresses) {
2120 if (!recipientMatchesThisAccount(address)) {
2121 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08002122 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07002123 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002124 }
2125 return toAddresses;
2126 }
2127
Scott Kennedyff8553f2013-04-05 20:57:44 -07002128 private void addRecipients(final Set<String> recipients, final String[] addresses) {
2129 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002130 // Do not add this account, or any of its custom from addresses, to
2131 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08002132 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08002133 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002134 recipients.add(email.replace("\"\"", ""));
2135 }
2136 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002137 }
2138
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002139 /**
2140 * A recipient matches this account if it has the same address as the
2141 * currently selected account OR one of the custom from addresses associated
2142 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002143 * @param recipientAddress address we are comparing with the currently selected account
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002144 */
mindyp5ee5d692012-11-19 16:02:16 -08002145 protected boolean recipientMatchesThisAccount(String recipientAddress) {
2146 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08002147 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002148 }
2149
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002150 /**
2151 * Returns a formatted subject string with the appropriate prefix for the action type.
2152 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
2153 */
Andrew Sapperstein7e04f142014-06-11 13:43:07 -07002154 public static String buildFormattedSubject(Resources res, String subject, int action) {
Tony Mantler41c3a252014-06-30 11:00:43 -07002155 final String prefix;
2156 final String correctedSubject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002157 if (action == ComposeActivity.COMPOSE) {
2158 prefix = "";
2159 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002160 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002161 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002162 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002163 }
2164
Tony Mantler41c3a252014-06-30 11:00:43 -07002165 if (TextUtils.isEmpty(subject)) {
2166 correctedSubject = prefix;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002167 } else {
Tony Mantler41c3a252014-06-30 11:00:43 -07002168 // Don't duplicate the prefix
2169 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
2170 correctedSubject = subject;
2171 } else {
2172 correctedSubject = String.format(
2173 res.getString(R.string.formatted_subject), prefix, subject);
2174 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002175 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002176
2177 return correctedSubject;
2178 }
2179
2180 private void setSubject(Message refMessage, int action) {
2181 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002182 }
2183
Mindy Pereira818143e2012-01-11 13:59:49 -08002184 private void initRecipients() {
2185 setupRecipients(mTo);
2186 setupRecipients(mCc);
2187 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002188 }
2189
Mindy Pereira818143e2012-01-11 13:59:49 -08002190 private void setupRecipients(RecipientEditTextView view) {
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002191 final DropdownChipLayouter layouter = getDropdownChipLayouter();
2192 if (layouter != null) {
2193 view.setDropdownChipLayouter(layouter);
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002194 }
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002195 view.setAdapter(getRecipientAdapter());
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002196 view.setRecipientEntryItemClickedListener(this);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002197 if (mValidator == null) {
Tony Mantler79b11562013-10-09 15:31:50 -07002198 final String accountName = mAccount.getEmailAddress();
Mindy Pereira33fe9082012-01-09 16:24:30 -08002199 int offset = accountName.indexOf("@") + 1;
2200 String account = accountName;
Tony Mantler79b11562013-10-09 15:31:50 -07002201 if (offset > 0) {
2202 account = account.substring(offset);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002203 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002204 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002205 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002206 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002207 }
2208
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002209 /**
2210 * Derived classes should override if they wish to provide their own autocomplete behavior.
2211 */
2212 public BaseRecipientAdapter getRecipientAdapter() {
2213 return new RecipientAdapter(this, mAccount);
2214 }
2215
2216 /**
2217 * Derived classes should override this to provide their own dropdown behavior.
2218 * If the result is null, the default {@link com.android.ex.chips.DropdownChipLayouter}
2219 * is used.
2220 */
2221 public DropdownChipLayouter getDropdownChipLayouter() {
2222 return null;
2223 }
2224
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002225 @Override
2226 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002227 final int id = v.getId();
2228 if (id == R.id.add_cc_bcc) {
2229 // Verify that cc/ bcc aren't showing.
2230 // Animate in cc/bcc.
2231 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002232 }
2233 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002234
2235 @Override
Jin Caoc5c550a2014-07-29 11:53:17 -07002236 public void onFocusChange (View v, boolean hasFocus) {
2237 final int id = v.getId();
2238 if (hasFocus && (id == R.id.subject || id == R.id.body)) {
2239 // Collapse cc/bcc iff both are empty
2240 final boolean showCcBccFields = !TextUtils.isEmpty(mCc.getText()) ||
2241 !TextUtils.isEmpty(mBcc.getText());
2242 mCcBccView.show(false /* animate */, showCcBccFields, showCcBccFields);
Jin Cao36e23872014-07-29 13:41:12 -07002243 mCcBccButton.setVisibility(showCcBccFields ? View.GONE : View.VISIBLE);
2244
2245 // On phones autoscroll down so that Cc aligns to the top if we are showing cc/bcc.
2246 if (getResources().getBoolean(R.bool.auto_scroll_cc) && showCcBccFields) {
2247 final int[] coords = new int[2];
2248 mCc.getLocationOnScreen(coords);
2249
2250 // Subtract status bar and action bar height from y-coord.
2251 final Rect rect = new Rect();
2252 getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07002253 final int deltaY = coords[1] - getSupportActionBar().getHeight() - rect.top;
Jin Cao36e23872014-07-29 13:41:12 -07002254
2255 // Only scroll down
2256 if (deltaY > 0) {
2257 mScrollView.smoothScrollBy(0, deltaY);
2258 }
2259 }
Jin Caoc5c550a2014-07-29 11:53:17 -07002260 }
2261 }
2262
2263 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002264 public boolean onCreateOptionsMenu(Menu menu) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002265 final boolean superCreated = super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07002266 // Don't render any menu items when there are no accounts.
2267 if (mAccounts == null || mAccounts.length == 0) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002268 return superCreated;
Mindy Pereirab199d172012-08-13 11:04:03 -07002269 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002270 MenuInflater inflater = getMenuInflater();
2271 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08002272
2273 /*
2274 * Start save in the correct enabled state.
2275 * 1) If a user launches compose from within gmail, save is disabled
2276 * until they add something, at which point, save is enabled, auto save
2277 * on exit; if the user empties everything, save is disabled, exiting does not
2278 * auto-save
2279 * 2) if a user replies/ reply all/ forwards from within gmail, save is
2280 * disabled until they change something, at which point, save is
2281 * enabled, auto save on exit; if the user empties everything, save is
2282 * disabled, exiting does not auto-save.
2283 * 3) If a user launches compose from another application and something
2284 * gets populated (attachments, recipients, body, subject, etc), save is
2285 * enabled, auto save on exit; if the user empties everything, save is
2286 * disabled, exiting does not auto-save
2287 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08002288 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08002289 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07002290 enableSave(mInnerSavedState != null ?
2291 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08002292 : (Intent.ACTION_SEND.equals(action)
2293 || Intent.ACTION_SEND_MULTIPLE.equals(action)
2294 || Intent.ACTION_SENDTO.equals(action)
2295 || shouldSave()));
2296
Greg Bullockd47a7042014-08-13 16:02:59 +02002297 final MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2298 final MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2299 final MenuItem attachFromServiceItem = menu.findItem(R.id.attach_from_service_stub1);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07002300 if (helpItem != null) {
2301 helpItem.setVisible(mAccount != null
2302 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2303 }
2304 if (sendFeedbackItem != null) {
2305 sendFeedbackItem.setVisible(mAccount != null
2306 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2307 }
Greg Bullockd47a7042014-08-13 16:02:59 +02002308 if (attachFromServiceItem != null) {
2309 attachFromServiceItem.setVisible(shouldEnableAttachFromServiceMenu(mAccount));
2310 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002311
Andrew Sapperstein8809f9f2013-10-11 16:13:35 -07002312 // Show attach picture on pre-K devices.
2313 menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002314
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002315 return true;
2316 }
2317
2318 @Override
2319 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002320 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002321
Andy Huangf8c59b02014-03-19 20:00:53 -07002322 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id,
2323 "compose", 0);
Andy Huangdc97bf42013-08-15 16:52:45 -07002324
Mindy Pereira75f66632012-01-11 11:42:02 -08002325 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002326 if (id == R.id.add_file_attachment) {
2327 doAttach(MIME_TYPE_ALL);
2328 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002329 doAttach(MIME_TYPE_PHOTO);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002330 } else if (id == R.id.save) {
2331 doSave(true);
2332 } else if (id == R.id.send) {
2333 doSend();
2334 } else if (id == R.id.discard) {
2335 doDiscard();
2336 } else if (id == R.id.settings) {
2337 Utils.showSettings(this, mAccount);
2338 } else if (id == android.R.id.home) {
2339 onAppUpPressed();
2340 } else if (id == R.id.help_info_menu_item) {
2341 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002342 } else {
2343 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002344 }
Tony Mantler581edd42014-02-18 15:41:22 -08002345 return handled || super.onOptionsItemSelected(item);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002346 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002347
Mindy Pereirab199d172012-08-13 11:04:03 -07002348 @Override
2349 public void onBackPressed() {
2350 // If we are showing the wait fragment, just exit.
2351 if (getWaitFragment() != null) {
2352 finish();
2353 } else {
2354 super.onBackPressed();
2355 }
2356 }
2357
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002358 /**
2359 * Carries out the "up" action in the action bar.
2360 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002361 private void onAppUpPressed() {
2362 if (mLaunchedFromEmail) {
2363 // If this was started from Gmail, simply treat app up as the system back button, so
2364 // that the last view is restored.
2365 onBackPressed();
2366 return;
2367 }
2368
2369 // Fire the main activity to ensure it launches the "top" screen of mail.
2370 // Since the main Activity is singleTask, it should revive that task if it was already
2371 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002372 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002373 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2374 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2375 startActivity(mailIntent);
2376 finish();
2377 }
2378
Mindy Pereira33fe9082012-01-09 16:24:30 -08002379 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002380 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002381 logSendOrSave(false /* save */);
2382 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002383 }
2384
Mindy Pereira48e31b02012-05-30 13:12:24 -07002385 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002386 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002387 }
2388
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002389 @Override
2390 public void onRecipientEntryItemClicked(int charactersTyped, int position) {
2391 // Send analytics of characters typed and position in dropdown selected.
2392 Analytics.getInstance().sendEvent(
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002393 "suggest_click", Integer.toString(charactersTyped), Integer.toString(position), 0);
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002394 }
2395
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002396 @VisibleForTesting
2397 public interface SendOrSaveCallback {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002398 void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
2399 void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2400 Message getMessage();
2401 void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2402 void incrementRecipientsTimesContacted(List<String> recipients);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002403 }
2404
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002405 @VisibleForTesting
2406 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002407 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002408 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002409 public final SendOrSaveCallback mSendOrSaveCallback;
2410 @VisibleForTesting
2411 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002412 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002413
2414 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002415 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002416 mContext = context;
2417 mSendOrSaveCallback = callback;
2418 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002419 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002420 }
2421
2422 @Override
2423 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002424 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002425
Mindy Pereira92551d02012-04-05 11:31:12 -07002426 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002427 Message message = mSendOrSaveCallback.getMessage();
2428 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002429 // If a previous draft has been saved, in an account that is different
2430 // than what the user wants to send from, remove the old draft, and treat this
2431 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002432 if (mExistingDraftAccount != null
2433 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002434 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2435 ContentResolver resolver = mContext.getContentResolver();
2436 ContentValues values = new ContentValues();
2437 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002438 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2439 new ContentProviderTask.UpdateTask()
2440 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2441 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002442 } else {
2443 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002444 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002445 // reset messageId to 0, so a new message will be created
2446 messageId = UIProvider.INVALID_MESSAGE_ID;
2447 }
2448 }
2449
2450 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002451 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002452
2453 if (!sendOrSaveMessage.mSave) {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002454 incrementRecipientsTimesContacted(
Andrew Sapperstein22a3a312014-06-24 18:35:49 -07002455 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO),
2456 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC),
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002457 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2458 }
2459 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2460 }
2461
Andrew Sapperstein22a3a312014-06-24 18:35:49 -07002462 private void incrementRecipientsTimesContacted(
2463 final String toAddresses, final String ccAddresses, final String bccAddresses) {
2464 final List<String> recipients = Lists.newArrayList();
2465 addAddressesToRecipientList(recipients, toAddresses);
2466 addAddressesToRecipientList(recipients, ccAddresses);
2467 addAddressesToRecipientList(recipients, bccAddresses);
2468 mSendOrSaveCallback.incrementRecipientsTimesContacted(recipients);
2469 }
2470
2471 private void addAddressesToRecipientList(
2472 final List<String> recipients, final String addressString) {
2473 if (recipients == null) {
2474 throw new IllegalArgumentException("recipientList cannot be null");
2475 }
Tony Mantler9f324232013-08-08 14:24:30 -07002476 if (TextUtils.isEmpty(addressString)) {
2477 return;
2478 }
2479 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
Tony Mantler581edd42014-02-18 15:41:22 -08002480 for (final Rfc822Token token : tokens) {
2481 recipients.add(token.getAddress());
Tony Mantler9f324232013-08-08 14:24:30 -07002482 }
Tony Mantler9f324232013-08-08 14:24:30 -07002483 }
2484
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002485 /**
2486 * Send or Save a message.
2487 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002488 private void sendOrSaveMessage(final long messageIdToSave,
2489 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002490 final ContentResolver resolver = mContext.getContentResolver();
2491 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2492
2493 final String accountMethod = sendOrSaveMessage.mSave ?
2494 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2495 UIProvider.AccountCallMethods.SEND_MESSAGE;
2496
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002497 try {
2498 if (updateExistingMessage) {
2499 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002500
Paul Westbrook013a23c2013-02-22 10:37:41 -08002501 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002502 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002503 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002504 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002505 final Bundle result = callAccountSendSaveMethod(resolver,
2506 selectedAccount.account, accountMethod, sendOrSaveMessage);
2507 if (result != null) {
2508 // If a non-null value was returned, then the provider handled the call
2509 // method
2510 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002511 }
2512 if (sendOrSaveMessage.mSave && messageUri != null) {
2513 final Cursor messageCursor = resolver.query(messageUri,
2514 UIProvider.MESSAGE_PROJECTION, null, null, null);
2515 if (messageCursor != null) {
2516 try {
2517 if (messageCursor.moveToFirst()) {
2518 // Broadcast notification that a new message has
2519 // been allocated
2520 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2521 new Message(messageCursor));
2522 }
2523 } finally {
2524 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002525 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002526 }
2527 }
2528 }
2529 } finally {
2530 // Close any opened file descriptors
2531 closeOpenedAttachmentFds(sendOrSaveMessage);
2532 }
2533 }
2534
Scott Kennedyff8553f2013-04-05 20:57:44 -07002535 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002536 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2537 if (openedFds != null) {
2538 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002539 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002540 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2541 if (fd != null) {
2542 try {
2543 fd.close();
2544 } catch (IOException e) {
2545 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002546 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002547 }
2548 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002549 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002550 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002551
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002552 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002553 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002554 *
2555 * If this was successful, this method will return an non-null Bundle instance
2556 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002557 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2558 final Account account, final String method,
2559 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002560 // Copy all of the values from the content values to the bundle
2561 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2562 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2563
2564 for (Entry<String, Object> entry : valueSet) {
2565 final Object entryValue = entry.getValue();
2566 final String key = entry.getKey();
2567 if (entryValue instanceof String) {
2568 methodExtras.putString(key, (String)entryValue);
2569 } else if (entryValue instanceof Boolean) {
2570 methodExtras.putBoolean(key, (Boolean)entryValue);
2571 } else if (entryValue instanceof Integer) {
2572 methodExtras.putInt(key, (Integer)entryValue);
2573 } else if (entryValue instanceof Long) {
2574 methodExtras.putLong(key, (Long)entryValue);
2575 } else {
2576 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2577 entryValue.getClass().getName());
2578 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002579 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002580
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002581 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2582 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2583 if (fdMap != null) {
2584 methodExtras.putParcelable(
2585 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2586 }
2587
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002588 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002589 }
2590 }
2591
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002592 /**
2593 * Reports recipients that have been contacted in order to improve auto-complete
2594 * suggestions. Default behavior updates usage statistics in ContactsProvider.
2595 * @param recipients addresses
2596 */
2597 protected void incrementRecipientsTimesContacted(List<String> recipients) {
2598 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(this);
2599 statsUpdater.updateWithAddress(recipients);
2600 }
2601
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002602 @VisibleForTesting
2603 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002604 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002605 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002606 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002607 @VisibleForTesting
2608 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002609 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002610 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002611
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002612 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2613 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002614 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002615 mValues = values;
2616 mRefMessageId = refMessageId;
2617 mSave = save;
2618 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002619
2620 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002621 }
2622
2623 int requestId() {
2624 return mRequestId;
2625 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002626
2627 Bundle attachmentFds() {
2628 return mAttachmentFds;
2629 }
2630
2631 /**
2632 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2633 * called before the ComposeActivity finishes.
2634 * Note: The caller is responsible for closing these file descriptors.
2635 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002636 private static Bundle initializeAttachmentFds(final Context context,
2637 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002638 if (attachments == null || attachments.size() == 0) {
2639 return null;
2640 }
2641
2642 final Bundle result = new Bundle(attachments.size());
2643 final ContentResolver resolver = context.getContentResolver();
2644
2645 for (Attachment attachment : attachments) {
2646 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2647 continue;
2648 }
2649
2650 ParcelFileDescriptor fileDescriptor;
2651 try {
2652 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2653 } catch (FileNotFoundException e) {
2654 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2655 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002656 } catch (SecurityException e) {
2657 // We have encountered a security exception when attempting to open the file
2658 // specified by the content uri. If the attachment has been cached, this
2659 // isn't a problem, as even through the original permission may have been
2660 // revoked, we have cached the file. This will happen when saving/sending
2661 // a previously saved draft.
2662 // TODO(markwei): Expose whether the attachment has been cached through the
2663 // attachment object. This would allow us to limit when the log is made, as
2664 // if the attachment has been cached, this really isn't an error
2665 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2666 // Just set the file descriptor to null, as the underlying provider needs
2667 // to handle the file descriptor not being set.
2668 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002669 }
2670
2671 if (fileDescriptor != null) {
2672 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2673 }
2674 }
2675
2676 return result;
2677 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002678 }
2679
2680 /**
2681 * Get the to recipients.
2682 */
2683 public String[] getToAddresses() {
2684 return getAddressesFromList(mTo);
2685 }
2686
2687 /**
2688 * Get the cc recipients.
2689 */
2690 public String[] getCcAddresses() {
2691 return getAddressesFromList(mCc);
2692 }
2693
2694 /**
2695 * Get the bcc recipients.
2696 */
2697 public String[] getBccAddresses() {
2698 return getAddressesFromList(mBcc);
2699 }
2700
2701 public String[] getAddressesFromList(RecipientEditTextView list) {
2702 if (list == null) {
2703 return new String[0];
2704 }
2705 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2706 int count = tokens.length;
2707 String[] result = new String[count];
2708 for (int i = 0; i < count; i++) {
2709 result[i] = tokens[i].toString();
2710 }
2711 return result;
2712 }
2713
2714 /**
2715 * Check for invalid email addresses.
2716 * @param to String array of email addresses to check.
2717 * @param wrongEmailsOut Emails addresses that were invalid.
2718 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002719 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002720 if (mValidator == null) {
2721 return;
2722 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002723 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002724 if (!mValidator.isValid(email)) {
2725 wrongEmailsOut.add(email);
2726 }
2727 }
2728 }
2729
Tony Mantler2558b502013-07-09 10:53:34 -07002730 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002731 // Public no-args constructor needed for fragment re-instantiation
2732 public RecipientErrorDialogFragment() {}
2733
Tony Mantler2558b502013-07-09 10:53:34 -07002734 public static RecipientErrorDialogFragment newInstance(final String message) {
2735 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2736 final Bundle args = new Bundle(1);
2737 args.putString("message", message);
2738 frag.setArguments(args);
2739 return frag;
2740 }
2741
2742 @Override
2743 public Dialog onCreateDialog(Bundle savedInstanceState) {
2744 final String message = getArguments().getString("message");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002745 return new AlertDialog.Builder(getActivity())
2746 .setMessage(message)
Tony Mantler2558b502013-07-09 10:53:34 -07002747 .setPositiveButton(
2748 R.string.ok, new Dialog.OnClickListener() {
2749 @Override
2750 public void onClick(DialogInterface dialog, int which) {
2751 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2752 }
2753 }).create();
2754 }
2755 }
2756
2757 private void finishRecipientErrorDialog() {
2758 // after the user dismisses the recipient error
2759 // dialog we want to make sure to refocus the
2760 // recipient to field so they can fix the issue
2761 // easily
2762 if (mTo != null) {
2763 mTo.requestFocus();
2764 }
2765 }
2766
Mindy Pereira82cc5662012-01-09 17:29:30 -08002767 /**
2768 * Show an error because the user has entered an invalid recipient.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002769 */
Tony Mantler2558b502013-07-09 10:53:34 -07002770 private void showRecipientErrorDialog(final String message) {
2771 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2772 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002773 }
2774
2775 /**
2776 * Update the state of the UI based on whether or not the current draft
2777 * needs to be saved and the message is not empty.
2778 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002779 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002780 if (mSave != null) {
2781 mSave.setEnabled((shouldSave() && !isBlank()));
2782 }
2783 }
2784
2785 /**
2786 * Returns true if we need to save the current draft.
2787 */
2788 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002789 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002790 // The message should only be saved if:
2791 // It hasn't been sent AND
2792 // Some text has been added to the message OR
2793 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002794 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002795 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002796 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002797 }
2798 }
2799
2800 /**
Greg Bullockd47a7042014-08-13 16:02:59 +02002801 * Returns whether the "Attach from Drive" menu item should be visible.
2802 */
2803 protected boolean shouldEnableAttachFromServiceMenu(Account mAccount) {
2804 return false;
2805 }
2806
2807 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002808 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002809 * @return boolean
2810 */
2811 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002812 // Need to check for null since isBlank() can be called from onPause()
2813 // before findViews() is called
2814 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2815 mAttachmentsView == null) {
2816 LogUtils.w(LOG_TAG, "null views in isBlank check");
2817 return true;
2818 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002819 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002820 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2821 mBodyView.getText().toString()) == 0)
2822 && mTo.length() == 0
2823 && mCc.length() == 0 && mBcc.length() == 0
2824 && mAttachmentsView.getAttachments().size() == 0;
2825 }
2826
2827 @VisibleForTesting
2828 protected int getSignatureStartPosition(String signature, String bodyText) {
2829 int startPos = -1;
2830
2831 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2832 return startPos;
2833 }
2834
2835 int bodyLength = bodyText.length();
2836 int signatureLength = signature.length();
2837 String printableVersion = convertToPrintableSignature(signature);
2838 int printableLength = printableVersion.length();
2839
2840 if (bodyLength >= printableLength
2841 && bodyText.substring(bodyLength - printableLength)
2842 .equals(printableVersion)) {
2843 startPos = bodyLength - printableLength;
2844 } else if (bodyLength >= signatureLength
2845 && bodyText.substring(bodyLength - signatureLength)
2846 .equals(signature)) {
2847 startPos = bodyLength - signatureLength;
2848 }
2849 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002850 }
2851
2852 /**
2853 * Allows any changes made by the user to be ignored. Called when the user
2854 * decides to discard a draft.
2855 */
2856 private void discardChanges() {
2857 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002858 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002859 mReplyFromChanged = false;
2860 }
2861
2862 /**
Tony Mantler581edd42014-02-18 15:41:22 -08002863 * @param save True to save, false to send
2864 * @param showToast True to show a toast once the message is sent/saved
Mindy Pereira181df782012-03-01 13:32:44 -08002865 */
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002866 protected void sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002867 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002868 if (mAccounts == null || mAccount == null) {
2869 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002870 if (autoSend) {
2871 finish();
2872 }
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002873 return;
Mark Wei009b3712012-10-18 18:07:50 -07002874 }
2875
Scott Kennedyff8553f2013-04-05 20:57:44 -07002876 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002877 if (orientationChanged) {
2878 to = cc = bcc = new String[0];
2879 } else {
2880 to = getToAddresses();
2881 cc = getCcAddresses();
2882 bcc = getBccAddresses();
2883 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002884
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002885 final ArrayList<String> recipients = buildEmailAddressList(to);
2886 recipients.addAll(buildEmailAddressList(cc));
2887 recipients.addAll(buildEmailAddressList(bcc));
2888
Mindy Pereira181df782012-03-01 13:32:44 -08002889 // Don't let the user send to nobody (but it's okay to save a message
2890 // with no recipients)
2891 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2892 showRecipientErrorDialog(getString(R.string.recipient_needed));
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002893 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002894 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002895
Mindy Pereira181df782012-03-01 13:32:44 -08002896 List<String> wrongEmails = new ArrayList<String>();
2897 if (!save) {
2898 checkInvalidEmails(to, wrongEmails);
2899 checkInvalidEmails(cc, wrongEmails);
2900 checkInvalidEmails(bcc, wrongEmails);
2901 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002902
Mindy Pereira181df782012-03-01 13:32:44 -08002903 // Don't let the user send an email with invalid recipients
2904 if (wrongEmails.size() > 0) {
2905 String errorText = String.format(getString(R.string.invalid_recipient),
2906 wrongEmails.get(0));
2907 showRecipientErrorDialog(errorText);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002908 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002909 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002910
Mindy Pereira181df782012-03-01 13:32:44 -08002911 if (!save) {
Alan Lau3d519042014-06-05 11:13:06 -07002912 if (autoSend) {
2913 // Skip all further checks during autosend. This flow is used by Android Wear
2914 // and Google Now.
2915 sendOrSave(save, showToast);
2916 return;
2917 }
2918
2919 // Show a warning before sending only if there are no attachments, body, or subject.
Mindy Pereira181df782012-03-01 13:32:44 -08002920 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2921 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002922 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002923
Mindy Pereira181df782012-03-01 13:32:44 -08002924 // A warning about an empty body may not be warranted when
2925 // forwarding mails, since a common use case is to forward
2926 // quoted text and not append any more text.
2927 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002928
Mindy Pereira181df782012-03-01 13:32:44 -08002929 // When we bring up a dialog warning the user about a send,
2930 // assume that they accept sending the message. If they do not,
2931 // the dialog listener is required to enable sending again.
2932 if (warnAboutEmptySubject) {
Tony Mantler581edd42014-02-18 15:41:22 -08002933 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002934 showToast, recipients);
2935 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002936 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002937
Mindy Pereira181df782012-03-01 13:32:44 -08002938 if (warnAboutEmptyBody) {
Tony Mantler581edd42014-02-18 15:41:22 -08002939 showSendConfirmDialog(R.string.confirm_send_message_with_no_body,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002940 showToast, recipients);
2941 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002942 }
2943 }
Alan Lau3d519042014-06-05 11:13:06 -07002944 // Ask for confirmation to send.
2945 if (showSendConfirmation()) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002946 showSendConfirmDialog(R.string.confirm_send_message, showToast, recipients);
2947 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002948 }
2949 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002950
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002951 performAdditionalSendOrSaveSanityChecks(save, showToast, recipients);
Mindy Pereira181df782012-03-01 13:32:44 -08002952 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002953
Mindy Pereira181df782012-03-01 13:32:44 -08002954 /**
2955 * Returns a boolean indicating whether warnings should be shown for empty
2956 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002957 *
Mindy Pereira181df782012-03-01 13:32:44 -08002958 * @return True if a warning should be shown for empty text fields
2959 */
2960 protected boolean showEmptyTextWarnings() {
2961 return mAttachmentsView.getAttachments().size() == 0;
2962 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002963
Mindy Pereira181df782012-03-01 13:32:44 -08002964 /**
2965 * Returns a boolean indicating whether the user should confirm each send
2966 *
2967 * @return True if a warning should be on each send
2968 */
2969 protected boolean showSendConfirmation() {
Tony Mantler581edd42014-02-18 15:41:22 -08002970 return mCachedSettings != null && mCachedSettings.confirmSend;
Mindy Pereira181df782012-03-01 13:32:44 -08002971 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002972
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002973 public static class SendConfirmDialogFragment extends DialogFragment
2974 implements DialogInterface.OnClickListener {
2975
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002976 private static final String MESSAGE_ID = "messageId";
2977 private static final String SHOW_TOAST = "showToast";
2978 private static final String RECIPIENTS = "recipients";
2979
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002980 private boolean mShowToast;
2981
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002982 private ArrayList<String> mRecipients;
2983
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002984 // Public no-args constructor needed for fragment re-instantiation
2985 public SendConfirmDialogFragment() {}
2986
Tony Mantler2558b502013-07-09 10:53:34 -07002987 public static SendConfirmDialogFragment newInstance(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002988 final boolean showToast, final ArrayList<String> recipients) {
Tony Mantler2558b502013-07-09 10:53:34 -07002989 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2990 final Bundle args = new Bundle(3);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002991 args.putInt(MESSAGE_ID, messageId);
2992 args.putBoolean(SHOW_TOAST, showToast);
2993 args.putStringArrayList(RECIPIENTS, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07002994 frag.setArguments(args);
2995 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002996 }
Tony Mantler2558b502013-07-09 10:53:34 -07002997
2998 @Override
2999 public Dialog onCreateDialog(Bundle savedInstanceState) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003000 final int messageId = getArguments().getInt(MESSAGE_ID);
3001 mShowToast = getArguments().getBoolean(SHOW_TOAST);
3002 mRecipients = getArguments().getStringArrayList(RECIPIENTS);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003003
3004 final int confirmTextId = (messageId == R.string.confirm_send_message) ?
3005 R.string.ok : R.string.send;
Tony Mantler2558b502013-07-09 10:53:34 -07003006
3007 return new AlertDialog.Builder(getActivity())
3008 .setMessage(messageId)
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003009 .setPositiveButton(confirmTextId, this)
Paul Westbrook7d1c5c42013-10-01 23:40:04 -07003010 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003011 .create();
3012 }
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003013
3014 @Override
3015 public void onClick(DialogInterface dialog, int which) {
3016 if (which == DialogInterface.BUTTON_POSITIVE) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003017 ((ComposeActivity) getActivity()).finishSendConfirmDialog(mShowToast, mRecipients);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003018 }
3019 }
Tony Mantler2558b502013-07-09 10:53:34 -07003020 }
3021
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003022 private void finishSendConfirmDialog(
3023 final boolean showToast, final ArrayList<String> recipients) {
3024 performAdditionalSendOrSaveSanityChecks(false /* save */, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003025 }
3026
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003027 // The list of recipients are used by the additional sendOrSave checks.
3028 // However, the send confirm dialog may be shown before performing
3029 // the additional checks. As a result, we need to plumb the recipient
3030 // list through the send confirm dialog so that
3031 // performAdditionalSendOrSaveChecks can be performed properly.
Tony Mantler581edd42014-02-18 15:41:22 -08003032 private void showSendConfirmDialog(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003033 final boolean showToast, final ArrayList<String> recipients) {
3034 final DialogFragment frag = SendConfirmDialogFragment.newInstance(
3035 messageId, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003036 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08003037 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003038
Mindy Pereira181df782012-03-01 13:32:44 -08003039 /**
3040 * Returns whether the ComposeArea believes there is any text in the body of
3041 * the composition. TODO: When ComposeArea controls the Body as well, add
3042 * that here.
3043 */
3044 public boolean isBodyEmpty() {
3045 return !mQuotedTextView.isTextIncluded();
3046 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003047
Mindy Pereira181df782012-03-01 13:32:44 -08003048 /**
3049 * Test to see if the subject is empty.
3050 *
3051 * @return boolean.
3052 */
3053 // TODO: this will likely go away when composeArea.focus() is implemented
3054 // after all the widget control is moved over.
3055 public boolean isSubjectEmpty() {
3056 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
3057 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003058
Andy Huang0a2a3462013-12-20 15:56:13 -08003059 @VisibleForTesting
3060 public String getSubject() {
3061 return mSubject.getText().toString();
3062 }
3063
Andy Huang91ede362014-01-21 19:16:00 -08003064 private int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Jin Cao77b4c2c2014-05-20 13:55:53 -07003065 Message message, final Message refMessage, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08003066 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07003067 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08003068 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003069
Paul Westbrookb4931c62013-01-14 17:51:18 -08003070 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07003071
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003072 MessageModification.putToAddresses(values, message.getToAddresses());
3073 MessageModification.putCcAddresses(values, message.getCcAddresses());
3074 MessageModification.putBccAddresses(values, message.getBccAddresses());
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003075 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07003076
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003077 MessageModification.putSubject(values, message.subject);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003078
Jin Cao77b4c2c2014-05-20 13:55:53 -07003079 // bodyHtml already have the composing spans removed.
3080 final String htmlBody = message.bodyHtml;
Jin Caoa9f5a8e2014-07-22 13:48:45 -07003081 final String textBody = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07003082 // fullbody will contain the actual body plus the quoted text.
3083 final String fullBody;
3084 final String quotedString;
3085 final boolean hasQuotedText = !TextUtils.isEmpty(quotedText);
3086 if (hasQuotedText) {
3087 // The quoted text is HTML at this point.
3088 quotedString = quotedText.toString();
3089 fullBody = htmlBody + quotedString;
3090 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
3091 MessageModification.putAppendRefMessageContent(values, true /* include quoted */);
3092 } else {
3093 fullBody = htmlBody;
3094 quotedString = null;
Mindy Pereira29ef1b82012-01-13 11:26:21 -08003095 }
Jin Caoa9f5a8e2014-07-22 13:48:45 -07003096 // Only take refMessage into account if either one of its html/text is not empty.
3097 if (refMessage != null && !(TextUtils.isEmpty(refMessage.bodyHtml) &&
3098 TextUtils.isEmpty(refMessage.bodyText))) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07003099 // The code below might need to be revisited. The quoted text position is different
3100 // between text/html and text/plain parts and they should be stored seperately and
3101 // the right version should be used in the UI. text/html should have preference
3102 // if both exist. Issues like this made me file b/14256940 to make sure that we
3103 // properly handle the existing of both text/html and text/plain parts and to verify
3104 // that we are not making some assumptions that break if there is no text/html part.
3105 int quotedTextPos = -1;
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003106 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
Jin Cao7800d292014-08-24 11:31:15 -07003107 MessageModification.putBodyHtml(values, fullBody);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003108 if (hasQuotedText) {
3109 quotedTextPos = htmlBody.length() +
3110 QuotedTextView.getQuotedTextOffset(quotedString);
3111 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003112 }
3113 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08003114 MessageModification.putBody(values,
Jin Cao7800d292014-08-24 11:31:15 -07003115 Utils.convertHtmlToPlainText(fullBody));
Anthony Lee2a3cc132014-04-22 14:15:25 -07003116 if (hasQuotedText && (quotedTextPos == -1)) {
3117 quotedTextPos = textBody.length();
3118 }
3119 }
3120 if (quotedTextPos != -1) {
3121 // The quoted text pos is the text/html version first and the text/plan version
3122 // if there is no text/html part. The reason for this is because preference
3123 // is given to text/html in the compose window if it exists. In the future, we
3124 // should calculate the index for both since the user could choose to compose
3125 // explicitly in text/plain.
3126 MessageModification.putQuoteStartPos(values, quotedTextPos);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003127 }
3128 } else {
Jin Cao7800d292014-08-24 11:31:15 -07003129 MessageModification.putBodyHtml(values, fullBody);
3130 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody));
Mindy Pereirac2031972012-04-03 09:38:35 -07003131 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07003132 int draftType = getDraftType(composeMode);
3133 MessageModification.putDraftType(values, draftType);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003134 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07003135 if (!TextUtils.isEmpty(refMessageId)) {
3136 MessageModification.putRefMessageId(values, refMessageId);
3137 }
Scott Kennedy60847252013-08-15 15:55:42 -07003138 if (extraValues != null) {
3139 values.putAll(extraValues);
3140 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07003141 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
3142 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08003143 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
3144 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003145
Mindy Pereira181df782012-03-01 13:32:44 -08003146 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08003147 // Do the send/save action on the specified handler to avoid possible
3148 // ANRs
3149 handler.post(sendOrSaveTask);
Jin Cao7800d292014-08-24 11:31:15 -07003150 LogUtils.i(LOG_TAG, "[compose] SendOrSaveMessage [%s] posted (isSave: %s) - " +
3151 "body length: %d, attachment count: %d",
3152 sendOrSaveMessage.requestId(), save, message.bodyText.length(),
3153 message.getAttachmentCount(true));
Mindy Pereira82cc5662012-01-09 17:29:30 -08003154
Mindy Pereira181df782012-03-01 13:32:44 -08003155 return sendOrSaveMessage.requestId();
3156 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003157
Paul Westbrookb4931c62013-01-14 17:51:18 -08003158 /**
3159 * Removes any composing spans from the specified string. This will create a new
3160 * SpannableString instance, as to not modify the behavior of the EditText view.
3161 */
3162 private static SpannableString removeComposingSpans(Spanned body) {
3163 final SpannableString messageBody = new SpannableString(body);
3164 BaseInputConnection.removeComposingSpans(messageBody);
Andy Huangff017272014-06-18 00:27:35 -07003165
3166 // Remove watcher spans while we're at it, so any off-UI thread manipulation of these
3167 // spans doesn't trigger unexpected side-effects. This copy is essentially 100% detached
3168 // from the EditText.
3169 //
3170 // (must remove SpanWatchers first to avoid triggering them as we remove other spans)
3171 removeSpansOfType(messageBody, SpanWatcher.class);
3172 removeSpansOfType(messageBody, TextWatcher.class);
3173
Paul Westbrookb4931c62013-01-14 17:51:18 -08003174 return messageBody;
3175 }
3176
Andy Huangff017272014-06-18 00:27:35 -07003177 private static void removeSpansOfType(SpannableString str, Class<?> cls) {
3178 for (Object span : str.getSpans(0, str.length(), cls)) {
3179 str.removeSpan(span);
3180 }
3181 }
3182
Mindy Pereira002ff522012-05-30 10:31:26 -07003183 private static int getDraftType(int mode) {
3184 int draftType = -1;
3185 switch (mode) {
3186 case ComposeActivity.COMPOSE:
3187 draftType = DraftType.COMPOSE;
3188 break;
3189 case ComposeActivity.REPLY:
3190 draftType = DraftType.REPLY;
3191 break;
3192 case ComposeActivity.REPLY_ALL:
3193 draftType = DraftType.REPLY_ALL;
3194 break;
3195 case ComposeActivity.FORWARD:
3196 draftType = DraftType.FORWARD;
3197 break;
3198 }
3199 return draftType;
3200 }
3201
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003202 /**
3203 * Derived classes should override this step to perform additional checks before
3204 * send or save. The default implementation simply calls {@link #sendOrSave(boolean, boolean)}.
3205 */
3206 protected void performAdditionalSendOrSaveSanityChecks(
3207 final boolean save, final boolean showToast, ArrayList<String> recipients) {
3208 sendOrSave(save, showToast);
3209 }
3210
3211 protected void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08003212 // Check if user is a monkey. Monkeys can compose and hit send
3213 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07003214 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08003215 return;
3216 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003217
Jin Cao77b4c2c2014-05-20 13:55:53 -07003218 final SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003219 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08003220 private int mRestoredRequestId;
3221
Marc Blank0bbc8582012-04-23 15:07:57 -07003222 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08003223 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08003224 synchronized (mActiveTasks) {
3225 int numTasks = mActiveTasks.size();
3226 if (numTasks == 0) {
3227 // Start service so we won't be killed if this app is
3228 // put in the background.
3229 startService(new Intent(ComposeActivity.this, EmptyService.class));
3230 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003231
Mindy Pereira181df782012-03-01 13:32:44 -08003232 mActiveTasks.add(sendOrSaveTask);
3233 }
3234 if (sTestSendOrSaveCallback != null) {
3235 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
3236 }
3237 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003238
Marc Blank0bbc8582012-04-23 15:07:57 -07003239 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003240 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
3241 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08003242 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08003243 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08003244 mDraftId = message.id;
3245 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003246 if (sRequestMessageIdMap != null) {
3247 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
3248 }
Mindy Pereira181df782012-03-01 13:32:44 -08003249 // Cache request message map, in case the process is killed
3250 saveRequestMap();
3251 }
3252 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003253 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08003254 }
3255 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003256
Marc Blank0bbc8582012-04-23 15:07:57 -07003257 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003258 public Message getMessage() {
3259 synchronized (mDraftLock) {
3260 return mDraft;
3261 }
3262 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003263
Marc Blank0bbc8582012-04-23 15:07:57 -07003264 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003265 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07003266 // Update the last sent from account.
3267 if (mAccount != null) {
3268 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
3269 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003270 if (success) {
3271 // Successfully sent or saved so reset change markers
3272 discardChanges();
3273 } else {
3274 // A failure happened with saving/sending the draft
3275 // TODO(pwestbro): add a better string that should be used
3276 // when failing to send or save
3277 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
3278 .show();
3279 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003280
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003281 int numTasks;
3282 synchronized (mActiveTasks) {
3283 // Remove the task from the list of active tasks
3284 mActiveTasks.remove(task);
3285 numTasks = mActiveTasks.size();
3286 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003287
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003288 if (numTasks == 0) {
3289 // Stop service so we can be killed.
3290 stopService(new Intent(ComposeActivity.this, EmptyService.class));
3291 }
3292 if (sTestSendOrSaveCallback != null) {
3293 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
3294 }
3295 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07003296
3297 @Override
3298 public void incrementRecipientsTimesContacted(final List<String> recipients) {
3299 ComposeActivity.this.incrementRecipientsTimesContacted(recipients);
3300 }
Mindy Pereira181df782012-03-01 13:32:44 -08003301 };
Tony Mantler1e05a1e2013-08-12 16:44:26 -07003302 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003303
Jin Cao77b4c2c2014-05-20 13:55:53 -07003304 final Spanned body = removeComposingSpans(mBodyView.getText());
3305 SEND_SAVE_TASK_HANDLER.post(new Runnable() {
3306 @Override
3307 public void run() {
3308 final Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode(), body);
3309 mRequestId = sendOrSaveInternal(ComposeActivity.this, mReplyFromAccount, msg,
3310 mRefMessage, mQuotedTextView.getQuotedTextIfIncluded(), callback,
3311 SEND_SAVE_TASK_HANDLER, save, mComposeMode, mDraftAccount, mExtraValues);
3312 }
3313 });
Mindy Pereira82cc5662012-01-09 17:29:30 -08003314
Mindy Pereira181df782012-03-01 13:32:44 -08003315 // Don't display the toast if the user is just changing the orientation,
3316 // but we still need to save the draft to the cursor because this is how we restore
3317 // the attachments when the configuration change completes.
3318 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
3319 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
3320 Toast.LENGTH_LONG).show();
3321 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003322
Mindy Pereira181df782012-03-01 13:32:44 -08003323 // Need to update variables here because the send or save completes
3324 // asynchronously even though the toast shows right away.
3325 discardChanges();
3326 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003327
Mindy Pereira181df782012-03-01 13:32:44 -08003328 // If we are sending, finish the activity
3329 if (!save) {
3330 finish();
3331 }
3332 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003333
Mindy Pereira181df782012-03-01 13:32:44 -08003334 /**
3335 * Save the state of the request messageid map. This allows for the Gmail
3336 * process to be killed, but and still allow for ComposeActivity instances
3337 * to be recreated correctly.
3338 */
3339 private void saveRequestMap() {
3340 // TODO: store the request map in user preferences.
3341 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003342
Tony Mantler581edd42014-02-18 15:41:22 -08003343 @SuppressLint("NewApi")
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003344 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08003345 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
3346 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07003347 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07003348 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003349 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08003350 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08003351 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
3352 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08003353 }
3354
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003355 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003356 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003357 if (mCcBccButton != null) {
Jin Cao9d358a12014-07-24 12:15:38 -07003358 mCcBccButton.setVisibility(View.GONE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003359 }
3360 }
3361
Andy Huang4fe0af82013-08-20 17:24:51 -07003362 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003363 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07003364 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003365 case COMPOSE:
3366 msgType = "new_message";
3367 break;
3368 case REPLY:
3369 msgType = "reply";
3370 break;
3371 case REPLY_ALL:
3372 msgType = "reply_all";
3373 break;
3374 case FORWARD:
3375 msgType = "forward";
3376 break;
3377 default:
3378 msgType = "unknown";
3379 break;
3380 }
Andy Huang4fe0af82013-08-20 17:24:51 -07003381 return msgType;
3382 }
3383
3384 private void logSendOrSave(boolean save) {
3385 if (!Analytics.isLoggable() || mAttachmentsView == null) {
3386 return;
3387 }
3388
3389 final String category = (save) ? "message_save" : "message_send";
3390 final int attachmentCount = getAttachments().size();
3391 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07003392 final String label;
3393 final long value;
3394 if (mComposeMode == COMPOSE) {
3395 label = Integer.toString(attachmentCount);
3396 value = attachmentCount;
3397 } else {
3398 label = null;
3399 value = 0;
3400 }
3401 Analytics.getInstance().sendEvent(category, msgType, label, value);
3402 }
3403
Mindy Pereira326c6602012-01-04 15:32:42 -08003404 @Override
3405 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003406 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003407 if (position == ComposeActivity.REPLY) {
3408 mComposeMode = ComposeActivity.REPLY;
3409 } else if (position == ComposeActivity.REPLY_ALL) {
3410 mComposeMode = ComposeActivity.REPLY_ALL;
3411 } else if (position == ComposeActivity.FORWARD) {
3412 mComposeMode = ComposeActivity.FORWARD;
3413 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003414 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003415 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003416 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003417 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003418 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003419 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003420 boolean showCc = false;
3421 boolean showBcc = false;
3422 if (mDraft != null) {
3423 // Following desktop behavior, if the user has added a BCC
3424 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003425 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003426 // Use the draft to determine what to populate.
3427 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003428 showCc = showBcc
3429 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003430 }
3431 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003432 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003433 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003434 }
Jin Caoc5c550a2014-07-29 11:53:17 -07003435 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003436 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003437 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003438 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003439 return true;
3440 }
3441
Mindy Pereirab3112a22012-06-20 12:10:03 -07003442 @VisibleForTesting
3443 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003444 // When switching between reply, reply all, forward,
3445 // follow the behavior of webview.
3446 // The contents of the following fields are cleared
3447 // so that they can be populated directly from the
3448 // ref message:
3449 // 1) Any recipient fields
3450 // 2) The subject
3451 mTo.setText("");
3452 mCc.setText("");
3453 mBcc.setText("");
3454 // Any edits to the subject are replaced with the original subject.
3455 mSubject.setText("");
3456
3457 // Any changes to the contents of the following fields are kept:
3458 // 1) Body
3459 // 2) Attachments
3460 // If the user made changes to attachments, keep their changes.
3461 if (!mAttachmentsChanged) {
3462 mAttachmentsView.deleteAllAttachments();
3463 }
3464 }
3465
Mindy Pereira326c6602012-01-04 15:32:42 -08003466 private class ComposeModeAdapter extends ArrayAdapter<String> {
3467
Jin Caof7461632014-08-11 15:21:43 -07003468 private Context mContext;
Mindy Pereira326c6602012-01-04 15:32:42 -08003469 private LayoutInflater mInflater;
3470
3471 public ComposeModeAdapter(Context context) {
3472 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3473 .getStringArray(R.array.compose_modes));
Jin Caof7461632014-08-11 15:21:43 -07003474 mContext = context;
Mindy Pereira326c6602012-01-04 15:32:42 -08003475 }
3476
3477 private LayoutInflater getInflater() {
3478 if (mInflater == null) {
Jin Caof7461632014-08-11 15:21:43 -07003479 mInflater = LayoutInflater.from(mContext);
Mindy Pereira326c6602012-01-04 15:32:42 -08003480 }
3481 return mInflater;
3482 }
3483
3484 @Override
3485 public View getView(int position, View convertView, ViewGroup parent) {
3486 if (convertView == null) {
3487 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3488 }
3489 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3490 return super.getView(position, convertView, parent);
3491 }
3492 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003493
3494 @Override
3495 public void onRespondInline(String text) {
3496 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003497 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003498 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003499 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003500 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003501 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003502 }
3503
3504 /**
3505 * Append text to the body of the message. If there is no existing body
3506 * text, just sets the body to text.
3507 *
Tony Mantler581edd42014-02-18 15:41:22 -08003508 * @param text Text to append
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003509 * @param withSignature True to append a signature.
3510 */
3511 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003512 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003513 if (bodyText != null && bodyText.length() > 0) {
3514 bodyText.append(text);
3515 } else {
3516 setBody(text, withSignature);
3517 }
3518 }
3519
3520 /**
3521 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003522 *
Tony Mantler581edd42014-02-18 15:41:22 -08003523 * @param text text to set
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003524 * @param withSignature True to append a signature.
3525 */
3526 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003527 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003528 if (withSignature) {
3529 appendSignature();
3530 }
3531 }
3532
3533 private void appendSignature() {
Tony Mantler6a7ac782014-02-19 15:22:02 -08003534 final String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3535 final int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
mindyp27083062012-11-15 09:02:01 -08003536 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003537 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003538 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003539 // Appending a signature does not count as changing text.
3540 mBodyView.removeTextChangedListener(this);
3541 mBodyView.append(convertToPrintableSignature(mSignature));
3542 mBodyView.addTextChangedListener(this);
3543 }
Tony Mantler6a7ac782014-02-19 15:22:02 -08003544 resetBodySelection();
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003545 }
3546 }
3547
3548 private String convertToPrintableSignature(String signature) {
3549 String signatureResource = getResources().getString(R.string.signature);
3550 if (signature == null) {
3551 signature = "";
3552 }
3553 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003554 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003555
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003556 @Override
3557 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003558 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3559 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003560 // Clear a signature, if there was one.
3561 mBodyView.removeTextChangedListener(this);
3562 String oldSignature = mSignature;
3563 String bodyText = getBody().getText().toString();
3564 if (!TextUtils.isEmpty(oldSignature)) {
3565 int pos = getSignatureStartPosition(oldSignature, bodyText);
3566 if (pos > -1) {
3567 mBodyView.setText(bodyText.substring(0, pos));
3568 }
3569 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003570 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003571 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003572 // TODO: handle discarding attachments when switching accounts.
3573 // Only enable save for this draft if there is any other content
3574 // in the message.
3575 if (!isBlank()) {
3576 enableSave(true);
3577 }
3578 mReplyFromChanged = true;
3579 initRecipients();
Greg Bullockd47a7042014-08-13 16:02:59 +02003580
3581 invalidateOptionsMenu();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003582 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003583 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003584
3585 public void enableSave(boolean enabled) {
3586 if (mSave != null) {
3587 mSave.setEnabled(enabled);
3588 }
3589 }
3590
Tony Mantler2558b502013-07-09 10:53:34 -07003591 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003592 // Public no-args constructor needed for fragment re-instantiation
3593 public DiscardConfirmDialogFragment() {}
3594
Tony Mantler2558b502013-07-09 10:53:34 -07003595 @Override
3596 public Dialog onCreateDialog(Bundle savedInstanceState) {
3597 return new AlertDialog.Builder(getActivity())
3598 .setMessage(R.string.confirm_discard_text)
3599 .setPositiveButton(R.string.discard,
3600 new DialogInterface.OnClickListener() {
3601 @Override
3602 public void onClick(DialogInterface dialog, int which) {
3603 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3604 }
3605 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003606 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003607 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003608 }
3609 }
3610
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003611 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003612 final DialogFragment frag = new DiscardConfirmDialogFragment();
3613 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003614 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003615 /**
3616 * Effectively discard the current message.
3617 *
3618 * This method is either invoked from the menu or from the dialog
3619 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003620 */
Tony Mantler2558b502013-07-09 10:53:34 -07003621 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003622 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003623 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3624 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003625 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003626 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003627 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3628 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003629 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003630 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003631 // This is not strictly necessary (since we should not try to
3632 // save the draft after calling this) but it ensures that if we
3633 // do save again for some reason we make a new draft rather than
3634 // trying to resave an expunged draft.
3635 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3636 }
3637 }
3638
Tony Mantler2558b502013-07-09 10:53:34 -07003639 // Display a toast to let the user know
3640 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003641
3642 // This prevents the draft from being saved in onPause().
3643 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003644 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003645 finish();
3646 }
3647
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003648 private void saveIfNeeded() {
3649 if (mAccount == null) {
3650 // We have not chosen an account yet so there's no way that we can save. This is ok,
3651 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3652 // user has not interacted with us yet and there is no real state to save.
3653 return;
3654 }
3655
3656 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003657 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003658 }
3659 }
3660
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003661 @Override
3662 public void onAttachmentDeleted() {
3663 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003664 // If we are showing any attachments, make sure we have an upper
3665 // divider.
3666 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003667 updateSaveUi();
3668 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003669
mindyp40882432012-09-06 11:07:40 -07003670 @Override
3671 public void onAttachmentAdded() {
3672 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3673 mAttachmentsView.focusLastAttachment();
3674 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003675
3676 /**
3677 * This is called any time one of our text fields changes.
3678 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003679 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003680 public void afterTextChanged(Editable s) {
3681 mTextChanged = true;
3682 updateSaveUi();
3683 }
3684
3685 @Override
3686 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3687 // Do nothing.
3688 }
3689
Marc Blank0bbc8582012-04-23 15:07:57 -07003690 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003691 public void onTextChanged(CharSequence s, int start, int before, int count) {
3692 // Do nothing.
3693 }
3694
3695
3696 // There is a big difference between the text associated with an address changing
3697 // to add the display name or to format properly and a recipient being added or deleted.
3698 // Make sure we only notify of changes when a recipient has been added or deleted.
3699 private class RecipientTextWatcher implements TextWatcher {
3700 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3701
3702 private RecipientEditTextView mView;
3703
3704 private TextWatcher mListener;
3705
3706 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3707 mView = view;
3708 mListener = listener;
3709 }
3710
3711 @Override
3712 public void afterTextChanged(Editable s) {
3713 if (hasChanged()) {
3714 mListener.afterTextChanged(s);
3715 }
3716 }
3717
3718 private boolean hasChanged() {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003719 final ArrayList<String> currRecips = buildEmailAddressList(getAddressesFromList(mView));
3720 int totalCount = currRecips.size();
Mindy Pereira75f66632012-01-11 11:42:02 -08003721 int totalPrevCount = 0;
3722 for (Entry<String, Integer> entry : mContent.entrySet()) {
3723 totalPrevCount += entry.getValue();
3724 }
3725 if (totalCount != totalPrevCount) {
3726 return true;
3727 }
3728
3729 for (String recip : currRecips) {
3730 if (!mContent.containsKey(recip)) {
3731 return true;
3732 } else {
3733 int count = mContent.get(recip) - 1;
3734 if (count < 0) {
3735 return true;
3736 } else {
3737 mContent.put(recip, count);
3738 }
3739 }
3740 }
3741 return false;
3742 }
3743
Mindy Pereira75f66632012-01-11 11:42:02 -08003744 @Override
3745 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003746 final ArrayList<String> recips = buildEmailAddressList(getAddressesFromList(mView));
Mindy Pereira75f66632012-01-11 11:42:02 -08003747 for (String recip : recips) {
3748 if (!mContent.containsKey(recip)) {
3749 mContent.put(recip, 1);
3750 } else {
3751 mContent.put(recip, (mContent.get(recip)) + 1);
3752 }
3753 }
3754 }
3755
3756 @Override
3757 public void onTextChanged(CharSequence s, int start, int before, int count) {
3758 // Do nothing.
3759 }
3760 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003761
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003762 /**
3763 * Returns a list of email addresses from the recipients. List only contains
3764 * email addresses strips additional info like the recipient's name.
3765 */
3766 private static ArrayList<String> buildEmailAddressList(String[] recips) {
3767 // Tokenize them all and put them in the list.
3768 final ArrayList<String> recipAddresses = Lists.newArrayListWithCapacity(recips.length);
3769 for (int i = 0; i < recips.length; i++) {
3770 recipAddresses.add(Rfc822Tokenizer.tokenize(recips[i])[0].getAddress());
3771 }
3772 return recipAddresses;
3773 }
3774
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003775 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3776 if (sTestSendOrSaveCallback != null && testCallback != null) {
3777 throw new IllegalStateException("Attempting to register more than one test callback");
3778 }
3779 sTestSendOrSaveCallback = testCallback;
3780 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003781
3782 @VisibleForTesting
3783 protected ArrayList<Attachment> getAttachments() {
3784 return mAttachmentsView.getAttachments();
3785 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003786
3787 @Override
3788 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3789 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003790 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3791 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3792 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003793 case REFERENCE_MESSAGE_LOADER:
3794 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3795 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003796 case LOADER_ACCOUNT_CURSOR:
3797 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3798 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003799 }
3800 return null;
3801 }
3802
3803 @Override
3804 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003805 int id = loader.getId();
3806 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003807 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003808 if (data != null && data.moveToFirst()) {
3809 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003810 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003811 initFromRefMessage(mComposeMode);
3812 finishSetup(mComposeMode, intent, null);
3813 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003814 String to = intent.getStringExtra(EXTRA_TO);
3815 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003816 mRefMessage.setTo(null);
3817 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003818 clearChangeListeners();
3819 mTo.append(to);
3820 initChangeListeners();
3821 }
3822 }
3823 } else {
3824 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003825 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003826 break;
Alice Yanga990a712013-03-13 18:37:00 -07003827 case REFERENCE_MESSAGE_LOADER:
3828 // Only populate mRefMessage and leave other fields untouched.
3829 if (data != null && data.moveToFirst()) {
3830 mRefMessage = new Message(data);
3831 }
Andy Huang9f855d62013-05-30 17:15:03 -07003832 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003833 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003834 case LOADER_ACCOUNT_CURSOR:
3835 if (data != null && data.moveToFirst()) {
3836 // there are accounts now!
3837 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003838 final ArrayList<Account> accounts = new ArrayList<Account>();
3839 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003840 do {
Ray Chen4b0c0122014-07-11 15:24:54 +02003841 account = Account.builder().buildFrom(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003842 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003843 initializedAccounts.add(account);
3844 }
3845 accounts.add(account);
3846 } while (data.moveToNext());
3847 if (initializedAccounts.size() > 0) {
3848 findViewById(R.id.wait).setVisibility(View.GONE);
3849 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3850 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003851 mAccounts = initializedAccounts.toArray(
3852 new Account[initializedAccounts.size()]);
3853
Mindy Pereirab199d172012-08-13 11:04:03 -07003854 finishCreate();
3855 invalidateOptionsMenu();
3856 } else {
3857 // Show "waiting"
3858 account = accounts.size() > 0 ? accounts.get(0) : null;
3859 showWaitFragment(account);
3860 }
3861 }
3862 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003863 }
3864 }
3865
Mindy Pereirab199d172012-08-13 11:04:03 -07003866 private void showWaitFragment(Account account) {
3867 WaitFragment fragment = getWaitFragment();
3868 if (fragment != null) {
3869 fragment.updateAccount(account);
3870 } else {
3871 findViewById(R.id.wait).setVisibility(View.VISIBLE);
Andy Huangc96efcc2014-04-09 15:30:42 -07003872 replaceFragment(WaitFragment.newInstance(account, false /* expectingMessages */),
Mindy Pereirab199d172012-08-13 11:04:03 -07003873 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3874 }
3875 }
3876
3877 private WaitFragment getWaitFragment() {
3878 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3879 }
3880
3881 private int replaceFragment(Fragment fragment, int transition, String tag) {
3882 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003883 fragmentTransaction.setTransition(transition);
3884 fragmentTransaction.replace(R.id.wait, fragment, tag);
3885 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3886 return transactionId;
3887 }
3888
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003889 @Override
3890 public void onLoaderReset(Loader<Cursor> arg0) {
3891 // Do nothing.
3892 }
Jin Cao77b4c2c2014-05-20 13:55:53 -07003893
3894 /**
3895 * Background task to convert the message's html to Spanned.
3896 */
3897 private class HtmlToSpannedTask extends AsyncTask<String, Void, Spanned> {
3898
3899 @Override
3900 protected Spanned doInBackground(String... input) {
Andy Huang9ed742c2014-06-18 02:34:50 -07003901 return HtmlUtils.htmlToSpan(input[0], mSpanConverterFactory);
Jin Cao77b4c2c2014-05-20 13:55:53 -07003902 }
3903
3904 @Override
3905 protected void onPostExecute(Spanned spanned) {
3906 mBodyView.removeTextChangedListener(ComposeActivity.this);
3907 mBodyView.setText(spanned);
3908 mTextChanged = false;
3909 mBodyView.addTextChangedListener(ComposeActivity.this);
3910 }
3911 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003912}