blob: e0341ce3fa4f1b459d1b4af5ffabe21e0c24728b [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;
Jin Caoadae7a32014-09-09 18:16:25 -070030import android.content.ClipDescription;
Mindy Pereira6349a042012-01-04 11:25:01 -080031import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080032import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080033import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070034import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080035import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080036import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070037import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080038import android.content.pm.ActivityInfo;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -070039import android.content.res.Resources;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080040import android.database.Cursor;
Jin Cao36e23872014-07-29 13:41:12 -070041import android.graphics.Rect;
Mindy Pereira6349a042012-01-04 11:25:01 -080042import android.net.Uri;
Alan Lau15490232014-03-06 14:53:14 -080043import android.os.AsyncTask;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070044import android.os.Build;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080045import android.os.Bundle;
Jin Caoadea2c82014-08-28 12:25:52 -070046import android.os.Environment;
Mindy Pereira82cc5662012-01-09 17:29:30 -080047import android.os.Handler;
48import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070049import android.os.ParcelFileDescriptor;
Mindy Pereira82cc5662012-01-09 17:29:30 -080050import android.provider.BaseColumns;
Alan Lau439aa5d2014-05-27 17:57:13 -070051import android.support.v4.app.RemoteInput;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -070052import android.support.v7.app.ActionBar;
53import android.support.v7.app.ActionBarActivity;
Andrew Sapperstein734718a2014-09-15 17:29:13 -070054import android.support.v7.view.ActionMode;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080055import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080056import android.text.Html;
Andy Huangff017272014-06-18 00:27:35 -070057import android.text.SpanWatcher;
mindyped9c2f02012-10-12 10:02:08 -070058import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080059import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080060import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080061import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080062import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080063import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070064import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070065import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080066import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080067import android.view.Menu;
68import android.view.MenuInflater;
69import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080070import android.view.View;
71import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070072import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080073import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070074import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080075import android.widget.ArrayAdapter;
Mindy Pereira433b1982012-04-03 11:53:07 -070076import android.widget.EditText;
Jin Cao36e23872014-07-29 13:41:12 -070077import android.widget.ScrollView;
Mindy Pereira6349a042012-01-04 11:25:01 -080078import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080079import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080080
Mindy Pereirac17d0732011-12-29 10:46:19 -080081import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070082import com.android.common.contacts.DataUsageStatUpdater;
Tony Mantler821e5782014-01-06 15:33:43 -080083import com.android.emailcommon.mail.Address;
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -070084import com.android.ex.chips.BaseRecipientAdapter;
85import com.android.ex.chips.DropdownChipLayouter;
Andy Huang5c5fd572012-04-08 18:19:29 -070086import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080087import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070088import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070089import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070090import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070091import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080092import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080093import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080094import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080095import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080096import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080097import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070098import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080099import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800100import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -0700101import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800102import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -0800103import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -0700104import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -0700105import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -0700106import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700107import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -0700108import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -0700109import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -0700110import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800111import com.android.mail.utils.ContentProviderTask;
Jin Cao77b4c2c2014-05-20 13:55:53 -0700112import com.android.mail.utils.HtmlUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -0700113import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800114import com.android.mail.utils.LogUtils;
Alan Lau15490232014-03-06 14:53:14 -0800115import com.android.mail.utils.NotificationActionUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800116import com.android.mail.utils.Utils;
Jin Caof14d8b32014-10-07 18:19:41 -0700117import com.android.mail.utils.ViewUtils;
Andy Huang9ed742c2014-06-18 02:34:50 -0700118import com.google.android.mail.common.html.parser.HtmlTree;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800119import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800120import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800121import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800122
Jin Caoadea2c82014-08-28 12:25:52 -0700123import java.io.File;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700124import java.io.FileNotFoundException;
125import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700126import java.io.UnsupportedEncodingException;
127import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800128import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700129import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800130import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800131import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800132import java.util.HashSet;
133import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700134import java.util.Map.Entry;
Jin Cao31bb3d62014-09-11 14:01:43 -0700135import java.util.Random;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700136import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800137import java.util.concurrent.ConcurrentHashMap;
Jin Caoaff451f2014-09-09 14:32:04 -0700138import java.util.concurrent.atomic.AtomicInteger;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800139
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700140public class ComposeActivity extends ActionBarActivity
141 implements OnClickListener, ActionBar.OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700142 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700143 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
Andrew Sappersteinffd61552014-05-14 15:04:23 -0700144 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
Jin Caoc5c550a2014-07-29 11:53:17 -0700145 RecipientEditTextView.RecipientEntryItemClickedListener, View.OnFocusChangeListener {
Scott Kennedya0287a82014-04-07 14:30:13 -0700146 /**
147 * An {@link Intent} action that launches {@link ComposeActivity}, but is handled as if the
148 * {@link Activity} were launched with no special action.
149 */
150 private static final String ACTION_LAUNCH_COMPOSE =
151 "com.android.mail.intent.action.LAUNCH_COMPOSE";
152
Mindy Pereira6349a042012-01-04 11:25:01 -0800153 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700154 public static final int COMPOSE = -1;
155 public static final int REPLY = 0;
156 public static final int REPLY_ALL = 1;
157 public static final int FORWARD = 2;
158 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800159
160 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700161 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800162
Mindy Pereira326689d2012-05-17 10:14:14 -0700163 private static final String EXTRA_SHOW_CC = "showCc";
164 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800165 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800166 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700167
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700168 private static final String UTF8_ENCODING_NAME = "UTF-8";
169
170 private static final String MAIL_TO = "mailto";
171
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700172 private static final String EXTRA_SUBJECT = "subject";
173
174 private static final String EXTRA_BODY = "body";
Jin Cao1fdbe1f2014-08-19 13:51:26 -0700175 private static final String EXTRA_TEXT_CHANGED ="extraTextChanged";
176
177 private static final String EXTRA_SKIP_PARSING_BODY = "extraSkipParsingBody";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700178
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700179 /**
180 * Expected to be html formatted text.
181 */
182 private static final String EXTRA_QUOTED_TEXT = "quotedText";
183
mindypd27b6ea2012-10-05 09:43:49 -0700184 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700185
Mark Wei62066e42012-09-13 12:07:02 -0700186 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
187
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700188 // Extra that we can get passed from other activities
Tony Mantler184ec732013-10-24 13:13:49 -0700189 @VisibleForTesting
190 protected static final String EXTRA_TO = "to";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700191 private static final String EXTRA_CC = "cc";
192 private static final String EXTRA_BCC = "bcc";
193
Régis Décampsae2d14d2016-02-23 16:16:54 +0100194 public static final String ANALYTICS_CATEGORY_ERRORS = "compose_errors";
195
Scott Kennedy60847252013-08-15 15:55:42 -0700196 /**
197 * An optional extra containing a {@link ContentValues} of values to be added to
198 * {@link SendOrSaveMessage#mValues}.
199 */
200 public static final String EXTRA_VALUES = "extra-values";
201
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700202 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700203 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
204 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700205
Alan Lau439aa5d2014-05-27 17:57:13 -0700206 private static final String LEGACY_WEAR_EXTRA = "com.google.android.wearable.extras";
207
Andrew Sapperstein09da9422014-05-30 09:48:08 -0700208 /**
209 * Constant value for the threshold to use for auto-complete suggestions
210 * for the to/cc/bcc fields.
211 */
212 private static final int COMPLETION_THRESHOLD = 1;
213
Mindy Pereira82cc5662012-01-09 17:29:30 -0800214 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
215 // Map containing information about requests to create new messages, and the id of the
216 // messages that were the result of those requests.
217 //
218 // This map is used when the activity that initiated the save a of a new message, is killed
219 // before the save has completed (and when we know the id of the newly created message). When
220 // a save is completed, the service that is running in the background, will update the map
221 //
222 // When a new ComposeActivity instance is created, it will attempt to use the information in
223 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
224 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
225 // to populate the map with data stored in shared preferences.
Jin Cao31bb3d62014-09-11 14:01:43 -0700226 private static final ConcurrentHashMap<Integer, Long> sRequestMessageIdMap =
227 new ConcurrentHashMap<Integer, Long>(10);
228 private static final Random sRandom = new Random(System.currentTimeMillis());
229
Mindy Pereira6349a042012-01-04 11:25:01 -0800230 /**
231 * Notifies the {@code Activity} that the caller is an Email
232 * {@code Activity}, so that the back behavior may be modified accordingly.
233 *
234 * @see #onAppUpPressed
235 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700236 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800237
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700238 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700239
Scott Kennedy5680ec22013-01-07 13:15:20 -0800240 /** If set, we will clear notifications for this folder. */
241 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
Alan Laue806c942014-06-06 16:19:15 -0700242 public static final String EXTRA_NOTIFICATION_CONVERSATION = "extra-notification-conversation";
Scott Kennedy5680ec22013-01-07 13:15:20 -0800243
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800244 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700245 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700246 // If this is a reply/forward then this extra will hold a uri we must query
247 // to get the original message.
248 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700249 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700250 // draft message
251 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800252 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700253 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800254 // Request numbers for activities we start
255 private static final int RESULT_PICK_ATTACHMENT = 1;
256 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700257 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700258 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700259
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700260 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
261 private static final String EXTRA_REQUEST_ID = "requestId";
262 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700263 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700264 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700265 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700266 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700267 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700268 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700269 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700270 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700271 private static final String MIME_TYPE_PHOTO = "image/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800272
Andy Huang9f855d62013-05-30 17:15:03 -0700273 private static final String KEY_INNER_SAVED_STATE = "compose_state";
274
Jin Caoadea2c82014-08-28 12:25:52 -0700275 // A single thread for running tasks in the background.
276 private static final Handler SEND_SAVE_TASK_HANDLER;
Jin Caoaff451f2014-09-09 14:32:04 -0700277 @VisibleForTesting
278 public static final AtomicInteger PENDING_SEND_OR_SAVE_TASKS_NUM = new AtomicInteger(0);
279
Régis Décampsae2d14d2016-02-23 16:16:54 +0100280 /* Path of the data directory (used for attachment uri checking). */
Jin Caoadea2c82014-08-28 12:25:52 -0700281 private static final String DATA_DIRECTORY_ROOT;
282
283 // Static initializations
Jin Cao5134be52014-05-06 19:18:38 -0700284 static {
285 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
286 handlerThread.start();
Jin Cao5134be52014-05-06 19:18:38 -0700287 SEND_SAVE_TASK_HANDLER = new Handler(handlerThread.getLooper());
Jin Caoadea2c82014-08-28 12:25:52 -0700288
289 DATA_DIRECTORY_ROOT = Environment.getDataDirectory().toString();
Jin Cao5134be52014-05-06 19:18:38 -0700290 }
291
Jin Caocfba4bb2014-10-07 19:29:15 -0700292 private final Rect mRect = new Rect();
yangyong0707b01a04a2015-05-11 15:39:02 +0800293 private Context mContext = null;
Jin Cao36e23872014-07-29 13:41:12 -0700294 private ScrollView mScrollView;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800295 private RecipientEditTextView mTo;
296 private RecipientEditTextView mCc;
297 private RecipientEditTextView mBcc;
Jin Cao9d358a12014-07-24 12:15:38 -0700298 private View mCcBccButton;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800299 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800300 private AttachmentsView mAttachmentsView;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700301 protected Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700302 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800303 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800304 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800305 private TextView mSubject;
306
Mindy Pereira326c6602012-01-04 15:32:42 -0800307 private ComposeModeAdapter mComposeModeAdapter;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700308 protected int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800309 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800310 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700311 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800312 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800313 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800314 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700315 @VisibleForTesting
316 protected FromAddressSpinner mFromSpinner;
Andy Huang5f082212014-06-11 22:19:21 -0700317 protected boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800318 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800319 private boolean mTextChanged;
320 private boolean mReplyFromChanged;
321 private MenuItem mSave;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700322 @VisibleForTesting
323 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800324 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
325 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800326 private ReplyFromAccount mDraftAccount;
Tony Mantler581edd42014-02-18 15:41:22 -0800327 private final Object mDraftLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800328
Mindy Pereira326c6602012-01-04 15:32:42 -0800329 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700330 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
331 */
332 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700333 private RecipientTextWatcher mToListener;
334 private RecipientTextWatcher mCcListener;
335 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700336 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700337 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700338 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700339 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700340
Jin Cao31bb3d62014-09-11 14:01:43 -0700341 // This is used to track pending requests, refer to sRequestMessageIdMap
mindyp1623f9b2012-11-21 12:41:16 -0800342 private int mRequestId;
343 private String mSignature;
344 private Account[] mAccounts;
345 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700346 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800347
Jin Cao23ab6ea2014-09-24 15:28:18 -0700348 // OnKeyListener solely used for intercepting CTRL+ENTER event for SEND.
349 private final View.OnKeyListener mKeyListenerForSendShortcut = new View.OnKeyListener() {
350 @Override
351 public boolean onKey(View v, int keyCode, KeyEvent event) {
352 if (event.hasModifiers(KeyEvent.META_CTRL_ON) &&
353 keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
354 doSend();
355 return true;
356 }
357 return false;
358 }
359 };
360
Andy Huang9ed742c2014-06-18 02:34:50 -0700361 private final HtmlTree.ConverterFactory mSpanConverterFactory =
362 new HtmlTree.ConverterFactory() {
363 @Override
364 public HtmlTree.Converter<Spanned> createInstance() {
365 return getSpanConverter();
366 }
367 };
368
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700369 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800370 * Can be called from a non-UI thread.
371 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800372 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700373 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
374 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800375 }
376
Mindy Pereira6349a042012-01-04 11:25:01 -0800377 /**
378 * Can be called from a non-UI thread.
379 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800380 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700381 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800382 }
383
384 /**
385 * Can be called from a non-UI thread.
386 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700387 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700388 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
389 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700390 }
391
392 /**
393 * Can be called from a non-UI thread.
394 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700395 public static void composeWithExtraValues(Context launcher, Account account,
396 String subject, final ContentValues extraValues) {
397 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
398 }
399
400 /**
401 * Can be called from a non-UI thread.
402 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800403 public static Intent createReplyIntent(final Context launcher, final Account account,
404 final Uri messageUri, final boolean isReplyAll) {
405 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
406 }
407
408 /**
409 * Can be called from a non-UI thread.
410 */
411 public static Intent createForwardIntent(final Context launcher, final Account account,
412 final Uri messageUri) {
413 return createActionIntent(launcher, account, messageUri, FORWARD);
414 }
415
Scott Kennedya0287a82014-04-07 14:30:13 -0700416 private static Intent createActionIntent(final Context context, final Account account,
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800417 final Uri messageUri, final int action) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700418 final Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
419 intent.setPackage(context.getPackageName());
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800420
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700421 updateActionIntent(account, messageUri, action, intent);
422
423 return intent;
424 }
425
426 @VisibleForTesting
427 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800428 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
429 intent.putExtra(EXTRA_ACTION, action);
430 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
431 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
432
433 return intent;
434 }
435
436 /**
437 * Can be called from a non-UI thread.
438 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800439 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700440 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800441 }
442
443 /**
444 * Can be called from a non-UI thread.
445 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800446 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700447 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
448 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800449 }
450
451 /**
452 * Can be called from a non-UI thread.
453 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800454 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700455 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800456 }
457
Alice Yang1ebc2db2013-03-14 21:21:44 -0700458 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
459 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700460 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700461 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700462 }
463
Scott Kennedya0287a82014-04-07 14:30:13 -0700464 private static void launch(Context context, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700465 String toAddress, String body, String quotedText, String subject,
466 final ContentValues extraValues) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700467 Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
468 intent.setPackage(context.getPackageName());
Mindy Pereira6349a042012-01-04 11:25:01 -0800469 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
470 intent.putExtra(EXTRA_ACTION, action);
471 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700472 if (action == EDIT_DRAFT) {
473 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
474 } else {
475 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
476 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700477 if (toAddress != null) {
478 intent.putExtra(EXTRA_TO, toAddress);
479 }
480 if (body != null) {
481 intent.putExtra(EXTRA_BODY, body);
482 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700483 if (quotedText != null) {
484 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
485 }
486 if (subject != null) {
487 intent.putExtra(EXTRA_SUBJECT, subject);
488 }
Scott Kennedy60847252013-08-15 15:55:42 -0700489 if (extraValues != null) {
490 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
491 intent.putExtra(EXTRA_VALUES, extraValues);
492 }
Andy Huange0f03202014-06-13 17:34:49 -0700493 if (action == COMPOSE) {
494 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
495 } else if (message != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700496 intent.setData(Utils.normalizeUri(message.uri));
Andy Huange0f03202014-06-13 17:34:49 -0700497 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700498 context.startActivity(intent);
Mindy Pereira6349a042012-01-04 11:25:01 -0800499 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800500
Scott Kennedya0287a82014-04-07 14:30:13 -0700501 public static void composeMailto(Context context, Account account, Uri mailto) {
502 final Intent intent = new Intent(Intent.ACTION_VIEW, mailto);
503 intent.setPackage(context.getPackageName());
Andy Huang0a2a3462013-12-20 15:56:13 -0800504 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
505 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Andy Huange0f03202014-06-13 17:34:49 -0700506 if (mailto != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700507 intent.setData(Utils.normalizeUri(mailto));
Andy Huange0f03202014-06-13 17:34:49 -0700508 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700509 context.startActivity(intent);
Andy Huang0a2a3462013-12-20 15:56:13 -0800510 }
511
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800512 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700513 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800514 super.onCreate(savedInstanceState);
Andrew Sappersteinae47d922014-10-09 14:35:51 -0700515 // Change the title for accessibility so we announce "Compose" instead
516 // of the app_name while still showing the app_name in recents.
517 setTitle(R.string.compose_title);
Mindy Pereira3528d362012-01-05 14:39:44 -0800518 setContentView(R.layout.compose);
yangyong0707b01a04a2015-05-11 15:39:02 +0800519 mContext = this;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700520 final ActionBar actionBar = getSupportActionBar();
Paul Westbrook4def3bf2014-07-01 00:38:17 -0700521 if (actionBar != null) {
522 // Hide the app icon.
Paul Westbrook5043cc22014-06-28 05:04:21 -0700523 actionBar.setIcon(null);
Paul Westbrook4def3bf2014-07-01 00:38:17 -0700524 actionBar.setDisplayUseLogoEnabled(false);
Paul Westbrook5043cc22014-06-28 05:04:21 -0700525 }
526
Andy Huang9f855d62013-05-30 17:15:03 -0700527 mInnerSavedState = (savedInstanceState != null) ?
528 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700529 checkValidAccounts();
530 }
531
532 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700533 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800534 findViews();
Tony Mantler581edd42014-02-18 15:41:22 -0800535 final Intent intent = getIntent();
536 final Message message;
537 final ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700538 mShowQuotedText = false;
Tony Mantler581edd42014-02-18 15:41:22 -0800539 final CharSequence quotedText;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700540 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700541 // Check for any of the possibly supplied accounts.;
Tony Mantler581edd42014-02-18 15:41:22 -0800542 final Account account;
Andy Huang9f855d62013-05-30 17:15:03 -0700543 if (hadSavedInstanceStateMessage(savedState)) {
544 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
545 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
Tony Mantler581edd42014-02-18 15:41:22 -0800546 message = savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700547
Andy Huang9f855d62013-05-30 17:15:03 -0700548 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800549 mRefMessage = savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700550 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700551
552 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Jin Cao31bb3d62014-09-11 14:01:43 -0700553
554 // Get the draft id from the request id if there is one.
555 if (savedState.containsKey(EXTRA_REQUEST_ID)) {
556 final int requestId = savedState.getInt(EXTRA_REQUEST_ID);
557 if (sRequestMessageIdMap.containsKey(requestId)) {
558 synchronized (mDraftLock) {
559 mDraftId = sRequestMessageIdMap.get(requestId);
560 }
561 }
562 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700563 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700564 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700565 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
566 // Initialize the message from the message in the intent
Tony Mantler581edd42014-02-18 15:41:22 -0800567 message = intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700568 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800569 mRefMessage = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
570 mRefMessageUri = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
571 quotedText = null;
Andy Huang4fe0af82013-08-20 17:24:51 -0700572
573 if (Analytics.isLoggable()) {
574 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
575 Analytics.getInstance().sendEvent(
576 "notification_action", "compose", getActionString(action), 0);
577 }
578 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700579 }
Mark Wei62066e42012-09-13 12:07:02 -0700580 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700581
582 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800583 if (mAccount == null) {
584 return;
585 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700586
Scott Kennedyfe853d32013-06-19 11:47:35 -0700587 initRecipients();
588
Scott Kennedy5680ec22013-01-07 13:15:20 -0800589 // Clear the notification and mark the conversation as seen, if necessary
590 final Folder notificationFolder =
591 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800592
Alan Laue806c942014-06-06 16:19:15 -0700593 if (notificationFolder != null) {
594 final Uri conversationUri = intent.getParcelableExtra(EXTRA_NOTIFICATION_CONVERSATION);
595 Intent actionIntent;
596 if (conversationUri != null) {
597 actionIntent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS_WEAR);
598 actionIntent.putExtra(Utils.EXTRA_CONVERSATION, conversationUri);
599 } else {
600 actionIntent = new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
601 actionIntent.setData(Utils.appendVersionQueryParameter(this,
602 notificationFolder.folderUri.fullUri));
603 }
604 actionIntent.setPackage(getPackageName());
605 actionIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
606 actionIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
607
608 startService(actionIntent);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800609 }
610
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700611 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
612 mLaunchedFromEmail = true;
613 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
614 final Uri dataUri = intent.getData();
615 if (dataUri != null) {
616 final String dataScheme = intent.getData().getScheme();
617 final String accountScheme = mAccount.composeIntentUri.getScheme();
618 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
619 }
620 }
621
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700622 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700623 mShowQuotedText = true;
624 mComposeMode = action;
Alan Lau15490232014-03-06 14:53:14 -0800625
626 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Alan Lau575255c2014-05-16 11:44:27 -0700627 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
Alan Lau439aa5d2014-05-27 17:57:13 -0700628 String wearReply = null;
Alan Lau575255c2014-05-16 11:44:27 -0700629 if (remoteInput != null) {
Alan Lau439aa5d2014-05-27 17:57:13 -0700630 LogUtils.d(LOG_TAG, "Got remote input from new api");
631 CharSequence input = remoteInput.getCharSequence(
Alan Lau575255c2014-05-16 11:44:27 -0700632 NotificationActionUtils.WEAR_REPLY_INPUT);
Alan Lau439aa5d2014-05-27 17:57:13 -0700633 if (input != null) {
634 wearReply = input.toString();
Alan Lau15490232014-03-06 14:53:14 -0800635 }
Alan Lau575255c2014-05-16 11:44:27 -0700636 } else {
Alan Lau439aa5d2014-05-27 17:57:13 -0700637 // TODO: remove after legacy code has been removed.
638 LogUtils.d(LOG_TAG,
639 "No remote input from new api, falling back to compatibility mode");
640 ClipData clipData = intent.getClipData();
641 if (clipData != null
642 && LEGACY_WEAR_EXTRA.equals(clipData.getDescription().getLabel())) {
643 Bundle extras = clipData.getItemAt(0).getIntent().getExtras();
644 if (extras != null) {
645 wearReply = extras.getString(NotificationActionUtils.WEAR_REPLY_INPUT);
646 }
647 }
648 }
649
650 if (!TextUtils.isEmpty(wearReply)) {
651 createWearReplyTask(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION,
652 mComposeMode, wearReply).execute();
653 finish();
654 return;
655 } else {
656 LogUtils.w(LOG_TAG, "remote input string is null");
Alan Lau15490232014-03-06 14:53:14 -0800657 }
658 }
659
Alice Yanga990a712013-03-13 18:37:00 -0700660 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700661 return;
662 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700663 initFromDraftMessage(message);
664 initQuotedTextFromRefMessage(mRefMessage, action);
Alice Yanga990a712013-03-13 18:37:00 -0700665 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700666 // if we should be showing quoted text but mRefMessage is null
667 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700668 if (mShowQuotedText && mRefMessage == null) {
669 if (quotedText != null) {
670 initQuotedText(quotedText, false /* shouldQuoteText */);
671 } else if (mExtraValues != null) {
672 initExtraValues(mExtraValues);
673 return;
674 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700675 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700676 } else if (action == EDIT_DRAFT) {
Tony Mantler581edd42014-02-18 15:41:22 -0800677 if (message == null) {
678 throw new IllegalStateException("Message must not be null to edit draft");
679 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700680 initFromDraftMessage(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700681 // Update the action to the draft type of the previous draft
682 switch (message.draftType) {
683 case UIProvider.DraftType.REPLY:
684 action = REPLY;
685 break;
686 case UIProvider.DraftType.REPLY_ALL:
687 action = REPLY_ALL;
688 break;
689 case UIProvider.DraftType.FORWARD:
690 action = FORWARD;
691 break;
692 case UIProvider.DraftType.COMPOSE:
693 default:
694 action = COMPOSE;
695 break;
696 }
Alice Yanga990a712013-03-13 18:37:00 -0700697 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
698
699 mShowQuotedText = message.appendRefMessageContent;
700 if (message.refMessageUri != null) {
701 // If we're editing an existing draft that was in reference to an existing message,
702 // still need to load that original message since we might need to refer to the
703 // original sender and recipients if user switches "reply <-> reply-all".
704 mRefMessageUri = message.refMessageUri;
705 mComposeMode = action;
706 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
707 return;
708 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700709 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
710 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800711 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700712 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700713 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700714 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700715 if (initFromExtras(intent)) {
716 return;
717 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700718 }
Alice Yanga990a712013-03-13 18:37:00 -0700719
720 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700721 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700722 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700723
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -0700724 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
Alan Lau15490232014-03-06 14:53:14 -0800725 private static AsyncTask<Void, Void, Message> createWearReplyTask(
726 final ComposeActivity composeActivity,
727 final Uri refMessageUri, final String[] projection, final int action,
728 final String wearReply) {
729 return new AsyncTask<Void, Void, Message>() {
730 private Intent mEmptyServiceIntent = new Intent(composeActivity, EmptyService.class);
731
732 @Override
733 protected void onPreExecute() {
734 // Start service so we won't be killed if this app is put in the background.
735 composeActivity.startService(mEmptyServiceIntent);
736 }
737
738 @Override
739 protected Message doInBackground(Void... params) {
740 Cursor cursor = composeActivity.getContentResolver()
741 .query(refMessageUri, projection, null, null, null, null);
742 if (cursor != null) {
743 try {
744 cursor.moveToFirst();
745 return new Message(cursor);
746 } finally {
747 cursor.close();
748 }
749 }
750 return null;
751 }
752
753 @Override
754 protected void onPostExecute(Message message) {
755 composeActivity.stopService(mEmptyServiceIntent);
756
757 composeActivity.mRefMessage = message;
758 composeActivity.initFromRefMessage(action);
759 composeActivity.setBody(wearReply, false);
760 composeActivity.finishSetup(action, composeActivity.getIntent(), null);
761 composeActivity.sendOrSaveWithSanityChecks(false /* save */, true /* show toast */,
762 false /* orientationChanged */, true /* autoSend */);
763 }
764 };
765 }
766
Mindy Pereirab199d172012-08-13 11:04:03 -0700767 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700768 final Account[] allAccounts = AccountUtils.getAccounts(this);
769 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700770 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
771 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700772 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700773 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
774 }
775 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700776 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700777 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700778 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700779 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700780 anySyncing = true;
781 break;
782 }
783 }
784 if (!anySyncing) {
785 // There are accounts, but none are sync'd, which is just like having no accounts.
786 mAccounts = null;
787 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
788 return;
789 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700790 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700791 finishCreate();
792 }
793 }
794
Mindy Pereira47d0e652012-07-23 09:45:07 -0700795 private Account obtainAccount(Intent intent) {
796 Account account = null;
797 Object accountExtra = null;
798 if (intent != null && intent.getExtras() != null) {
799 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
800 if (accountExtra instanceof Account) {
801 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700802 } else if (accountExtra instanceof String) {
803 // This is the Account attached to the widget compose intent.
Tony Mantler26a20752014-02-28 16:44:24 -0800804 account = Account.newInstance((String) accountExtra);
mindyp7ae042e2012-08-27 13:27:37 -0700805 if (account != null) {
806 return account;
807 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700808 }
mindyp5ee9dc42013-01-08 09:54:54 -0800809 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
810 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
811 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700812 }
Tony Mantler581edd42014-02-18 15:41:22 -0800813
814 MailAppProvider provider = MailAppProvider.getInstance();
815 String lastAccountUri = provider.getLastSentFromAccount();
816 if (TextUtils.isEmpty(lastAccountUri)) {
817 lastAccountUri = provider.getLastViewedAccount();
Mindy Pereira47d0e652012-07-23 09:45:07 -0700818 }
Tony Mantler581edd42014-02-18 15:41:22 -0800819 if (!TextUtils.isEmpty(lastAccountUri)) {
820 accountExtra = Uri.parse(lastAccountUri);
821 }
822
Mindy Pereirab199d172012-08-13 11:04:03 -0700823 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700824 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
825 // For backwards compatibility, we need to check account
826 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700827 for (Account a : mAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -0700828 if (a.getEmailAddress().equals(accountExtra)) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700829 account = a;
830 }
831 }
832 } else if (accountExtra instanceof Uri) {
833 // The uri of the last viewed account is what is stored in
834 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700835 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700836 if (a.uri.equals(accountExtra)) {
837 account = a;
838 }
839 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700840 }
841 if (account == null) {
842 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700843 }
844 }
845 return account;
846 }
847
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700848 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800849 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700850 // Don't bother with the intent if we have procured a message from the
851 // intent already.
852 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
853 initAttachmentsFromIntent(intent);
854 }
Alice Yanga990a712013-03-13 18:37:00 -0700855 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700856 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
857 action);
mindypd4a48662012-11-08 17:13:49 -0800858
859 // If this is a draft message, the draft account is whatever account was
860 // used to open the draft message in Compose.
861 if (mDraft != null) {
862 mDraftAccount = mReplyFromAccount;
863 }
864
Mindy Pereira75f66632012-01-11 11:42:02 -0800865 initChangeListeners();
Jin Cao32973b42014-05-06 16:12:11 -0700866
867 // These two should be identical since we check CC and BCC the same way
868 boolean showCc = !TextUtils.isEmpty(mCc.getText()) || (savedInstanceState != null &&
869 savedInstanceState.getBoolean(EXTRA_SHOW_CC));
870 boolean showBcc = !TextUtils.isEmpty(mBcc.getText()) || (savedInstanceState != null &&
871 savedInstanceState.getBoolean(EXTRA_SHOW_BCC));
872 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereira326689d2012-05-17 10:14:14 -0700873 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700874 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800875
Tony Mantler581edd42014-02-18 15:41:22 -0800876 mRespondedInline = mInnerSavedState != null &&
877 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE);
mindyp1623f9b2012-11-21 12:41:16 -0800878 if (mRespondedInline) {
879 mQuotedTextView.setVisibility(View.GONE);
880 }
Jin Cao1fdbe1f2014-08-19 13:51:26 -0700881
882 mTextChanged = (savedInstanceState != null) ?
883 savedInstanceState.getBoolean(EXTRA_TEXT_CHANGED) : false;
Mindy Pereira71c9e562012-05-17 11:01:02 -0700884 }
885
Scott Kennedyff8553f2013-04-05 20:57:44 -0700886 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700887 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
888 }
889
Mindy Pereira71c9e562012-05-17 11:01:02 -0700890 private void updateHideOrShowQuotedText(boolean showQuotedText) {
891 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700892 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700893 }
894
895 private void setFocus(int action) {
896 if (action == EDIT_DRAFT) {
897 int type = mDraft.draftType;
898 switch (type) {
899 case UIProvider.DraftType.COMPOSE:
900 case UIProvider.DraftType.FORWARD:
901 action = COMPOSE;
902 break;
903 case UIProvider.DraftType.REPLY:
904 case UIProvider.DraftType.REPLY_ALL:
905 default:
906 action = REPLY;
907 break;
908 }
909 }
910 switch (action) {
911 case FORWARD:
912 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800913 if (TextUtils.isEmpty(mTo.getText())) {
914 mTo.requestFocus();
915 break;
916 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700917 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700918 case REPLY:
919 case REPLY_ALL:
920 default:
921 focusBody();
922 break;
923 }
924 }
925
926 /**
927 * Focus the body of the message.
928 */
Tony Mantler6a7ac782014-02-19 15:22:02 -0800929 private void focusBody() {
Mindy Pereira433b1982012-04-03 11:53:07 -0700930 mBodyView.requestFocus();
Tony Mantler6a7ac782014-02-19 15:22:02 -0800931 resetBodySelection();
932 }
Mindy Pereira433b1982012-04-03 11:53:07 -0700933
Tony Mantler6a7ac782014-02-19 15:22:02 -0800934 private void resetBodySelection() {
935 int length = mBodyView.getText().length();
Mindy Pereira433b1982012-04-03 11:53:07 -0700936 int signatureStartPos = getSignatureStartPosition(
937 mSignature, mBodyView.getText().toString());
938 if (signatureStartPos > -1) {
939 // In case the user deleted the newlines...
940 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700941 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700942 // Move cursor to the end.
943 mBodyView.setSelection(length);
944 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800945 }
946
947 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700948 protected void onStart() {
949 super.onStart();
950
951 Analytics.getInstance().activityStart(this);
952 }
953
954 @Override
955 protected void onStop() {
956 super.onStop();
957
958 Analytics.getInstance().activityStop(this);
959 }
960
961 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800962 protected void onResume() {
963 super.onResume();
964 // Update the from spinner as other accounts
965 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800966 if (mFromSpinner != null && mAccount != null) {
Andrew Sappersteina01ddca2014-03-04 10:59:56 -0800967 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800968 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800969 }
970
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800971 @Override
972 protected void onPause() {
973 super.onPause();
974
Mindy Pereiraa2148332012-07-02 13:54:14 -0700975 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700976 // Don't save unnecessary drafts if we are only changing the orientation.
977 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700978 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700979
Andy Huange003b4c2013-08-16 10:32:05 -0700980 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700981 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
982 // because that method can be invoked many times in a single compose session.)
983 logSendOrSave(true /* save */);
984 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700985 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800986 }
987
988 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700989 protected void onActivityResult(int request, int result, Intent data) {
Andy Huang5f082212014-06-11 22:19:21 -0700990 if (request == RESULT_PICK_ATTACHMENT) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700991 mAddingAttachment = false;
Andy Huang5f082212014-06-11 22:19:21 -0700992 if (result == RESULT_OK) {
993 addAttachmentAndUpdateView(data);
994 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700995 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700996 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700997 if (result != RESULT_OK) {
998 finish();
999 } else {
1000 // Watch for accounts to show up!
1001 // restart the loader to get the updated list of accounts
1002 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
1003 showWaitFragment(null);
1004 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001005 }
1006 }
1007
1008 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -07001009 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -07001010 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
1011 if (hasAccounts) {
1012 clearChangeListeners();
1013 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001014 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -07001015 if (mInnerSavedState != null) {
1016 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
1017 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
1018 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001019 // There should be a focus and it should be an EditText since we
1020 // only save these extras if these conditions are true.
1021 EditText focusEditText = (EditText) getCurrentFocus();
1022 final int length = focusEditText.getText().length();
1023 if (selectionStart < length && selectionEnd < length) {
1024 focusEditText.setSelection(selectionStart, selectionEnd);
1025 }
1026 }
1027 }
Yorke Lee7bec2b92013-04-26 08:31:42 -07001028 if (hasAccounts) {
1029 initChangeListeners();
1030 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001031 }
1032
1033 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07001034 protected void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001035 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -07001036 final Bundle inner = new Bundle();
1037 saveState(inner);
1038 state.putBundle(KEY_INNER_SAVED_STATE, inner);
1039 }
1040
1041 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -07001042 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
1043 if (mAccounts == null || mAccounts.length == 0) {
1044 return;
1045 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001046 // The framework is happy to save and restore the selection but only if it also saves and
1047 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
1048 // this manually.
1049 View focus = getCurrentFocus();
1050 if (focus != null && focus instanceof EditText) {
1051 EditText focusEditText = (EditText) focus;
1052 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
1053 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
1054 }
Paul Westbrook6273e962012-04-23 10:44:15 -07001055
1056 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -07001057 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -07001058 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
1059 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
1060 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001061 if (selectedReplyFromAccount != null) {
1062 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
1063 .toString());
1064 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
1065 } else {
1066 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
1067 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001068
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001069 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
1070 // We don't have a draft id, and we have a request id,
1071 // save the request id.
1072 state.putInt(EXTRA_REQUEST_ID, mRequestId);
1073 }
1074
1075 // We want to restore the current mode after a pause
1076 // or rotation.
1077 int mode = getMode();
1078 state.putInt(EXTRA_ACTION, mode);
1079
Jin Cao77b4c2c2014-05-20 13:55:53 -07001080 final Message message = createMessage(selectedReplyFromAccount, mRefMessage, mode,
1081 removeComposingSpans(mBodyView.getText()));
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001082 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -08001083 message.id = mDraft.id;
1084 message.serverId = mDraft.serverId;
1085 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001086 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001087 state.putParcelable(EXTRA_MESSAGE, message);
1088
1089 if (mRefMessage != null) {
1090 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001091 } else if (message.appendRefMessageContent) {
1092 // If we have no ref message but should be appending
1093 // ref message content, we have orphaned quoted text. Save it.
1094 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001095 }
Mindy Pereira326689d2012-05-17 10:14:14 -07001096 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
1097 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -08001098 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -08001099 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -07001100 state.putParcelableArrayList(
1101 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -07001102
1103 state.putParcelable(EXTRA_VALUES, mExtraValues);
Jin Cao1fdbe1f2014-08-19 13:51:26 -07001104
1105 state.putBoolean(EXTRA_TEXT_CHANGED, mTextChanged);
1106 // On configuration changes, we don't actually need to parse the body html ourselves because
1107 // the framework can correctly restore the body EditText to its exact original state.
1108 state.putBoolean(EXTRA_SKIP_PARSING_BODY, isChangingConfigurations());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001109 }
1110
1111 private int getMode() {
1112 int mode = ComposeActivity.COMPOSE;
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001113 final ActionBar actionBar = getSupportActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001114 if (actionBar != null
1115 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001116 mode = actionBar.getSelectedNavigationIndex();
1117 }
1118 return mode;
1119 }
1120
Jin Cao77b4c2c2014-05-20 13:55:53 -07001121 /**
1122 * This function might be called from a background thread, so be sure to move everything that
1123 * can potentially modify the UI to the main thread (e.g. removeComposingSpans for body).
1124 */
Anthony Lee2a3cc132014-04-22 14:15:25 -07001125 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, Message refMessage,
Jin Cao77b4c2c2014-05-20 13:55:53 -07001126 int mode, Spanned body) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001127 Message message = new Message();
1128 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -07001129 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001130 message.uri = null;
1131 message.conversationUri = null;
1132 message.subject = mSubject.getText().toString();
1133 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001134 message.setTo(formatSenders(mTo.getText().toString()));
1135 message.setCc(formatSenders(mCc.getText().toString()));
1136 message.setBcc(formatSenders(mBcc.getText().toString()));
1137 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001138 message.dateReceivedMs = 0;
Jin Cao77b4c2c2014-05-20 13:55:53 -07001139 message.bodyHtml = spannedBodyToHtml(body, true);
Jin Cao7800d292014-08-24 11:31:15 -07001140 message.bodyText = body.toString();
Jin Cao0a8e8222014-10-02 11:27:52 -07001141 // Fallback to use the text version if html conversion fails for whatever the reason.
Jin Caoefd62cb2014-10-02 17:50:07 -07001142 final String htmlInPlainText = Utils.convertHtmlToPlainText(message.bodyHtml);
Jin Cao0d9a37e2014-10-14 12:07:29 -07001143 if (message.bodyText != null && message.bodyText.trim().length() > 0 &&
1144 TextUtils.isEmpty(htmlInPlainText)) {
Jin Caoefd62cb2014-10-02 17:50:07 -07001145 LogUtils.w(LOG_TAG, "FAILED HTML CONVERSION: from %d to %d", message.bodyText.length(),
1146 htmlInPlainText.length());
Régis Décampsae2d14d2016-02-23 16:16:54 +01001147 Analytics.getInstance().sendEvent(ANALYTICS_CATEGORY_ERRORS,
1148 "failed_html_conversion", null, 0);
Jin Cao0a8e8222014-10-02 11:27:52 -07001149 message.bodyHtml = "<p>" + message.bodyText + "</p>";
1150 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001151 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -07001152 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001153 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
1154 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
1155 message.hasAttachments = attachments != null && attachments.size() > 0;
1156 message.attachmentListUri = null;
1157 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001158 message.alwaysShowImages = false;
1159 message.attachmentsJson = Attachment.toJSONArray(attachments);
1160 CharSequence quotedText = mQuotedTextView.getQuotedText();
Anthony Lee2a3cc132014-04-22 14:15:25 -07001161 message.quotedTextOffset = -1; // Just a default value.
1162 if (refMessage != null && !TextUtils.isEmpty(quotedText)) {
1163 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1164 // We want the index to point to just the quoted text and not the
1165 // "On December 25, 2014..." part of it.
1166 message.quotedTextOffset =
1167 QuotedTextView.getQuotedTextOffset(quotedText.toString());
1168 } else if (!TextUtils.isEmpty(refMessage.bodyText)) {
1169 // We want to point to the entire quoted text.
1170 message.quotedTextOffset = QuotedTextView.findQuotedTextIndex(quotedText);
1171 }
1172 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001173 message.accountUri = null;
Greg Bullock14fd3042014-08-12 09:21:15 +02001174 message.setFrom(computeFromForAccount(selectedReplyFromAccount));
1175 message.draftType = getDraftType(mode);
1176 return message;
1177 }
1178
1179 protected String computeFromForAccount(ReplyFromAccount selectedReplyFromAccount) {
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001180 final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
1181 : mAccount != null ? mAccount.getEmailAddress() : null;
Tony Mantlerf441d142013-10-22 11:46:00 -07001182 final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
1183 : mAccount != null ? mAccount.getSenderName() : null;
Tony Mantler821e5782014-01-06 15:33:43 -08001184 final Address address = new Address(email, senderName);
Greg Bullock14fd3042014-08-12 09:21:15 +02001185 return address.toHeader();
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001186 }
1187
Scott Kennedyff8553f2013-04-05 20:57:44 -07001188 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -07001189 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
1190 return string.substring(0, string.length() - 1);
1191 }
1192 return string;
1193 }
1194
Mindy Pereira818143e2012-01-11 13:59:49 -08001195 @VisibleForTesting
Andy Huang91ede362014-01-21 19:16:00 -08001196 protected void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -07001197 if (account == null) {
1198 return;
1199 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001200 if (!account.equals(mAccount)) {
1201 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001202 mCachedSettings = mAccount.settings;
1203 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001204 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001205 if (mAccount != null) {
Tony Mantler79b11562013-10-09 15:31:50 -07001206 MailActivity.setNfcMessage(mAccount.getEmailAddress());
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001207 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001208 }
1209
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001210 private void initFromSpinner(Bundle bundle, int action) {
1211 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001212 action = COMPOSE;
1213 }
Andrew Sappersteina01ddca2014-03-04 10:59:56 -08001214 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001215
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001216 if (bundle != null) {
1217 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
1218 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
1219 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
1220 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001221 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001222 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
1223 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001224 }
1225 if (mReplyFromAccount == null) {
1226 if (mDraft != null) {
Jin Cao24c892d2014-09-29 11:06:03 -07001227 mReplyFromAccount = getReplyFromAccountFromDraft(mDraft);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001228 } else if (mRefMessage != null) {
1229 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
1230 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001231 }
1232 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -07001233 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001234 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001235
Mindy Pereira62de1b12012-04-06 12:17:56 -07001236 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001237
Mindy Pereira62de1b12012-04-06 12:17:56 -07001238 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001239 // If there is only 1 account, just show that account.
1240 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001241 // send mail from / save drafts to.
1242 mFromStatic.setVisibility(View.GONE);
Andy Huangca4676f2014-01-16 13:22:20 -08001243 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001244 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001245 } else {
1246 mFromStatic.setVisibility(View.VISIBLE);
Andy Huangca4676f2014-01-16 13:22:20 -08001247 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001248 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001249 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001250 }
1251
Mindy Pereira62de1b12012-04-06 12:17:56 -07001252 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1253 if (refMessage.accountUri != null) {
1254 // This must be from combined inbox.
1255 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1256 for (ReplyFromAccount from : replyFromAccounts) {
1257 if (from.account.uri.equals(refMessage.accountUri)) {
1258 return from;
1259 }
1260 }
1261 return null;
1262 } else {
1263 return getReplyFromAccount(account, refMessage);
1264 }
1265 }
1266
1267 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001268 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001269 * return who the message should be sent from.
1270 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001271 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001272 * @return the address from which to reply.
1273 */
1274 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1275 // First see if we are supposed to use the default address or
1276 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001277 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001278 return getDefaultReplyFromAccount(account);
1279 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001280 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001281 // all the message recipients and find one that matches
1282 // a custom from or account.
1283 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001284 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1285 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001286 return getMatchingRecipient(account, allRecipients);
1287 }
1288 }
1289
1290 /**
1291 * Compare all the recipients of an email to the current account and all
1292 * custom addresses associated with that account. Return the match if there
1293 * is one, or the default account if there isn't.
1294 */
1295 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1296 // Tokenize the list and place in a hashmap.
1297 ReplyFromAccount matchingReplyFrom = null;
1298 Rfc822Token[] tokens;
1299 HashSet<String> recipientsMap = new HashSet<String>();
1300 for (String address : sentTo) {
1301 tokens = Rfc822Tokenizer.tokenize(address);
Tony Mantler581edd42014-02-18 15:41:22 -08001302 for (final Rfc822Token token : tokens) {
1303 recipientsMap.add(token.getAddress());
Mindy Pereira62de1b12012-04-06 12:17:56 -07001304 }
1305 }
1306
1307 int matchingAddressCount = 0;
1308 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001309 customFroms = account.getReplyFroms();
1310 if (customFroms != null) {
1311 for (ReplyFromAccount entry : customFroms) {
1312 if (recipientsMap.contains(entry.address)) {
1313 matchingReplyFrom = entry;
1314 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001315 }
1316 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001317 }
1318 if (matchingAddressCount > 1) {
1319 matchingReplyFrom = getDefaultReplyFromAccount(account);
1320 }
1321 return matchingReplyFrom;
1322 }
1323
Scott Kennedyff8553f2013-04-05 20:57:44 -07001324 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1325 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001326 if (from.isDefault) {
1327 return from;
1328 }
1329 }
Tony Mantlerf441d142013-10-22 11:46:00 -07001330 return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1331 account.getSenderName(), account.getEmailAddress(), true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001332 }
1333
Jin Cao24c892d2014-09-29 11:06:03 -07001334 private ReplyFromAccount getReplyFromAccountFromDraft(final Message msg) {
Tony Mantlerf441d142013-10-22 11:46:00 -07001335 final Address[] draftFroms = Address.parse(msg.getFrom());
1336 final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
Mindy Pereira62de1b12012-04-06 12:17:56 -07001337 ReplyFromAccount replyFromAccount = null;
Jin Cao24c892d2014-09-29 11:06:03 -07001338 // Do not try to check against the "default" account because the default might be an alias.
1339 for (ReplyFromAccount fromAccount : mFromSpinner.getReplyFromAccounts()) {
1340 if (TextUtils.equals(fromAccount.address, sender)) {
1341 replyFromAccount = fromAccount;
1342 break;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001343 }
1344 }
1345 return replyFromAccount;
1346 }
1347
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001348 private void findViews() {
Jin Cao36e23872014-07-29 13:41:12 -07001349 mScrollView = (ScrollView) findViewById(R.id.compose);
1350 mScrollView.setVisibility(View.VISIBLE);
Jin Cao9d358a12014-07-24 12:15:38 -07001351 mCcBccButton = findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001352 if (mCcBccButton != null) {
1353 mCcBccButton.setOnClickListener(this);
1354 }
1355 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001356 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira818143e2012-01-11 13:59:49 -08001357 mTo = (RecipientEditTextView) findViewById(R.id.to);
Jin Cao23ab6ea2014-09-24 15:28:18 -07001358 mTo.setOnKeyListener(mKeyListenerForSendShortcut);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001359 initializeRecipientEditTextView(mTo);
Jin Cao15f09d72014-08-08 13:27:34 -07001360 mTo.setAlternatePopupAnchor(findViewById(R.id.compose_to_dropdown_anchor));
Mindy Pereira818143e2012-01-11 13:59:49 -08001361 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Jin Cao23ab6ea2014-09-24 15:28:18 -07001362 mCc.setOnKeyListener(mKeyListenerForSendShortcut);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001363 initializeRecipientEditTextView(mCc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001364 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Jin Cao23ab6ea2014-09-24 15:28:18 -07001365 mBcc.setOnKeyListener(mKeyListenerForSendShortcut);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001366 initializeRecipientEditTextView(mBcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001367 // TODO: add special chips text change watchers before adding
1368 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001369 mSubject = (TextView) findViewById(R.id.subject);
Jin Cao23ab6ea2014-09-24 15:28:18 -07001370 mSubject.setOnKeyListener(mKeyListenerForSendShortcut);
mindyp62d3ec72012-08-24 13:04:09 -07001371 mSubject.setOnEditorActionListener(this);
Jin Caoc5c550a2014-07-29 11:53:17 -07001372 mSubject.setOnFocusChangeListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001373 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1374 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001375 mBodyView = (EditText) findViewById(R.id.body);
Jin Cao23ab6ea2014-09-24 15:28:18 -07001376 mBodyView.setOnKeyListener(mKeyListenerForSendShortcut);
Jin Caoc5c550a2014-07-29 11:53:17 -07001377 mBodyView.setOnFocusChangeListener(this);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001378 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001379 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001380 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001381 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Jin Cao95b8a7b2014-09-02 16:45:42 -07001382
1383 // Bottom placeholder to forward click events to the body
1384 findViewById(R.id.composearea_tap_trap_bottom).setOnClickListener(new OnClickListener() {
1385 @Override
1386 public void onClick(View v) {
1387 mBodyView.requestFocus();
1388 mBodyView.setSelection(mBodyView.getText().length());
1389 }
1390 });
Mindy Pereira6349a042012-01-04 11:25:01 -08001391 }
1392
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001393 private void initializeRecipientEditTextView(RecipientEditTextView view) {
1394 view.setTokenizer(new Rfc822Tokenizer());
1395 view.setThreshold(COMPLETION_THRESHOLD);
1396 }
1397
mindyp62d3ec72012-08-24 13:04:09 -07001398 @Override
1399 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1400 if (action == EditorInfo.IME_ACTION_DONE) {
1401 focusBody();
1402 return true;
1403 }
1404 return false;
1405 }
1406
Andy Huang91ede362014-01-21 19:16:00 -08001407 /**
1408 * Convert the body text (in {@link Spanned} form) to ready-to-send HTML format as a plain
1409 * String.
1410 *
1411 * @param body the body text including fancy style spans
Jin Cao77b4c2c2014-05-20 13:55:53 -07001412 * @param removedComposing whether the function already removed composingSpans. Necessary
1413 * because we cannot call removeComposingSpans from a background thread.
Andy Huang91ede362014-01-21 19:16:00 -08001414 * @return HTML formatted body that's suitable for sending or saving
1415 */
Jin Cao77b4c2c2014-05-20 13:55:53 -07001416 private String spannedBodyToHtml(Spanned body, boolean removedComposing) {
1417 if (!removedComposing) {
1418 body = removeComposingSpans(body);
1419 }
1420 final HtmlifyBeginResult r = onHtmlifyBegin(body);
Andy Huang91ede362014-01-21 19:16:00 -08001421 return onHtmlifyEnd(Html.toHtml(r.result), r.extras);
1422 }
1423
1424 /**
1425 * A hook for subclasses to convert custom spans in the body text prior to system HTML
1426 * conversion. That HTML conversion is lossy, so anything above and beyond its capability
1427 * has to be handled here.
1428 *
1429 * @param body
1430 * @return a copy of the body text with custom spans replaced with HTML
1431 */
1432 protected HtmlifyBeginResult onHtmlifyBegin(Spanned body) {
1433 return new HtmlifyBeginResult(body, null /* extras */);
1434 }
1435
1436 protected String onHtmlifyEnd(String html, Object extras) {
1437 return html;
1438 }
1439
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001440 protected TextView getBody() {
1441 return mBodyView;
1442 }
1443
1444 @VisibleForTesting
Andy Huang0a2a3462013-12-20 15:56:13 -08001445 public String getBodyHtml() {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001446 return spannedBodyToHtml(mBodyView.getText(), false);
Andy Huang0a2a3462013-12-20 15:56:13 -08001447 }
1448
1449 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001450 public Account getFromAccount() {
1451 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1452 mReplyFromAccount.account : mAccount;
1453 }
1454
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001455 private void clearChangeListeners() {
1456 mSubject.removeTextChangedListener(this);
1457 mBodyView.removeTextChangedListener(this);
1458 mTo.removeTextChangedListener(mToListener);
1459 mCc.removeTextChangedListener(mCcListener);
1460 mBcc.removeTextChangedListener(mBccListener);
1461 mFromSpinner.setOnAccountChangedListener(null);
1462 mAttachmentsView.setAttachmentChangesListener(null);
1463 }
1464
Mindy Pereira75f66632012-01-11 11:42:02 -08001465 // Now that the message has been initialized from any existing draft or
1466 // ref message data, set up listeners for any changes that occur to the
1467 // message.
1468 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001469 // Make sure we only add text changed listeners once!
1470 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001471 mSubject.addTextChangedListener(this);
1472 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001473 if (mToListener == null) {
1474 mToListener = new RecipientTextWatcher(mTo, this);
1475 }
1476 mTo.addTextChangedListener(mToListener);
1477 if (mCcListener == null) {
1478 mCcListener = new RecipientTextWatcher(mCc, this);
1479 }
1480 mCc.addTextChangedListener(mCcListener);
1481 if (mBccListener == null) {
1482 mBccListener = new RecipientTextWatcher(mBcc, this);
1483 }
1484 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001485 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001486 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001487 }
1488
Alice Yanga990a712013-03-13 18:37:00 -07001489 private void initActionBar() {
1490 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Andrew Sapperstein52882ff2014-07-27 12:30:18 -07001491 final ActionBar actionBar = getSupportActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001492 if (actionBar == null) {
1493 return;
1494 }
Alice Yanga990a712013-03-13 18:37:00 -07001495 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001496 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
Jin Caof0a0b4c2014-08-20 15:35:50 -07001497 actionBar.setTitle(R.string.compose_title);
Mindy Pereira326c6602012-01-04 15:32:42 -08001498 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001499 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001500 if (mComposeModeAdapter == null) {
Jin Caof7461632014-08-11 15:21:43 -07001501 mComposeModeAdapter = new ComposeModeAdapter(actionBar.getThemedContext());
Mindy Pereira326c6602012-01-04 15:32:42 -08001502 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001503 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1504 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001505 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001506 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001507 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001508 break;
1509 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001510 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001511 break;
1512 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001513 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001514 break;
1515 }
1516 }
Paul Westbrook4def3bf2014-07-01 00:38:17 -07001517 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
1518 ActionBar.DISPLAY_HOME_AS_UP);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001519 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001520 }
1521
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001522 private void initFromRefMessage(int action) {
1523 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001524
1525 // Check if To: address and email body needs to be prefilled based on extras.
1526 // This is used for reporting rendering feedback.
1527 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1528 Intent intent = getIntent();
1529 if (intent.getExtras() != null) {
1530 String toAddresses = intent.getStringExtra(EXTRA_TO);
1531 if (toAddresses != null) {
1532 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1533 }
1534 String body = intent.getStringExtra(EXTRA_BODY);
1535 if (body != null) {
1536 setBody(body, false /* withSignature */);
1537 }
1538 }
1539 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001540 }
1541
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001542 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001543 setSubject(mRefMessage, action);
1544 // Setup recipients
1545 if (action == FORWARD) {
1546 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001547 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001548 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001549 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001550 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1551 initAttachments(mRefMessage);
1552 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001553 }
1554
Andy Huang9ed742c2014-06-18 02:34:50 -07001555 protected HtmlTree.Converter<Spanned> getSpanConverter() {
1556 return new HtmlUtils.SpannedConverter();
1557 }
1558
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001559 private void initFromDraftMessage(Message message) {
Jin Cao31bb3d62014-09-11 14:01:43 -07001560 LogUtils.d(LOG_TAG, "Initializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001561
Jin Cao31bb3d62014-09-11 14:01:43 -07001562 synchronized (mDraftLock) {
1563 // Draft id might already be set by the request to id map, if so we don't need to set it
1564 if (mDraftId == UIProvider.INVALID_MESSAGE_ID) {
1565 mDraftId = message.id;
1566 } else {
1567 message.id = mDraftId;
1568 }
1569 mDraft = message;
1570 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001571 mSubject.setText(message.subject);
1572 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Jin Cao1fdbe1f2014-08-19 13:51:26 -07001573
Tony Mantler9016a5e2013-07-19 11:54:17 -07001574 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001575 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001576 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1577 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001578 if (message.hasAttachments) {
1579 List<Attachment> attachments = message.getAttachments();
1580 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001581 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001582 }
1583 }
Jin Cao1fdbe1f2014-08-19 13:51:26 -07001584
1585 // If we don't need to re-populate the body, and the quoted text will be restored from
1586 // ref message. So we can skip rest of this code.
1587 if (mInnerSavedState != null && mInnerSavedState.getBoolean(EXTRA_SKIP_PARSING_BODY)) {
1588 LogUtils.i(LOG_TAG, "Skipping manually populating body and quoted text from draft.");
1589 return;
1590 }
1591
Anthony Lee2a3cc132014-04-22 14:15:25 -07001592 int quotedTextIndex = message.appendRefMessageContent ? message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001593 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001594 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001595 if (!TextUtils.isEmpty(message.bodyHtml)) {
Jin Cao32f453b2014-07-22 14:21:15 -07001596 String body = message.bodyHtml;
Mindy Pereira002ff522012-05-30 10:31:26 -07001597 if (quotedTextIndex > -1) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07001598 // Find the offset in the html text of the actual quoted text and strip it out.
1599 // Note that the actual quotedTextOffset in the message has not changed as
1600 // this different offset is used only for display purposes. They point to different
1601 // parts of the original message. Please see the comments in QuoteTextView
1602 // to see the differences.
Mindy Pereira752222d2012-07-19 09:58:53 -07001603 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1604 if (quotedTextIndex > -1) {
Jin Cao32f453b2014-07-22 14:21:15 -07001605 body = message.bodyHtml.substring(0, quotedTextIndex);
Mindy Pereira752222d2012-07-19 09:58:53 -07001606 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1607 message.bodyHtml.length());
1608 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001609 }
Jin Cao32f453b2014-07-22 14:21:15 -07001610 new HtmlToSpannedTask().execute(body);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001611 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001612 final String body = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07001613 final CharSequence bodyText;
1614 if (TextUtils.isEmpty(body)) {
1615 bodyText = "";
1616 quotedText = null;
1617 } else {
1618 if (quotedTextIndex > body.length()) {
1619 // Sanity check to guarantee that we will not over index the String.
1620 // If this happens there is a bigger problem. This should never happen hence
1621 // the wtf logging.
1622 quotedTextIndex = -1;
1623 LogUtils.wtf(LOG_TAG, "quotedTextIndex (%d) > body.length() (%d)",
1624 quotedTextIndex, body.length());
1625 }
1626 bodyText = quotedTextIndex > -1 ? body.substring(0, quotedTextIndex) : body;
1627 if (quotedTextIndex > -1) {
1628 quotedText = body.substring(quotedTextIndex);
1629 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001630 }
Jin Cao738cafe2014-09-02 16:10:29 -07001631 setBody(bodyText, false);
Mindy Pereira002ff522012-05-30 10:31:26 -07001632 }
1633 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001634 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001635 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001636 }
1637
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001638 /**
1639 * Fill all the widgets with the content found in the Intent Extra, if any.
1640 * Also apply the same style to all widgets. Note: if initFromExtras is
1641 * called as a result of switching between reply, reply all, and forward per
1642 * the latest revision of Gmail, and the user has already made changes to
1643 * attachments on a previous incarnation of the message (as a reply, reply
1644 * all, or forward), the original attachments from the message will not be
1645 * re-instantiated. The user's changes will be respected. This follows the
1646 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001647 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001648 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001649 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001650 // If we were invoked with a SENDTO intent, the value
1651 // should take precedence
1652 final Uri dataUri = intent.getData();
1653 if (dataUri != null) {
1654 if (MAIL_TO.equals(dataUri.getScheme())) {
1655 initFromMailTo(dataUri.toString());
1656 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001657 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001658 String toText = dataUri.getSchemeSpecificPart();
1659 if (toText != null) {
1660 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001661 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001662 }
1663 }
1664 }
1665 }
1666
1667 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1668 if (extraStrings != null) {
1669 addToAddresses(Arrays.asList(extraStrings));
1670 }
1671 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1672 if (extraStrings != null) {
1673 addCcAddresses(Arrays.asList(extraStrings), null);
1674 }
1675 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1676 if (extraStrings != null) {
1677 addBccAddresses(Arrays.asList(extraStrings));
1678 }
1679
1680 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1681 if (extraString != null) {
1682 mSubject.setText(extraString);
1683 }
1684
1685 for (String extra : ALL_EXTRAS) {
1686 if (intent.hasExtra(extra)) {
1687 String value = intent.getStringExtra(extra);
1688 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001689 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001690 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001691 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001692 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001693 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001694 } else if (EXTRA_SUBJECT.equals(extra)) {
1695 mSubject.setText(value);
1696 } else if (EXTRA_BODY.equals(extra)) {
1697 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001698 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1699 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001700 }
1701 }
1702 }
1703
1704 Bundle extras = intent.getExtras();
1705 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001706 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
Jin Caoa8f34ff2014-07-24 14:43:57 -07001707 setBody((text != null) ? text : "", true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001708
1709 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001710 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001711
1712 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1713 if (mExtraValues != null) {
1714 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1715 initExtraValues(mExtraValues);
1716 return true;
1717 }
1718
1719 return false;
1720 }
1721
1722 protected void initExtraValues(ContentValues extraValues) {
1723 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001724 }
1725
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001726
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001727 @VisibleForTesting
1728 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001729 // TODO: handle the case where there are spaces in the display name as
1730 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1731 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001732 // Since URLDecode.decode changes + into ' ', and + is a valid
1733 // email character, we need to find/ replace these ourselves before
1734 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001735 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001736 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001737 } catch (IllegalArgumentException e) {
1738 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1739 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1740 } else {
1741 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1742 }
1743 return null;
1744 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001745 }
1746
1747 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001748 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1749 * changing '+' into ' '
1750 *
1751 * @param toReplace Input string
1752 * @return The string with all "+" characters replaced with "%2B"
1753 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001754 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001755 return toReplace.replace("+", "%2B");
1756 }
1757
1758 /**
Jin Caod67d7e32014-03-26 16:49:48 -07001759 * Replaces all occurrences of '%' with "%25", to prevent URLDecode.decode from
1760 * crashing on decoded '%' symbols
1761 *
1762 * @param toReplace Input string
1763 * @return The string with all "%" characters replaced with "%25"
1764 */
1765 private static String replacePercent(String toReplace) {
1766 return toReplace.replace("%", "%25");
1767 }
1768
1769 /**
1770 * Helper function to encapsulate encoding/decoding string from Uri.getQueryParameters
1771 * @param content Input string
1772 * @return The string that's properly escaped to be shown in mail subject/content
1773 */
1774 private static String decodeContentFromQueryParam(String content) {
1775 try {
1776 return URLDecoder.decode(replacePlus(replacePercent(content)), UTF8_ENCODING_NAME);
1777 } catch (UnsupportedEncodingException e) {
1778 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), content);
1779 return ""; // Default to empty string so setText/setBody has same behavior as before.
1780 }
1781 }
1782
1783 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001784 * Initialize the compose view from a String representing a mailTo uri.
1785 * @param mailToString The uri as a string.
1786 */
1787 public void initFromMailTo(String mailToString) {
1788 // We need to disguise this string as a URI in order to parse it
1789 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1790 Uri uri = Uri.parse("foo://" + mailToString);
1791 int index = mailToString.indexOf("?");
1792 int length = "mailto".length() + 1;
1793 String to;
1794 try {
1795 // Extract the recipient after mailto:
1796 if (index == -1) {
1797 to = decodeEmailInUri(mailToString.substring(length));
1798 } else {
1799 to = decodeEmailInUri(mailToString.substring(length, index));
1800 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001801 if (!TextUtils.isEmpty(to)) {
1802 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1803 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001804 } catch (UnsupportedEncodingException e) {
1805 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1806 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1807 } else {
1808 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1809 }
1810 }
1811
1812 List<String> cc = uri.getQueryParameters("cc");
1813 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1814
1815 List<String> otherTo = uri.getQueryParameters("to");
1816 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1817
1818 List<String> bcc = uri.getQueryParameters("bcc");
1819 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1820
Jin Caod67d7e32014-03-26 16:49:48 -07001821 // NOTE: Uri.getQueryParameters already decodes % encoded characters
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001822 List<String> subject = uri.getQueryParameters("subject");
1823 if (subject.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001824 mSubject.setText(decodeContentFromQueryParam(subject.get(0)));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001825 }
1826
1827 List<String> body = uri.getQueryParameters("body");
1828 if (body.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001829 setBody(decodeContentFromQueryParam(body.get(0)), true /* with signature */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001830 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001831 }
1832
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001833 @VisibleForTesting
1834 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001835 addAttachments(refMessage.getAttachments());
1836 }
1837
1838 public long addAttachments(List<Attachment> attachments) {
1839 long size = 0;
1840 AttachmentFailureException error = null;
1841 for (Attachment a : attachments) {
1842 try {
1843 size += mAttachmentsView.addAttachment(mAccount, a);
1844 } catch (AttachmentFailureException e) {
1845 error = e;
1846 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001847 }
Mark Wei434f2942012-08-24 11:54:02 -07001848 if (error != null) {
1849 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1850 if (attachments.size() > 1) {
1851 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1852 } else {
1853 showAttachmentTooBigToast(error.getErrorRes());
1854 }
1855 }
1856 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001857 }
1858
1859 /**
1860 * When an attachment is too large to be added to a message, show a toast.
1861 * This method also updates the position of the toast so that it is shown
1862 * clearly above they keyboard if it happens to be open.
1863 */
Mark Wei434f2942012-08-24 11:54:02 -07001864 private void showAttachmentTooBigToast(int errorRes) {
1865 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1866 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1867 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001868 }
1869
Mark Wei434f2942012-08-24 11:54:02 -07001870 private void showErrorToast(String message) {
1871 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1872 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001873 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1874 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1875 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001876 }
1877
Paul Westbrookf97588b2012-03-20 11:11:37 -07001878 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001879 Bundle extras = intent.getExtras();
1880 if (extras == null) {
1881 extras = Bundle.EMPTY;
1882 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001883 final String action = intent.getAction();
1884 if (!mAttachmentsChanged) {
1885 long totalSize = 0;
1886 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
Jin Caoadea2c82014-08-28 12:25:52 -07001887 final String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1888 final ArrayList<Uri> parsedUris = Lists.newArrayListWithCapacity(uris.length);
1889 for (String uri : uris) {
1890 parsedUris.add(Uri.parse(uri));
Paul Westbrookf97588b2012-03-20 11:11:37 -07001891 }
Jin Caoadea2c82014-08-28 12:25:52 -07001892 totalSize += handleAttachmentUrisFromIntent(parsedUris);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001893 }
mindyp9a9e8d62012-10-03 12:24:07 -07001894 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1895 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
Andy Huang91ede362014-01-21 19:16:00 -08001896 final ArrayList<Uri> uris = extras
mindyp9a9e8d62012-10-03 12:24:07 -07001897 .getParcelableArrayList(Intent.EXTRA_STREAM);
Jin Caoadea2c82014-08-28 12:25:52 -07001898 totalSize += handleAttachmentUrisFromIntent(uris);
mindyp9a9e8d62012-10-03 12:24:07 -07001899 } else {
Tony Mantler581edd42014-02-18 15:41:22 -08001900 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
Jin Caoadea2c82014-08-28 12:25:52 -07001901 final ArrayList<Uri> uris = Lists.newArrayList(uri);
1902 totalSize += handleAttachmentUrisFromIntent(uris);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001903 }
1904 }
1905
1906 if (totalSize > 0) {
1907 mAttachmentsChanged = true;
1908 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001909
1910 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1911 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001912 }
1913 }
1914 }
1915
Jin Caoadea2c82014-08-28 12:25:52 -07001916 /**
Sam Lee29b63fa2016-03-23 16:53:20 -07001917 * @return the authority of EmailProvider for this app. should be overridden in concrete
1918 * app implementations. can't be known here because this project doesn't know about that sort
1919 * of thing.
1920 */
1921 protected String getEmailProviderAuthority() {
1922 throw new UnsupportedOperationException("unimplemented, EmailProvider unknown");
1923 }
1924
1925 /**
Jin Caoadea2c82014-08-28 12:25:52 -07001926 * Helper function to handle a list of uris to attach.
1927 * @return the total size of all successfully attached files.
1928 */
1929 private long handleAttachmentUrisFromIntent(List<Uri> uris) {
1930 ArrayList<Attachment> attachments = Lists.newArrayList();
1931 for (Uri uri : uris) {
1932 try {
1933 if (uri != null) {
Sam Lee29b63fa2016-03-23 16:53:20 -07001934 if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
Régis Décampsae2d14d2016-02-23 16:16:54 +01001935 // We must not allow files from /data, even from our process.
Jin Caoadea2c82014-08-28 12:25:52 -07001936 final File f = new File(uri.getPath());
Jin Cao24ed2942014-09-02 10:21:37 -07001937 final String filePath = f.getCanonicalPath();
1938 if (filePath.startsWith(DATA_DIRECTORY_ROOT)) {
Régis Décampsae2d14d2016-02-23 16:16:54 +01001939 showErrorToast(getString(R.string.attachment_permission_denied));
1940 Analytics.getInstance().sendEvent(ANALYTICS_CATEGORY_ERRORS,
1941 "send_intent_attachment", "data_dir", 0);
1942 continue;
Jin Caoadea2c82014-08-28 12:25:52 -07001943 }
Sam Lee29b63fa2016-03-23 16:53:20 -07001944 } else if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
1945 // disallow attachments from our own EmailProvider (b/27308057)
1946 if (getEmailProviderAuthority().equals(uri.getAuthority())) {
1947 showErrorToast(getString(R.string.attachment_permission_denied));
1948 Analytics.getInstance().sendEvent(ANALYTICS_CATEGORY_ERRORS,
1949 "send_intent_attachment", "email_provider", 0);
1950 continue;
1951 }
Jin Caoadea2c82014-08-28 12:25:52 -07001952 }
Sam Lee29b63fa2016-03-23 16:53:20 -07001953
Jin Caoadea2c82014-08-28 12:25:52 -07001954 if (!handleSpecialAttachmentUri(uri)) {
1955 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1956 attachments.add(a);
1957
1958 Analytics.getInstance().sendEvent("send_intent_attachment",
1959 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1960 }
1961 }
1962 } catch (AttachmentFailureException e) {
1963 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1964 showAttachmentTooBigToast(e.getErrorRes());
1965 } catch (IOException | SecurityException e) {
Jin Cao24ed2942014-09-02 10:21:37 -07001966 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Jin Caoadea2c82014-08-28 12:25:52 -07001967 showErrorToast(getString(R.string.attachment_permission_denied));
1968 }
1969 }
1970 return addAttachments(attachments);
1971 }
1972
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001973 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001974 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1975 mShowQuotedText = true;
1976 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001977
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001978 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1979 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001980 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1981 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001982 }
1983
1984 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001985 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001986 boolean ccVisible = mCcBccView.isCcVisible();
1987 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001988 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001989 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001990 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001991 } else {
Jin Cao9d358a12014-07-24 12:15:38 -07001992 mCcBccButton.setVisibility(View.GONE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001993 }
1994 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001995 }
1996
Mindy Pereira013194c2012-01-06 15:09:33 -08001997 /**
1998 * Add attachment and update the compose area appropriately.
Mindy Pereira013194c2012-01-06 15:09:33 -08001999 */
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08002000 private void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07002001 if (data == null) {
2002 return;
2003 }
2004
2005 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
2006 final ClipData clipData = data.getClipData();
2007 if (clipData != null) {
2008 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
2009 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
2010 }
2011 return;
2012 }
2013 }
2014
2015 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07002016 }
2017
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08002018 private void addAttachmentAndUpdateView(Uri contentUri) {
Andy Huang5c5fd572012-04-08 18:19:29 -07002019 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07002020 return;
2021 }
Andy Huang91ede362014-01-21 19:16:00 -08002022
Ekin Oguzfa0d4012016-11-28 15:34:02 -08002023 if (handleSpecialAttachmentUri(contentUri)) {
2024 return;
2025 }
Andy Huang91ede362014-01-21 19:16:00 -08002026
Ekin Oguzfa0d4012016-11-28 15:34:02 -08002027 final long size = handleAttachmentUrisFromIntent(Arrays.asList(contentUri));
2028 if (size > 0) {
2029 mAttachmentsChanged = true;
2030 updateSaveUi();
Andy Huang5c5fd572012-04-08 18:19:29 -07002031 }
2032 }
2033
Andy Huang91ede362014-01-21 19:16:00 -08002034 /**
2035 * Allow subclasses to implement custom handling of attachments.
2036 *
2037 * @param contentUri a passed-in URI from a pick intent
2038 * @return true iff handled
2039 */
2040 protected boolean handleSpecialAttachmentUri(final Uri contentUri) {
2041 return false;
2042 }
2043
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08002044 private void addAttachmentAndUpdateView(Attachment attachment) {
Andy Huang5c5fd572012-04-08 18:19:29 -07002045 try {
Mark Wei434f2942012-08-24 11:54:02 -07002046 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08002047 if (size > 0) {
2048 mAttachmentsChanged = true;
2049 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08002050 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08002051 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08002052 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07002053 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08002054 }
2055 }
2056
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08002057 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002058 // Don't populate the address if this is a forward.
2059 if (action == ComposeActivity.FORWARD) {
2060 return;
2061 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002062 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002063 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002064
Paul Westbrook6d2442b2013-07-17 17:51:51 -07002065 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
2066 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08002067 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07002068 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07002069 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002070 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07002071 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
2072 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
Andy Huange2af8872014-01-16 12:36:27 -08002073 final String[] replyToAddresses = getReplyToAddresses(
2074 refMessage.getReplyToAddressesUnescaped(), fromAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002075
2076 // If this is a reply, the Cc list is empty. If this is a reply-all, the
2077 // Cc list is the union of the To and Cc recipients of the original
2078 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002079 // already on the To list.
2080 if (action == ComposeActivity.REPLY) {
Tony Mantler24f116f2014-01-16 10:20:50 -08002081 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002082 addToAddresses(toAddresses);
2083 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002084 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002085 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08002086 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07002087 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07002088 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002089 addCcAddresses(ccAddresses, toAddresses);
2090 }
2091 }
2092
Andy Huange2af8872014-01-16 12:36:27 -08002093 // If there is no reply to address, the reply to address is the sender.
2094 private static String[] getReplyToAddresses(String[] replyTo, String from) {
2095 boolean hasReplyTo = false;
2096 for (final String replyToAddress : replyTo) {
2097 if (!TextUtils.isEmpty(replyToAddress)) {
2098 hasReplyTo = true;
2099 }
2100 }
2101 return hasReplyTo ? replyTo : new String[] {from};
2102 }
2103
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002104 private void addToAddresses(Collection<String> addresses) {
2105 addAddressesToList(addresses, mTo);
2106 }
2107
2108 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002109 addCcAddressesToList(tokenizeAddressList(addresses),
2110 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002111 }
2112
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07002113 private void addBccAddresses(Collection<String> addresses) {
2114 addAddressesToList(addresses, mBcc);
2115 }
2116
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002117 @VisibleForTesting
2118 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
2119 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
2120 String address;
2121
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002122 if (compareToList == null) {
Tony Mantler581edd42014-02-18 15:41:22 -08002123 for (final Rfc822Token[] tokens : addresses) {
2124 for (final Rfc822Token token : tokens) {
2125 address = token.toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002126 list.append(address + END_TOKEN);
2127 }
2128 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002129 } else {
2130 HashSet<String> compareTo = convertToHashSet(compareToList);
Tony Mantler581edd42014-02-18 15:41:22 -08002131 for (final Rfc822Token[] tokens : addresses) {
2132 for (final Rfc822Token token : tokens) {
2133 address = token.toString();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002134 // Check if this is a duplicate:
Tony Mantler581edd42014-02-18 15:41:22 -08002135 if (!compareTo.contains(token.getAddress())) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002136 // Get the address here
2137 list.append(address + END_TOKEN);
2138 }
2139 }
2140 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002141 }
2142 }
2143
Scott Kennedyff8553f2013-04-05 20:57:44 -07002144 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
2145 final HashSet<String> hash = new HashSet<String>();
2146 for (final Rfc822Token[] tokens : list) {
Tony Mantler581edd42014-02-18 15:41:22 -08002147 for (final Rfc822Token token : tokens) {
2148 hash.add(token.getAddress());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002149 }
2150 }
2151 return hash;
2152 }
2153
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002154 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
2155 @VisibleForTesting
2156 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
2157
2158 for (String address: addresses) {
2159 tokenized.add(Rfc822Tokenizer.tokenize(address));
2160 }
2161 return tokenized;
2162 }
2163
2164 @VisibleForTesting
2165 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
2166 for (String address : addresses) {
2167 addAddressToList(address, list);
2168 }
2169 }
2170
Scott Kennedyff8553f2013-04-05 20:57:44 -07002171 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002172 if (address == null || list == null)
2173 return;
2174
Scott Kennedyff8553f2013-04-05 20:57:44 -07002175 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002176
Tony Mantler581edd42014-02-18 15:41:22 -08002177 for (final Rfc822Token token : tokens) {
2178 list.append(token + END_TOKEN);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002179 }
2180 }
2181
2182 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07002183 protected Collection<String> initToRecipients(final String fullSenderAddress,
Tony Mantler24f116f2014-01-16 10:20:50 -08002184 final String[] replyToAddresses, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002185 // The To recipient is the reply-to address specified in the original
2186 // message, unless it is:
2187 // the current user OR a custom from of the current user, in which case
2188 // it's the To recipient list of the original message.
2189 // OR missing, in which case use the sender of the original message
2190 Set<String> toAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002191 for (final String replyToAddress : replyToAddresses) {
2192 if (!TextUtils.isEmpty(replyToAddress)
2193 && !recipientMatchesThisAccount(replyToAddress)) {
2194 toAddresses.add(replyToAddress);
2195 }
2196 }
2197 if (toAddresses.size() == 0) {
mindyp65b06f52012-11-21 10:35:08 -08002198 // In this case, the user is replying to a message in which their
Tony Mantler24f116f2014-01-16 10:20:50 -08002199 // current account or some of their custom from addresses are the only
2200 // recipients and they sent the original message.
mindyp65b06f52012-11-21 10:35:08 -08002201 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
2202 && recipientMatchesThisAccount(inToAddresses[0])) {
2203 toAddresses.add(inToAddresses[0]);
2204 return toAddresses;
2205 }
2206 // This happens if the user replies to a message they originally
2207 // wrote. In this case, "reply" really means "re-send," so we
2208 // target the original recipients. This works as expected even
2209 // if the user sent the original message to themselves.
2210 for (String address : inToAddresses) {
2211 if (!recipientMatchesThisAccount(address)) {
2212 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08002213 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07002214 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002215 }
2216 return toAddresses;
2217 }
2218
Scott Kennedyff8553f2013-04-05 20:57:44 -07002219 private void addRecipients(final Set<String> recipients, final String[] addresses) {
2220 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002221 // Do not add this account, or any of its custom from addresses, to
2222 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08002223 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08002224 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002225 recipients.add(email.replace("\"\"", ""));
2226 }
2227 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002228 }
2229
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002230 /**
2231 * A recipient matches this account if it has the same address as the
2232 * currently selected account OR one of the custom from addresses associated
2233 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002234 * @param recipientAddress address we are comparing with the currently selected account
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002235 */
mindyp5ee5d692012-11-19 16:02:16 -08002236 protected boolean recipientMatchesThisAccount(String recipientAddress) {
2237 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08002238 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002239 }
2240
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002241 /**
2242 * Returns a formatted subject string with the appropriate prefix for the action type.
2243 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
2244 */
Andrew Sapperstein7e04f142014-06-11 13:43:07 -07002245 public static String buildFormattedSubject(Resources res, String subject, int action) {
Tony Mantler41c3a252014-06-30 11:00:43 -07002246 final String prefix;
2247 final String correctedSubject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002248 if (action == ComposeActivity.COMPOSE) {
2249 prefix = "";
2250 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002251 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002252 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002253 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002254 }
2255
Tony Mantler41c3a252014-06-30 11:00:43 -07002256 if (TextUtils.isEmpty(subject)) {
2257 correctedSubject = prefix;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002258 } else {
Tony Mantler41c3a252014-06-30 11:00:43 -07002259 // Don't duplicate the prefix
2260 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
2261 correctedSubject = subject;
2262 } else {
2263 correctedSubject = String.format(
2264 res.getString(R.string.formatted_subject), prefix, subject);
2265 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002266 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002267
2268 return correctedSubject;
2269 }
2270
2271 private void setSubject(Message refMessage, int action) {
2272 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002273 }
2274
Mindy Pereira818143e2012-01-11 13:59:49 -08002275 private void initRecipients() {
2276 setupRecipients(mTo);
2277 setupRecipients(mCc);
2278 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002279 }
2280
Mindy Pereira818143e2012-01-11 13:59:49 -08002281 private void setupRecipients(RecipientEditTextView view) {
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002282 final DropdownChipLayouter layouter = getDropdownChipLayouter();
2283 if (layouter != null) {
2284 view.setDropdownChipLayouter(layouter);
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002285 }
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002286 view.setAdapter(getRecipientAdapter());
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002287 view.setRecipientEntryItemClickedListener(this);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002288 if (mValidator == null) {
Tony Mantler79b11562013-10-09 15:31:50 -07002289 final String accountName = mAccount.getEmailAddress();
Mindy Pereira33fe9082012-01-09 16:24:30 -08002290 int offset = accountName.indexOf("@") + 1;
2291 String account = accountName;
Tony Mantler79b11562013-10-09 15:31:50 -07002292 if (offset > 0) {
2293 account = account.substring(offset);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002294 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002295 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002296 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002297 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002298 }
2299
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002300 /**
2301 * Derived classes should override if they wish to provide their own autocomplete behavior.
2302 */
2303 public BaseRecipientAdapter getRecipientAdapter() {
2304 return new RecipientAdapter(this, mAccount);
2305 }
2306
2307 /**
2308 * Derived classes should override this to provide their own dropdown behavior.
2309 * If the result is null, the default {@link com.android.ex.chips.DropdownChipLayouter}
2310 * is used.
2311 */
2312 public DropdownChipLayouter getDropdownChipLayouter() {
2313 return null;
2314 }
2315
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002316 @Override
2317 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002318 final int id = v.getId();
2319 if (id == R.id.add_cc_bcc) {
2320 // Verify that cc/ bcc aren't showing.
2321 // Animate in cc/bcc.
2322 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002323 }
2324 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002325
2326 @Override
Jin Caoc5c550a2014-07-29 11:53:17 -07002327 public void onFocusChange (View v, boolean hasFocus) {
2328 final int id = v.getId();
2329 if (hasFocus && (id == R.id.subject || id == R.id.body)) {
2330 // Collapse cc/bcc iff both are empty
2331 final boolean showCcBccFields = !TextUtils.isEmpty(mCc.getText()) ||
2332 !TextUtils.isEmpty(mBcc.getText());
2333 mCcBccView.show(false /* animate */, showCcBccFields, showCcBccFields);
Jin Cao36e23872014-07-29 13:41:12 -07002334 mCcBccButton.setVisibility(showCcBccFields ? View.GONE : View.VISIBLE);
2335
2336 // On phones autoscroll down so that Cc aligns to the top if we are showing cc/bcc.
2337 if (getResources().getBoolean(R.bool.auto_scroll_cc) && showCcBccFields) {
2338 final int[] coords = new int[2];
2339 mCc.getLocationOnScreen(coords);
2340
2341 // Subtract status bar and action bar height from y-coord.
Jin Caocfba4bb2014-10-07 19:29:15 -07002342 getWindow().getDecorView().getWindowVisibleDisplayFrame(mRect);
2343 final int deltaY = coords[1] - getSupportActionBar().getHeight() - mRect.top;
Jin Cao36e23872014-07-29 13:41:12 -07002344
2345 // Only scroll down
2346 if (deltaY > 0) {
2347 mScrollView.smoothScrollBy(0, deltaY);
2348 }
2349 }
Jin Caoc5c550a2014-07-29 11:53:17 -07002350 }
2351 }
2352
2353 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002354 public boolean onCreateOptionsMenu(Menu menu) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002355 final boolean superCreated = super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07002356 // Don't render any menu items when there are no accounts.
2357 if (mAccounts == null || mAccounts.length == 0) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002358 return superCreated;
Mindy Pereirab199d172012-08-13 11:04:03 -07002359 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002360 MenuInflater inflater = getMenuInflater();
2361 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08002362
2363 /*
2364 * Start save in the correct enabled state.
2365 * 1) If a user launches compose from within gmail, save is disabled
2366 * until they add something, at which point, save is enabled, auto save
2367 * on exit; if the user empties everything, save is disabled, exiting does not
2368 * auto-save
2369 * 2) if a user replies/ reply all/ forwards from within gmail, save is
2370 * disabled until they change something, at which point, save is
2371 * enabled, auto save on exit; if the user empties everything, save is
2372 * disabled, exiting does not auto-save.
2373 * 3) If a user launches compose from another application and something
2374 * gets populated (attachments, recipients, body, subject, etc), save is
2375 * enabled, auto save on exit; if the user empties everything, save is
2376 * disabled, exiting does not auto-save
2377 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08002378 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08002379 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07002380 enableSave(mInnerSavedState != null ?
2381 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08002382 : (Intent.ACTION_SEND.equals(action)
2383 || Intent.ACTION_SEND_MULTIPLE.equals(action)
2384 || Intent.ACTION_SENDTO.equals(action)
Jin Caoe0037922014-09-16 10:23:50 -07002385 || isDraftDirty()));
mindyp1d7e9142012-11-21 13:54:30 -08002386
Greg Bullockd47a7042014-08-13 16:02:59 +02002387 final MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2388 final MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2389 final MenuItem attachFromServiceItem = menu.findItem(R.id.attach_from_service_stub1);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07002390 if (helpItem != null) {
2391 helpItem.setVisible(mAccount != null
2392 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2393 }
2394 if (sendFeedbackItem != null) {
2395 sendFeedbackItem.setVisible(mAccount != null
2396 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2397 }
Greg Bullockd47a7042014-08-13 16:02:59 +02002398 if (attachFromServiceItem != null) {
2399 attachFromServiceItem.setVisible(shouldEnableAttachFromServiceMenu(mAccount));
2400 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002401
Andrew Sapperstein8809f9f2013-10-11 16:13:35 -07002402 // Show attach picture on pre-K devices.
2403 menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002404
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002405 return true;
2406 }
2407
2408 @Override
2409 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002410 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002411
Andy Huangf8c59b02014-03-19 20:00:53 -07002412 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id,
2413 "compose", 0);
Andy Huangdc97bf42013-08-15 16:52:45 -07002414
Mindy Pereira75f66632012-01-11 11:42:02 -08002415 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002416 if (id == R.id.add_file_attachment) {
2417 doAttach(MIME_TYPE_ALL);
2418 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002419 doAttach(MIME_TYPE_PHOTO);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002420 } else if (id == R.id.save) {
2421 doSave(true);
2422 } else if (id == R.id.send) {
2423 doSend();
2424 } else if (id == R.id.discard) {
2425 doDiscard();
2426 } else if (id == R.id.settings) {
2427 Utils.showSettings(this, mAccount);
2428 } else if (id == android.R.id.home) {
2429 onAppUpPressed();
2430 } else if (id == R.id.help_info_menu_item) {
2431 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002432 } else {
2433 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002434 }
Tony Mantler581edd42014-02-18 15:41:22 -08002435 return handled || super.onOptionsItemSelected(item);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002436 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002437
Mindy Pereirab199d172012-08-13 11:04:03 -07002438 @Override
2439 public void onBackPressed() {
2440 // If we are showing the wait fragment, just exit.
2441 if (getWaitFragment() != null) {
2442 finish();
2443 } else {
2444 super.onBackPressed();
2445 }
2446 }
2447
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002448 /**
2449 * Carries out the "up" action in the action bar.
2450 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002451 private void onAppUpPressed() {
2452 if (mLaunchedFromEmail) {
2453 // If this was started from Gmail, simply treat app up as the system back button, so
2454 // that the last view is restored.
2455 onBackPressed();
2456 return;
2457 }
2458
2459 // Fire the main activity to ensure it launches the "top" screen of mail.
2460 // Since the main Activity is singleTask, it should revive that task if it was already
2461 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002462 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002463 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2464 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2465 startActivity(mailIntent);
2466 finish();
2467 }
2468
Mindy Pereira33fe9082012-01-09 16:24:30 -08002469 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002470 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002471 logSendOrSave(false /* save */);
2472 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002473 }
2474
Mindy Pereira48e31b02012-05-30 13:12:24 -07002475 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002476 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002477 }
2478
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002479 @Override
2480 public void onRecipientEntryItemClicked(int charactersTyped, int position) {
2481 // Send analytics of characters typed and position in dropdown selected.
2482 Analytics.getInstance().sendEvent(
Andrew Sapperstein9afa8222014-06-23 16:19:23 -07002483 "suggest_click", Integer.toString(charactersTyped), Integer.toString(position), 0);
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002484 }
2485
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002486 @VisibleForTesting
2487 public interface SendOrSaveCallback {
Jin Caoaff451f2014-09-09 14:32:04 -07002488 void initializeSendOrSave();
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002489 void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
Jin Cao31bb3d62014-09-11 14:01:43 -07002490 long getMessageId();
Jin Caoaff451f2014-09-09 14:32:04 -07002491 void sendOrSaveFinished(SendOrSaveMessage message, boolean success);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002492 }
2493
Jin Caoaff451f2014-09-09 14:32:04 -07002494 private void runSendOrSaveProviderCalls(SendOrSaveMessage sendOrSaveMessage,
Jin Cao31bb3d62014-09-11 14:01:43 -07002495 SendOrSaveCallback callback, ReplyFromAccount currReplyFromAccount,
2496 ReplyFromAccount originalReplyFromAccount) {
2497 long messageId = callback.getMessageId();
Jin Caoaff451f2014-09-09 14:32:04 -07002498 // If a previous draft has been saved, in an account that is different
2499 // than what the user wants to send from, remove the old draft, and treat this
2500 // as a new message
Jin Cao31bb3d62014-09-11 14:01:43 -07002501 if (originalReplyFromAccount != null
2502 && !currReplyFromAccount.account.uri.equals(originalReplyFromAccount.account.uri)) {
Jin Caoaff451f2014-09-09 14:32:04 -07002503 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2504 ContentResolver resolver = getContentResolver();
2505 ContentValues values = new ContentValues();
2506 values.put(BaseColumns._ID, messageId);
Jin Cao31bb3d62014-09-11 14:01:43 -07002507 if (originalReplyFromAccount.account.expungeMessageUri != null) {
Jin Caoaff451f2014-09-09 14:32:04 -07002508 new ContentProviderTask.UpdateTask()
Jin Cao31bb3d62014-09-11 14:01:43 -07002509 .run(resolver, originalReplyFromAccount.account.expungeMessageUri,
Jin Caoaff451f2014-09-09 14:32:04 -07002510 values, null, null);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002511 } else {
Jin Caoaff451f2014-09-09 14:32:04 -07002512 // TODO(mindyp) delete the conversation.
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002513 }
Jin Caoaff451f2014-09-09 14:32:04 -07002514 // reset messageId to 0, so a new message will be created
2515 messageId = UIProvider.INVALID_MESSAGE_ID;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002516 }
2517 }
2518
Jin Caoaff451f2014-09-09 14:32:04 -07002519 final long messageIdToSave = messageId;
Jin Cao31bb3d62014-09-11 14:01:43 -07002520 sendOrSaveMessage(callback, messageIdToSave, sendOrSaveMessage, currReplyFromAccount);
Jin Caoaff451f2014-09-09 14:32:04 -07002521
2522 if (!sendOrSaveMessage.mSave) {
2523 incrementRecipientsTimesContacted(
2524 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO),
2525 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC),
2526 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2527 }
2528 callback.sendOrSaveFinished(sendOrSaveMessage, true);
2529 }
2530
2531 private void incrementRecipientsTimesContacted(
2532 final String toAddresses, final String ccAddresses, final String bccAddresses) {
2533 final List<String> recipients = Lists.newArrayList();
2534 addAddressesToRecipientList(recipients, toAddresses);
2535 addAddressesToRecipientList(recipients, ccAddresses);
2536 addAddressesToRecipientList(recipients, bccAddresses);
2537 incrementRecipientsTimesContacted(recipients);
2538 }
2539
2540 private void addAddressesToRecipientList(
2541 final List<String> recipients, final String addressString) {
2542 if (recipients == null) {
2543 throw new IllegalArgumentException("recipientList cannot be null");
2544 }
2545 if (TextUtils.isEmpty(addressString)) {
2546 return;
2547 }
2548 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2549 for (final Rfc822Token token : tokens) {
2550 recipients.add(token.getAddress());
2551 }
2552 }
2553
2554 /**
2555 * Send or Save a message.
2556 */
2557 private void sendOrSaveMessage(SendOrSaveCallback callback, final long messageIdToSave,
2558 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
2559 final ContentResolver resolver = getContentResolver();
2560 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2561
2562 final String accountMethod = sendOrSaveMessage.mSave ?
2563 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2564 UIProvider.AccountCallMethods.SEND_MESSAGE;
2565
2566 try {
2567 if (updateExistingMessage) {
2568 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
2569
2570 callAccountSendSaveMethod(resolver,
2571 selectedAccount.account, accountMethod, sendOrSaveMessage);
2572 } else {
2573 Uri messageUri = null;
2574 final Bundle result = callAccountSendSaveMethod(resolver,
2575 selectedAccount.account, accountMethod, sendOrSaveMessage);
2576 if (result != null) {
2577 // If a non-null value was returned, then the provider handled the call
2578 // method
2579 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
2580 }
2581 if (sendOrSaveMessage.mSave && messageUri != null) {
2582 final Cursor messageCursor = resolver.query(messageUri,
2583 UIProvider.MESSAGE_PROJECTION, null, null, null);
2584 if (messageCursor != null) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002585 try {
Jin Caoaff451f2014-09-09 14:32:04 -07002586 if (messageCursor.moveToFirst()) {
2587 // Broadcast notification that a new message has
2588 // been allocated
2589 callback.notifyMessageIdAllocated(sendOrSaveMessage,
2590 new Message(messageCursor));
2591 }
2592 } finally {
2593 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002594 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002595 }
2596 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002597 }
Jin Caoaff451f2014-09-09 14:32:04 -07002598 } finally {
2599 // Close any opened file descriptors
2600 closeOpenedAttachmentFds(sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002601 }
Jin Caoaff451f2014-09-09 14:32:04 -07002602 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002603
Jin Caoaff451f2014-09-09 14:32:04 -07002604 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
2605 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2606 if (openedFds != null) {
2607 final Set<String> keys = openedFds.keySet();
2608 for (final String key : keys) {
2609 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2610 if (fd != null) {
2611 try {
2612 fd.close();
2613 } catch (IOException e) {
2614 // Do nothing
2615 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002616 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002617 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002618 }
2619 }
2620
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002621 /**
Jin Caoaff451f2014-09-09 14:32:04 -07002622 * Use the {@link ContentResolver#call} method to send or save the message.
2623 *
2624 * If this was successful, this method will return an non-null Bundle instance
2625 */
2626 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2627 final Account account, final String method,
2628 final SendOrSaveMessage sendOrSaveMessage) {
2629 // Copy all of the values from the content values to the bundle
2630 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2631 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2632
2633 for (Entry<String, Object> entry : valueSet) {
2634 final Object entryValue = entry.getValue();
2635 final String key = entry.getKey();
2636 if (entryValue instanceof String) {
2637 methodExtras.putString(key, (String)entryValue);
2638 } else if (entryValue instanceof Boolean) {
2639 methodExtras.putBoolean(key, (Boolean)entryValue);
2640 } else if (entryValue instanceof Integer) {
2641 methodExtras.putInt(key, (Integer)entryValue);
2642 } else if (entryValue instanceof Long) {
2643 methodExtras.putLong(key, (Long)entryValue);
2644 } else {
2645 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2646 entryValue.getClass().getName());
2647 }
2648 }
2649
2650 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2651 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2652 if (fdMap != null) {
2653 methodExtras.putParcelable(
2654 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2655 }
2656
2657 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
2658 }
2659
2660 /**
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002661 * Reports recipients that have been contacted in order to improve auto-complete
2662 * suggestions. Default behavior updates usage statistics in ContactsProvider.
2663 * @param recipients addresses
2664 */
2665 protected void incrementRecipientsTimesContacted(List<String> recipients) {
2666 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(this);
2667 statsUpdater.updateWithAddress(recipients);
2668 }
2669
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002670 @VisibleForTesting
2671 public static class SendOrSaveMessage {
Jin Cao31bb3d62014-09-11 14:01:43 -07002672 final int mRequestId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002673 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002674 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002675 @VisibleForTesting
2676 public final boolean mSave;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002677 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002678
Jin Cao31bb3d62014-09-11 14:01:43 -07002679 public SendOrSaveMessage(Context context, int requestId, ContentValues values,
Jin Caoadae7a32014-09-09 18:16:25 -07002680 String refMessageId, List<Attachment> attachments, Bundle optionalAttachmentFds,
2681 boolean save) {
Jin Cao31bb3d62014-09-11 14:01:43 -07002682 mRequestId = requestId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002683 mValues = values;
2684 mRefMessageId = refMessageId;
2685 mSave = save;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002686
Jin Caoadae7a32014-09-09 18:16:25 -07002687 // If the attachments are already open for us (pre-JB), then don't open them again
2688 if (optionalAttachmentFds != null) {
2689 mAttachmentFds = optionalAttachmentFds;
2690 } else {
2691 mAttachmentFds = initializeAttachmentFds(context, attachments);
2692 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002693 }
2694
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002695 Bundle attachmentFds() {
2696 return mAttachmentFds;
2697 }
Jin Caoadae7a32014-09-09 18:16:25 -07002698 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002699
Jin Caoadae7a32014-09-09 18:16:25 -07002700 /**
2701 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2702 * called before the ComposeActivity finishes.
2703 * Note: The caller is responsible for closing these file descriptors.
2704 */
2705 private static Bundle initializeAttachmentFds(final Context context,
2706 final List<Attachment> attachments) {
2707 if (attachments == null || attachments.size() == 0) {
2708 return null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002709 }
Jin Caoadae7a32014-09-09 18:16:25 -07002710
2711 final Bundle result = new Bundle(attachments.size());
2712 final ContentResolver resolver = context.getContentResolver();
2713
2714 for (Attachment attachment : attachments) {
2715 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2716 continue;
2717 }
2718
2719 ParcelFileDescriptor fileDescriptor;
2720 try {
2721 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2722 } catch (FileNotFoundException e) {
2723 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2724 fileDescriptor = null;
2725 } catch (SecurityException e) {
2726 // We have encountered a security exception when attempting to open the file
2727 // specified by the content uri. If the attachment has been cached, this
2728 // isn't a problem, as even through the original permission may have been
2729 // revoked, we have cached the file. This will happen when saving/sending
2730 // a previously saved draft.
2731 // TODO(markwei): Expose whether the attachment has been cached through the
2732 // attachment object. This would allow us to limit when the log is made, as
2733 // if the attachment has been cached, this really isn't an error
2734 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2735 // Just set the file descriptor to null, as the underlying provider needs
2736 // to handle the file descriptor not being set.
2737 fileDescriptor = null;
2738 }
2739
2740 if (fileDescriptor != null) {
2741 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2742 }
2743 }
2744
2745 return result;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002746 }
2747
2748 /**
2749 * Get the to recipients.
2750 */
2751 public String[] getToAddresses() {
2752 return getAddressesFromList(mTo);
2753 }
2754
2755 /**
2756 * Get the cc recipients.
2757 */
2758 public String[] getCcAddresses() {
2759 return getAddressesFromList(mCc);
2760 }
2761
2762 /**
2763 * Get the bcc recipients.
2764 */
2765 public String[] getBccAddresses() {
2766 return getAddressesFromList(mBcc);
2767 }
2768
2769 public String[] getAddressesFromList(RecipientEditTextView list) {
2770 if (list == null) {
2771 return new String[0];
2772 }
2773 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2774 int count = tokens.length;
2775 String[] result = new String[count];
2776 for (int i = 0; i < count; i++) {
2777 result[i] = tokens[i].toString();
2778 }
2779 return result;
2780 }
2781
2782 /**
2783 * Check for invalid email addresses.
2784 * @param to String array of email addresses to check.
2785 * @param wrongEmailsOut Emails addresses that were invalid.
2786 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002787 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002788 if (mValidator == null) {
2789 return;
2790 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002791 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002792 if (!mValidator.isValid(email)) {
2793 wrongEmailsOut.add(email);
2794 }
2795 }
2796 }
2797
Tony Mantler2558b502013-07-09 10:53:34 -07002798 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002799 // Public no-args constructor needed for fragment re-instantiation
2800 public RecipientErrorDialogFragment() {}
2801
Tony Mantler2558b502013-07-09 10:53:34 -07002802 public static RecipientErrorDialogFragment newInstance(final String message) {
2803 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2804 final Bundle args = new Bundle(1);
2805 args.putString("message", message);
2806 frag.setArguments(args);
2807 return frag;
2808 }
2809
2810 @Override
2811 public Dialog onCreateDialog(Bundle savedInstanceState) {
2812 final String message = getArguments().getString("message");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002813 return new AlertDialog.Builder(getActivity())
2814 .setMessage(message)
Tony Mantler2558b502013-07-09 10:53:34 -07002815 .setPositiveButton(
2816 R.string.ok, new Dialog.OnClickListener() {
2817 @Override
2818 public void onClick(DialogInterface dialog, int which) {
2819 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2820 }
2821 }).create();
2822 }
2823 }
2824
2825 private void finishRecipientErrorDialog() {
2826 // after the user dismisses the recipient error
2827 // dialog we want to make sure to refocus the
2828 // recipient to field so they can fix the issue
2829 // easily
2830 if (mTo != null) {
2831 mTo.requestFocus();
2832 }
2833 }
2834
Mindy Pereira82cc5662012-01-09 17:29:30 -08002835 /**
2836 * Show an error because the user has entered an invalid recipient.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002837 */
Tony Mantler2558b502013-07-09 10:53:34 -07002838 private void showRecipientErrorDialog(final String message) {
2839 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2840 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002841 }
2842
2843 /**
2844 * Update the state of the UI based on whether or not the current draft
2845 * needs to be saved and the message is not empty.
2846 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002847 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002848 if (mSave != null) {
Jin Caoe0037922014-09-16 10:23:50 -07002849 mSave.setEnabled((isDraftDirty() && !isBlank()));
Mindy Pereira82cc5662012-01-09 17:29:30 -08002850 }
2851 }
2852
2853 /**
Jin Caoe0037922014-09-16 10:23:50 -07002854 * Returns true if the current draft is modified from the version we previously saved.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002855 */
Jin Caoe0037922014-09-16 10:23:50 -07002856 private boolean isDraftDirty() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002857 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002858 // The message should only be saved if:
2859 // It hasn't been sent AND
2860 // Some text has been added to the message OR
2861 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002862 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002863 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002864 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002865 }
2866 }
2867
2868 /**
Greg Bullockd47a7042014-08-13 16:02:59 +02002869 * Returns whether the "Attach from Drive" menu item should be visible.
2870 */
2871 protected boolean shouldEnableAttachFromServiceMenu(Account mAccount) {
2872 return false;
2873 }
2874
2875 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002876 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002877 * @return boolean
2878 */
2879 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002880 // Need to check for null since isBlank() can be called from onPause()
2881 // before findViews() is called
2882 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2883 mAttachmentsView == null) {
2884 LogUtils.w(LOG_TAG, "null views in isBlank check");
2885 return true;
2886 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002887 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002888 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2889 mBodyView.getText().toString()) == 0)
2890 && mTo.length() == 0
2891 && mCc.length() == 0 && mBcc.length() == 0
2892 && mAttachmentsView.getAttachments().size() == 0;
2893 }
2894
2895 @VisibleForTesting
2896 protected int getSignatureStartPosition(String signature, String bodyText) {
2897 int startPos = -1;
2898
2899 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2900 return startPos;
2901 }
2902
2903 int bodyLength = bodyText.length();
2904 int signatureLength = signature.length();
2905 String printableVersion = convertToPrintableSignature(signature);
2906 int printableLength = printableVersion.length();
2907
2908 if (bodyLength >= printableLength
2909 && bodyText.substring(bodyLength - printableLength)
2910 .equals(printableVersion)) {
2911 startPos = bodyLength - printableLength;
2912 } else if (bodyLength >= signatureLength
2913 && bodyText.substring(bodyLength - signatureLength)
2914 .equals(signature)) {
2915 startPos = bodyLength - signatureLength;
2916 }
2917 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002918 }
2919
2920 /**
2921 * Allows any changes made by the user to be ignored. Called when the user
2922 * decides to discard a draft.
2923 */
2924 private void discardChanges() {
2925 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002926 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002927 mReplyFromChanged = false;
2928 }
2929
2930 /**
Tony Mantler581edd42014-02-18 15:41:22 -08002931 * @param save True to save, false to send
2932 * @param showToast True to show a toast once the message is sent/saved
Mindy Pereira181df782012-03-01 13:32:44 -08002933 */
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002934 protected void sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002935 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002936 if (mAccounts == null || mAccount == null) {
2937 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002938 if (autoSend) {
2939 finish();
2940 }
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002941 return;
Mark Wei009b3712012-10-18 18:07:50 -07002942 }
2943
Scott Kennedyff8553f2013-04-05 20:57:44 -07002944 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002945 if (orientationChanged) {
2946 to = cc = bcc = new String[0];
2947 } else {
2948 to = getToAddresses();
2949 cc = getCcAddresses();
2950 bcc = getBccAddresses();
2951 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002952
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002953 final ArrayList<String> recipients = buildEmailAddressList(to);
2954 recipients.addAll(buildEmailAddressList(cc));
2955 recipients.addAll(buildEmailAddressList(bcc));
2956
Mindy Pereira181df782012-03-01 13:32:44 -08002957 // Don't let the user send to nobody (but it's okay to save a message
2958 // with no recipients)
2959 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2960 showRecipientErrorDialog(getString(R.string.recipient_needed));
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002961 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002962 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002963
Mindy Pereira181df782012-03-01 13:32:44 -08002964 List<String> wrongEmails = new ArrayList<String>();
2965 if (!save) {
2966 checkInvalidEmails(to, wrongEmails);
2967 checkInvalidEmails(cc, wrongEmails);
2968 checkInvalidEmails(bcc, wrongEmails);
2969 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002970
Mindy Pereira181df782012-03-01 13:32:44 -08002971 // Don't let the user send an email with invalid recipients
2972 if (wrongEmails.size() > 0) {
2973 String errorText = String.format(getString(R.string.invalid_recipient),
2974 wrongEmails.get(0));
2975 showRecipientErrorDialog(errorText);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002976 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002977 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002978
Mindy Pereira181df782012-03-01 13:32:44 -08002979 if (!save) {
Alan Lau3d519042014-06-05 11:13:06 -07002980 if (autoSend) {
2981 // Skip all further checks during autosend. This flow is used by Android Wear
2982 // and Google Now.
2983 sendOrSave(save, showToast);
2984 return;
2985 }
2986
2987 // Show a warning before sending only if there are no attachments, body, or subject.
Mindy Pereira181df782012-03-01 13:32:44 -08002988 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2989 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002990 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002991
Mindy Pereira181df782012-03-01 13:32:44 -08002992 // A warning about an empty body may not be warranted when
2993 // forwarding mails, since a common use case is to forward
2994 // quoted text and not append any more text.
2995 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002996
Mindy Pereira181df782012-03-01 13:32:44 -08002997 // When we bring up a dialog warning the user about a send,
2998 // assume that they accept sending the message. If they do not,
2999 // the dialog listener is required to enable sending again.
3000 if (warnAboutEmptySubject) {
Tony Mantler581edd42014-02-18 15:41:22 -08003001 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003002 showToast, recipients);
3003 return;
Mindy Pereira181df782012-03-01 13:32:44 -08003004 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003005
Mindy Pereira181df782012-03-01 13:32:44 -08003006 if (warnAboutEmptyBody) {
Tony Mantler581edd42014-02-18 15:41:22 -08003007 showSendConfirmDialog(R.string.confirm_send_message_with_no_body,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003008 showToast, recipients);
3009 return;
Mindy Pereira181df782012-03-01 13:32:44 -08003010 }
3011 }
Alan Lau3d519042014-06-05 11:13:06 -07003012 // Ask for confirmation to send.
3013 if (showSendConfirmation()) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003014 showSendConfirmDialog(R.string.confirm_send_message, showToast, recipients);
3015 return;
Mindy Pereira181df782012-03-01 13:32:44 -08003016 }
3017 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003018
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003019 performAdditionalSendOrSaveSanityChecks(save, showToast, recipients);
Mindy Pereira181df782012-03-01 13:32:44 -08003020 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003021
Mindy Pereira181df782012-03-01 13:32:44 -08003022 /**
3023 * Returns a boolean indicating whether warnings should be shown for empty
3024 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07003025 *
Mindy Pereira181df782012-03-01 13:32:44 -08003026 * @return True if a warning should be shown for empty text fields
3027 */
3028 protected boolean showEmptyTextWarnings() {
3029 return mAttachmentsView.getAttachments().size() == 0;
3030 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003031
Mindy Pereira181df782012-03-01 13:32:44 -08003032 /**
3033 * Returns a boolean indicating whether the user should confirm each send
3034 *
3035 * @return True if a warning should be on each send
3036 */
3037 protected boolean showSendConfirmation() {
Tony Mantler581edd42014-02-18 15:41:22 -08003038 return mCachedSettings != null && mCachedSettings.confirmSend;
Mindy Pereira181df782012-03-01 13:32:44 -08003039 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003040
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003041 public static class SendConfirmDialogFragment extends DialogFragment
3042 implements DialogInterface.OnClickListener {
3043
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003044 private static final String MESSAGE_ID = "messageId";
3045 private static final String SHOW_TOAST = "showToast";
3046 private static final String RECIPIENTS = "recipients";
3047
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003048 private boolean mShowToast;
3049
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003050 private ArrayList<String> mRecipients;
3051
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003052 // Public no-args constructor needed for fragment re-instantiation
3053 public SendConfirmDialogFragment() {}
3054
Tony Mantler2558b502013-07-09 10:53:34 -07003055 public static SendConfirmDialogFragment newInstance(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003056 final boolean showToast, final ArrayList<String> recipients) {
Tony Mantler2558b502013-07-09 10:53:34 -07003057 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
3058 final Bundle args = new Bundle(3);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003059 args.putInt(MESSAGE_ID, messageId);
3060 args.putBoolean(SHOW_TOAST, showToast);
3061 args.putStringArrayList(RECIPIENTS, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003062 frag.setArguments(args);
3063 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08003064 }
Tony Mantler2558b502013-07-09 10:53:34 -07003065
3066 @Override
3067 public Dialog onCreateDialog(Bundle savedInstanceState) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003068 final int messageId = getArguments().getInt(MESSAGE_ID);
3069 mShowToast = getArguments().getBoolean(SHOW_TOAST);
3070 mRecipients = getArguments().getStringArrayList(RECIPIENTS);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003071
3072 final int confirmTextId = (messageId == R.string.confirm_send_message) ?
3073 R.string.ok : R.string.send;
Tony Mantler2558b502013-07-09 10:53:34 -07003074
3075 return new AlertDialog.Builder(getActivity())
3076 .setMessage(messageId)
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003077 .setPositiveButton(confirmTextId, this)
Paul Westbrook7d1c5c42013-10-01 23:40:04 -07003078 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003079 .create();
3080 }
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003081
3082 @Override
3083 public void onClick(DialogInterface dialog, int which) {
3084 if (which == DialogInterface.BUTTON_POSITIVE) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003085 ((ComposeActivity) getActivity()).finishSendConfirmDialog(mShowToast, mRecipients);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07003086 }
3087 }
Tony Mantler2558b502013-07-09 10:53:34 -07003088 }
3089
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003090 private void finishSendConfirmDialog(
3091 final boolean showToast, final ArrayList<String> recipients) {
3092 performAdditionalSendOrSaveSanityChecks(false /* save */, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003093 }
3094
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003095 // The list of recipients are used by the additional sendOrSave checks.
3096 // However, the send confirm dialog may be shown before performing
3097 // the additional checks. As a result, we need to plumb the recipient
3098 // list through the send confirm dialog so that
3099 // performAdditionalSendOrSaveChecks can be performed properly.
Tony Mantler581edd42014-02-18 15:41:22 -08003100 private void showSendConfirmDialog(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003101 final boolean showToast, final ArrayList<String> recipients) {
3102 final DialogFragment frag = SendConfirmDialogFragment.newInstance(
3103 messageId, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003104 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08003105 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003106
Mindy Pereira181df782012-03-01 13:32:44 -08003107 /**
3108 * Returns whether the ComposeArea believes there is any text in the body of
3109 * the composition. TODO: When ComposeArea controls the Body as well, add
3110 * that here.
3111 */
3112 public boolean isBodyEmpty() {
3113 return !mQuotedTextView.isTextIncluded();
3114 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003115
Mindy Pereira181df782012-03-01 13:32:44 -08003116 /**
3117 * Test to see if the subject is empty.
3118 *
3119 * @return boolean.
3120 */
3121 // TODO: this will likely go away when composeArea.focus() is implemented
3122 // after all the widget control is moved over.
3123 public boolean isSubjectEmpty() {
3124 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
3125 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003126
Andy Huang0a2a3462013-12-20 15:56:13 -08003127 @VisibleForTesting
3128 public String getSubject() {
3129 return mSubject.getText().toString();
3130 }
3131
Jin Cao31bb3d62014-09-11 14:01:43 -07003132 private void sendOrSaveInternal(Context context, int requestId,
3133 ReplyFromAccount currReplyFromAccount, ReplyFromAccount originalReplyFromAccount,
3134 Message message, Message refMessage, CharSequence quotedText,
3135 SendOrSaveCallback callback, boolean save, int composeMode, ContentValues extraValues,
Jin Caoadae7a32014-09-09 18:16:25 -07003136 Bundle optionalAttachmentFds) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08003137 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003138
Paul Westbrookb4931c62013-01-14 17:51:18 -08003139 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07003140
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003141 MessageModification.putToAddresses(values, message.getToAddresses());
3142 MessageModification.putCcAddresses(values, message.getCcAddresses());
3143 MessageModification.putBccAddresses(values, message.getBccAddresses());
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003144 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07003145
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003146 MessageModification.putSubject(values, message.subject);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003147
Jin Cao77b4c2c2014-05-20 13:55:53 -07003148 // bodyHtml already have the composing spans removed.
3149 final String htmlBody = message.bodyHtml;
Jin Caoa9f5a8e2014-07-22 13:48:45 -07003150 final String textBody = message.bodyText;
Jin Cao0a8e8222014-10-02 11:27:52 -07003151 // fullbodyhtml/fullbodytext will contain the actual body plus the quoted text.
3152 String fullBodyHtml = htmlBody;
3153 String fullBodyText = textBody;
3154 String quotedString = null;
Anthony Lee2a3cc132014-04-22 14:15:25 -07003155 final boolean hasQuotedText = !TextUtils.isEmpty(quotedText);
3156 if (hasQuotedText) {
3157 // The quoted text is HTML at this point.
3158 quotedString = quotedText.toString();
Jin Cao0a8e8222014-10-02 11:27:52 -07003159 fullBodyHtml = htmlBody + quotedString;
3160 fullBodyText = textBody + Utils.convertHtmlToPlainText(quotedString);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003161 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
3162 MessageModification.putAppendRefMessageContent(values, true /* include quoted */);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08003163 }
Jin Cao0a8e8222014-10-02 11:27:52 -07003164
Jin Caoa9f5a8e2014-07-22 13:48:45 -07003165 // Only take refMessage into account if either one of its html/text is not empty.
Jin Cao0a8e8222014-10-02 11:27:52 -07003166 int quotedTextPos = -1;
Jin Caoa9f5a8e2014-07-22 13:48:45 -07003167 if (refMessage != null && !(TextUtils.isEmpty(refMessage.bodyHtml) &&
3168 TextUtils.isEmpty(refMessage.bodyText))) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07003169 // The code below might need to be revisited. The quoted text position is different
3170 // between text/html and text/plain parts and they should be stored seperately and
3171 // the right version should be used in the UI. text/html should have preference
3172 // if both exist. Issues like this made me file b/14256940 to make sure that we
3173 // properly handle the existing of both text/html and text/plain parts and to verify
3174 // that we are not making some assumptions that break if there is no text/html part.
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003175 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
Jin Cao0a8e8222014-10-02 11:27:52 -07003176 MessageModification.putBodyHtml(values, fullBodyHtml);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003177 if (hasQuotedText) {
3178 quotedTextPos = htmlBody.length() +
3179 QuotedTextView.getQuotedTextOffset(quotedString);
3180 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003181 }
3182 if (!TextUtils.isEmpty(refMessage.bodyText)) {
Jin Cao0a8e8222014-10-02 11:27:52 -07003183 MessageModification.putBody(values, fullBodyText);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003184 if (hasQuotedText && (quotedTextPos == -1)) {
3185 quotedTextPos = textBody.length();
3186 }
3187 }
3188 if (quotedTextPos != -1) {
3189 // The quoted text pos is the text/html version first and the text/plan version
3190 // if there is no text/html part. The reason for this is because preference
3191 // is given to text/html in the compose window if it exists. In the future, we
3192 // should calculate the index for both since the user could choose to compose
3193 // explicitly in text/plain.
3194 MessageModification.putQuoteStartPos(values, quotedTextPos);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003195 }
3196 } else {
Jin Cao0a8e8222014-10-02 11:27:52 -07003197 MessageModification.putBodyHtml(values, fullBodyHtml);
3198 MessageModification.putBody(values, fullBodyText);
Mindy Pereirac2031972012-04-03 09:38:35 -07003199 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07003200 int draftType = getDraftType(composeMode);
3201 MessageModification.putDraftType(values, draftType);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003202 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07003203 if (!TextUtils.isEmpty(refMessageId)) {
3204 MessageModification.putRefMessageId(values, refMessageId);
3205 }
Scott Kennedy60847252013-08-15 15:55:42 -07003206 if (extraValues != null) {
3207 values.putAll(extraValues);
3208 }
Jin Caoaff451f2014-09-09 14:32:04 -07003209
Jin Cao31bb3d62014-09-11 14:01:43 -07003210 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, requestId,
Jin Caoadae7a32014-09-09 18:16:25 -07003211 values, refMessageId, message.getAttachments(), optionalAttachmentFds, save);
Jin Cao0a8e8222014-10-02 11:27:52 -07003212 runSendOrSaveProviderCalls(sendOrSaveMessage, callback, currReplyFromAccount,
3213 originalReplyFromAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003214
Jin Cao7800d292014-08-24 11:31:15 -07003215 LogUtils.i(LOG_TAG, "[compose] SendOrSaveMessage [%s] posted (isSave: %s) - " +
Jin Cao0a8e8222014-10-02 11:27:52 -07003216 "bodyHtml length: %d, bodyText length: %d, quoted text pos: %d, attach count: %d",
3217 requestId, save, message.bodyHtml.length(), message.bodyText.length(),
3218 quotedTextPos, message.getAttachmentCount(true));
Mindy Pereira181df782012-03-01 13:32:44 -08003219 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003220
Paul Westbrookb4931c62013-01-14 17:51:18 -08003221 /**
3222 * Removes any composing spans from the specified string. This will create a new
3223 * SpannableString instance, as to not modify the behavior of the EditText view.
3224 */
3225 private static SpannableString removeComposingSpans(Spanned body) {
3226 final SpannableString messageBody = new SpannableString(body);
3227 BaseInputConnection.removeComposingSpans(messageBody);
Andy Huangff017272014-06-18 00:27:35 -07003228
3229 // Remove watcher spans while we're at it, so any off-UI thread manipulation of these
3230 // spans doesn't trigger unexpected side-effects. This copy is essentially 100% detached
3231 // from the EditText.
3232 //
3233 // (must remove SpanWatchers first to avoid triggering them as we remove other spans)
3234 removeSpansOfType(messageBody, SpanWatcher.class);
3235 removeSpansOfType(messageBody, TextWatcher.class);
3236
Paul Westbrookb4931c62013-01-14 17:51:18 -08003237 return messageBody;
3238 }
3239
Andy Huangff017272014-06-18 00:27:35 -07003240 private static void removeSpansOfType(SpannableString str, Class<?> cls) {
3241 for (Object span : str.getSpans(0, str.length(), cls)) {
3242 str.removeSpan(span);
3243 }
3244 }
3245
Mindy Pereira002ff522012-05-30 10:31:26 -07003246 private static int getDraftType(int mode) {
3247 int draftType = -1;
3248 switch (mode) {
3249 case ComposeActivity.COMPOSE:
3250 draftType = DraftType.COMPOSE;
3251 break;
3252 case ComposeActivity.REPLY:
3253 draftType = DraftType.REPLY;
3254 break;
3255 case ComposeActivity.REPLY_ALL:
3256 draftType = DraftType.REPLY_ALL;
3257 break;
3258 case ComposeActivity.FORWARD:
3259 draftType = DraftType.FORWARD;
3260 break;
3261 }
3262 return draftType;
3263 }
3264
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003265 /**
3266 * Derived classes should override this step to perform additional checks before
3267 * send or save. The default implementation simply calls {@link #sendOrSave(boolean, boolean)}.
3268 */
3269 protected void performAdditionalSendOrSaveSanityChecks(
3270 final boolean save, final boolean showToast, ArrayList<String> recipients) {
3271 sendOrSave(save, showToast);
3272 }
3273
3274 protected void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08003275 // Check if user is a monkey. Monkeys can compose and hit send
3276 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07003277 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08003278 return;
3279 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003280
Jin Cao77b4c2c2014-05-20 13:55:53 -07003281 final SendOrSaveCallback callback = new SendOrSaveCallback() {
Marc Blank0bbc8582012-04-23 15:07:57 -07003282 @Override
Jin Caoaff451f2014-09-09 14:32:04 -07003283 public void initializeSendOrSave() {
Jin Caoadae7a32014-09-09 18:16:25 -07003284 final Intent i = new Intent(ComposeActivity.this, EmptyService.class);
3285
3286 // API 16+ allows for setClipData. For pre-16 we are going to open the fds
3287 // on the main thread.
3288 if (Utils.isRunningJellybeanOrLater()) {
3289 // Grant the READ permission for the attachments to the service so that
3290 // as long as the service stays alive we won't hit PermissionExceptions.
3291 final ClipDescription desc = new ClipDescription("attachment_uris",
3292 new String[]{ClipDescription.MIMETYPE_TEXT_URILIST});
3293 ClipData clipData = null;
3294 for (Attachment a : mAttachmentsView.getAttachments()) {
3295 if (a != null && !Utils.isEmpty(a.contentUri)) {
3296 final ClipData.Item uriItem = new ClipData.Item(a.contentUri);
3297 if (clipData == null) {
3298 clipData = new ClipData(desc, uriItem);
3299 } else {
3300 clipData.addItem(uriItem);
3301 }
3302 }
3303 }
3304 i.setClipData(clipData);
3305 i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
3306 }
3307
Jin Caoaff451f2014-09-09 14:32:04 -07003308 synchronized (PENDING_SEND_OR_SAVE_TASKS_NUM) {
3309 if (PENDING_SEND_OR_SAVE_TASKS_NUM.getAndAdd(1) == 0) {
Mindy Pereira181df782012-03-01 13:32:44 -08003310 // Start service so we won't be killed if this app is
3311 // put in the background.
Jin Caoadae7a32014-09-09 18:16:25 -07003312 startService(i);
Mindy Pereira181df782012-03-01 13:32:44 -08003313 }
Mindy Pereira181df782012-03-01 13:32:44 -08003314 }
3315 if (sTestSendOrSaveCallback != null) {
Jin Caoaff451f2014-09-09 14:32:04 -07003316 sTestSendOrSaveCallback.initializeSendOrSave();
Mindy Pereira181df782012-03-01 13:32:44 -08003317 }
3318 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003319
Marc Blank0bbc8582012-04-23 15:07:57 -07003320 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003321 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
3322 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08003323 synchronized (mDraftLock) {
3324 mDraftId = message.id;
3325 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003326 if (sRequestMessageIdMap != null) {
Jin Cao31bb3d62014-09-11 14:01:43 -07003327 sRequestMessageIdMap.put(sendOrSaveMessage.mRequestId, mDraftId);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003328 }
Mindy Pereira181df782012-03-01 13:32:44 -08003329 // Cache request message map, in case the process is killed
3330 saveRequestMap();
3331 }
3332 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003333 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08003334 }
3335 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003336
Marc Blank0bbc8582012-04-23 15:07:57 -07003337 @Override
Jin Cao31bb3d62014-09-11 14:01:43 -07003338 public long getMessageId() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003339 synchronized (mDraftLock) {
Jin Cao31bb3d62014-09-11 14:01:43 -07003340 return mDraftId;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003341 }
3342 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003343
Marc Blank0bbc8582012-04-23 15:07:57 -07003344 @Override
Jin Caoaff451f2014-09-09 14:32:04 -07003345 public void sendOrSaveFinished(SendOrSaveMessage message, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07003346 // Update the last sent from account.
3347 if (mAccount != null) {
3348 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
3349 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003350 if (success) {
3351 // Successfully sent or saved so reset change markers
3352 discardChanges();
3353 } else {
3354 // A failure happened with saving/sending the draft
3355 // TODO(pwestbro): add a better string that should be used
3356 // when failing to send or save
3357 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
3358 .show();
3359 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003360
Jin Caoaff451f2014-09-09 14:32:04 -07003361 synchronized (PENDING_SEND_OR_SAVE_TASKS_NUM) {
3362 if (PENDING_SEND_OR_SAVE_TASKS_NUM.addAndGet(-1) == 0) {
3363 // Stop service so we can be killed.
3364 stopService(new Intent(ComposeActivity.this, EmptyService.class));
3365 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003366 }
3367 if (sTestSendOrSaveCallback != null) {
Jin Caoaff451f2014-09-09 14:32:04 -07003368 sTestSendOrSaveCallback.sendOrSaveFinished(message, success);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003369 }
3370 }
Mindy Pereira181df782012-03-01 13:32:44 -08003371 };
Tony Mantler1e05a1e2013-08-12 16:44:26 -07003372 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003373
Jin Cao77b4c2c2014-05-20 13:55:53 -07003374 final Spanned body = removeComposingSpans(mBodyView.getText());
Jin Caoadae7a32014-09-09 18:16:25 -07003375 callback.initializeSendOrSave();
3376
3377 // For pre-JB we need to open the fds on the main thread
3378 final Bundle attachmentFds;
3379 if (!Utils.isRunningJellybeanOrLater()) {
3380 attachmentFds = initializeAttachmentFds(this, mAttachmentsView.getAttachments());
3381 } else {
3382 attachmentFds = null;
3383 }
3384
Jin Cao31bb3d62014-09-11 14:01:43 -07003385 // Generate a unique message id for this request
3386 mRequestId = sRandom.nextInt();
Jin Cao77b4c2c2014-05-20 13:55:53 -07003387 SEND_SAVE_TASK_HANDLER.post(new Runnable() {
3388 @Override
3389 public void run() {
3390 final Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode(), body);
Jin Cao31bb3d62014-09-11 14:01:43 -07003391 sendOrSaveInternal(ComposeActivity.this, mRequestId, mReplyFromAccount,
3392 mDraftAccount, msg, mRefMessage, mQuotedTextView.getQuotedTextIfIncluded(),
3393 callback, save, mComposeMode, mExtraValues, attachmentFds);
Jin Cao77b4c2c2014-05-20 13:55:53 -07003394 }
3395 });
Mindy Pereira82cc5662012-01-09 17:29:30 -08003396
Mindy Pereira181df782012-03-01 13:32:44 -08003397 // Don't display the toast if the user is just changing the orientation,
3398 // but we still need to save the draft to the cursor because this is how we restore
3399 // the attachments when the configuration change completes.
3400 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
3401 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
3402 Toast.LENGTH_LONG).show();
3403 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003404
Mindy Pereira181df782012-03-01 13:32:44 -08003405 // Need to update variables here because the send or save completes
3406 // asynchronously even though the toast shows right away.
3407 discardChanges();
3408 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003409
Mindy Pereira181df782012-03-01 13:32:44 -08003410 // If we are sending, finish the activity
3411 if (!save) {
3412 finish();
3413 }
3414 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003415
Mindy Pereira181df782012-03-01 13:32:44 -08003416 /**
3417 * Save the state of the request messageid map. This allows for the Gmail
3418 * process to be killed, but and still allow for ComposeActivity instances
3419 * to be recreated correctly.
3420 */
3421 private void saveRequestMap() {
3422 // TODO: store the request map in user preferences.
3423 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003424
Tony Mantler581edd42014-02-18 15:41:22 -08003425 @SuppressLint("NewApi")
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003426 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08003427 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
3428 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07003429 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07003430 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003431 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08003432 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08003433 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
3434 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08003435 }
3436
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003437 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003438 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003439 if (mCcBccButton != null) {
Jin Cao9d358a12014-07-24 12:15:38 -07003440 mCcBccButton.setVisibility(View.GONE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003441 }
3442 }
3443
Andy Huang4fe0af82013-08-20 17:24:51 -07003444 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003445 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07003446 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003447 case COMPOSE:
3448 msgType = "new_message";
3449 break;
3450 case REPLY:
3451 msgType = "reply";
3452 break;
3453 case REPLY_ALL:
3454 msgType = "reply_all";
3455 break;
3456 case FORWARD:
3457 msgType = "forward";
3458 break;
3459 default:
3460 msgType = "unknown";
3461 break;
3462 }
Andy Huang4fe0af82013-08-20 17:24:51 -07003463 return msgType;
3464 }
3465
3466 private void logSendOrSave(boolean save) {
3467 if (!Analytics.isLoggable() || mAttachmentsView == null) {
3468 return;
3469 }
3470
3471 final String category = (save) ? "message_save" : "message_send";
3472 final int attachmentCount = getAttachments().size();
3473 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07003474 final String label;
3475 final long value;
3476 if (mComposeMode == COMPOSE) {
3477 label = Integer.toString(attachmentCount);
3478 value = attachmentCount;
3479 } else {
3480 label = null;
3481 value = 0;
3482 }
3483 Analytics.getInstance().sendEvent(category, msgType, label, value);
3484 }
3485
Mindy Pereira326c6602012-01-04 15:32:42 -08003486 @Override
3487 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003488 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003489 if (position == ComposeActivity.REPLY) {
3490 mComposeMode = ComposeActivity.REPLY;
3491 } else if (position == ComposeActivity.REPLY_ALL) {
3492 mComposeMode = ComposeActivity.REPLY_ALL;
3493 } else if (position == ComposeActivity.FORWARD) {
3494 mComposeMode = ComposeActivity.FORWARD;
3495 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003496 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003497 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003498 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003499 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003500 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003501 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003502 boolean showCc = false;
3503 boolean showBcc = false;
3504 if (mDraft != null) {
3505 // Following desktop behavior, if the user has added a BCC
3506 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003507 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003508 // Use the draft to determine what to populate.
3509 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003510 showCc = showBcc
3511 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003512 }
3513 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003514 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003515 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003516 }
Jin Caoc5c550a2014-07-29 11:53:17 -07003517 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003518 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003519 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003520 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003521 return true;
3522 }
3523
Mindy Pereirab3112a22012-06-20 12:10:03 -07003524 @VisibleForTesting
3525 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003526 // When switching between reply, reply all, forward,
3527 // follow the behavior of webview.
3528 // The contents of the following fields are cleared
3529 // so that they can be populated directly from the
3530 // ref message:
3531 // 1) Any recipient fields
3532 // 2) The subject
3533 mTo.setText("");
3534 mCc.setText("");
3535 mBcc.setText("");
3536 // Any edits to the subject are replaced with the original subject.
3537 mSubject.setText("");
3538
3539 // Any changes to the contents of the following fields are kept:
3540 // 1) Body
3541 // 2) Attachments
3542 // If the user made changes to attachments, keep their changes.
3543 if (!mAttachmentsChanged) {
3544 mAttachmentsView.deleteAllAttachments();
3545 }
3546 }
3547
Mindy Pereira326c6602012-01-04 15:32:42 -08003548 private class ComposeModeAdapter extends ArrayAdapter<String> {
3549
Jin Caof7461632014-08-11 15:21:43 -07003550 private Context mContext;
Mindy Pereira326c6602012-01-04 15:32:42 -08003551 private LayoutInflater mInflater;
3552
3553 public ComposeModeAdapter(Context context) {
3554 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3555 .getStringArray(R.array.compose_modes));
Jin Caof7461632014-08-11 15:21:43 -07003556 mContext = context;
Mindy Pereira326c6602012-01-04 15:32:42 -08003557 }
3558
3559 private LayoutInflater getInflater() {
3560 if (mInflater == null) {
Jin Caof7461632014-08-11 15:21:43 -07003561 mInflater = LayoutInflater.from(mContext);
Mindy Pereira326c6602012-01-04 15:32:42 -08003562 }
3563 return mInflater;
3564 }
3565
3566 @Override
3567 public View getView(int position, View convertView, ViewGroup parent) {
3568 if (convertView == null) {
3569 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3570 }
3571 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3572 return super.getView(position, convertView, parent);
3573 }
3574 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003575
3576 @Override
3577 public void onRespondInline(String text) {
3578 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003579 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003580 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003581 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003582 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003583 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003584 }
3585
3586 /**
3587 * Append text to the body of the message. If there is no existing body
3588 * text, just sets the body to text.
3589 *
Tony Mantler581edd42014-02-18 15:41:22 -08003590 * @param text Text to append
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003591 * @param withSignature True to append a signature.
3592 */
3593 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003594 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003595 if (bodyText != null && bodyText.length() > 0) {
3596 bodyText.append(text);
3597 } else {
3598 setBody(text, withSignature);
3599 }
3600 }
3601
3602 /**
3603 * Set the body of the message.
Jin Cao738cafe2014-09-02 16:10:29 -07003604 * Please try to exclusively use this method instead of calling mBodyView.setText(..) directly.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003605 *
Tony Mantler581edd42014-02-18 15:41:22 -08003606 * @param text text to set
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003607 * @param withSignature True to append a signature.
3608 */
3609 public void setBody(CharSequence text, boolean withSignature) {
Jin Cao738cafe2014-09-02 16:10:29 -07003610 LogUtils.i(LOG_TAG, "Body populated, len: %d, sig: %b", text.length(), withSignature);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003611 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003612 if (withSignature) {
3613 appendSignature();
3614 }
3615 }
3616
3617 private void appendSignature() {
Tony Mantler6a7ac782014-02-19 15:22:02 -08003618 final String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3619 final int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
mindyp27083062012-11-15 09:02:01 -08003620 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003621 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003622 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003623 // Appending a signature does not count as changing text.
3624 mBodyView.removeTextChangedListener(this);
3625 mBodyView.append(convertToPrintableSignature(mSignature));
3626 mBodyView.addTextChangedListener(this);
3627 }
Tony Mantler6a7ac782014-02-19 15:22:02 -08003628 resetBodySelection();
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003629 }
3630 }
3631
3632 private String convertToPrintableSignature(String signature) {
3633 String signatureResource = getResources().getString(R.string.signature);
3634 if (signature == null) {
3635 signature = "";
3636 }
3637 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003638 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003639
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003640 @Override
3641 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003642 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3643 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003644 // Clear a signature, if there was one.
3645 mBodyView.removeTextChangedListener(this);
3646 String oldSignature = mSignature;
3647 String bodyText = getBody().getText().toString();
3648 if (!TextUtils.isEmpty(oldSignature)) {
3649 int pos = getSignatureStartPosition(oldSignature, bodyText);
3650 if (pos > -1) {
Jin Cao738cafe2014-09-02 16:10:29 -07003651 setBody(bodyText.substring(0, pos), false);
mindypf432dbc2012-11-12 16:00:44 -08003652 }
3653 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003654 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003655 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003656 // TODO: handle discarding attachments when switching accounts.
3657 // Only enable save for this draft if there is any other content
3658 // in the message.
3659 if (!isBlank()) {
3660 enableSave(true);
3661 }
3662 mReplyFromChanged = true;
3663 initRecipients();
Greg Bullockd47a7042014-08-13 16:02:59 +02003664
3665 invalidateOptionsMenu();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003666 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003667 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003668
3669 public void enableSave(boolean enabled) {
3670 if (mSave != null) {
3671 mSave.setEnabled(enabled);
3672 }
3673 }
3674
Tony Mantler2558b502013-07-09 10:53:34 -07003675 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003676 // Public no-args constructor needed for fragment re-instantiation
3677 public DiscardConfirmDialogFragment() {}
3678
Tony Mantler2558b502013-07-09 10:53:34 -07003679 @Override
3680 public Dialog onCreateDialog(Bundle savedInstanceState) {
3681 return new AlertDialog.Builder(getActivity())
3682 .setMessage(R.string.confirm_discard_text)
3683 .setPositiveButton(R.string.discard,
3684 new DialogInterface.OnClickListener() {
3685 @Override
3686 public void onClick(DialogInterface dialog, int which) {
3687 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3688 }
3689 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003690 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003691 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003692 }
3693 }
3694
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003695 private void doDiscard() {
Jin Caoe0037922014-09-16 10:23:50 -07003696 // Only need to ask for confirmation if the draft is in a dirty state.
3697 if (isDraftDirty()) {
3698 final DialogFragment frag = new DiscardConfirmDialogFragment();
3699 frag.show(getFragmentManager(), "discard confirm");
3700 } else {
3701 doDiscardWithoutConfirmation();
3702 }
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003703 }
Jin Caoe0037922014-09-16 10:23:50 -07003704
Mindy Pereira82cc5662012-01-09 17:29:30 -08003705 /**
3706 * Effectively discard the current message.
3707 *
3708 * This method is either invoked from the menu or from the dialog
3709 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003710 */
Tony Mantler2558b502013-07-09 10:53:34 -07003711 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003712 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003713 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3714 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003715 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003716 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003717 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3718 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003719 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003720 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003721 // This is not strictly necessary (since we should not try to
3722 // save the draft after calling this) but it ensures that if we
3723 // do save again for some reason we make a new draft rather than
3724 // trying to resave an expunged draft.
3725 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3726 }
3727 }
3728
Tony Mantler2558b502013-07-09 10:53:34 -07003729 // Display a toast to let the user know
3730 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003731
3732 // This prevents the draft from being saved in onPause().
3733 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003734 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003735 finish();
3736 }
3737
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003738 private void saveIfNeeded() {
3739 if (mAccount == null) {
3740 // We have not chosen an account yet so there's no way that we can save. This is ok,
3741 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3742 // user has not interacted with us yet and there is no real state to save.
3743 return;
3744 }
3745
Jin Caoe0037922014-09-16 10:23:50 -07003746 if (isDraftDirty()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003747 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003748 }
3749 }
3750
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003751 @Override
3752 public void onAttachmentDeleted() {
3753 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003754 // If we are showing any attachments, make sure we have an upper
3755 // divider.
zengqiang1024a98bbaf2015-05-28 15:04:57 +08003756 if(mDraft!= null) {
3757 String msgId = mDraft.id + "";
3758 updateMessageFlagAttachment(msgId);
3759 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3760 updateSaveUi();
3761 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003762 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003763
yangyong0707b01a04a2015-05-11 15:39:02 +08003764 private int updateMessageFlagAttachment(String msgId) {
3765 ContentResolver resolver = mContext.getContentResolver();
3766 Uri uri = Uri.parse("content://com.android.email.provider/message/");
3767 ContentValues values = new ContentValues();
3768 values.put("flagAttachment", 0);
3769 return resolver.update(uri, values, "_id=?", new String[] { msgId });
3770 }
3771
mindyp40882432012-09-06 11:07:40 -07003772 @Override
3773 public void onAttachmentAdded() {
3774 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3775 mAttachmentsView.focusLastAttachment();
3776 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003777
3778 /**
3779 * This is called any time one of our text fields changes.
3780 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003781 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003782 public void afterTextChanged(Editable s) {
3783 mTextChanged = true;
3784 updateSaveUi();
3785 }
3786
3787 @Override
3788 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3789 // Do nothing.
3790 }
3791
Marc Blank0bbc8582012-04-23 15:07:57 -07003792 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003793 public void onTextChanged(CharSequence s, int start, int before, int count) {
3794 // Do nothing.
3795 }
3796
3797
3798 // There is a big difference between the text associated with an address changing
3799 // to add the display name or to format properly and a recipient being added or deleted.
3800 // Make sure we only notify of changes when a recipient has been added or deleted.
3801 private class RecipientTextWatcher implements TextWatcher {
3802 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3803
3804 private RecipientEditTextView mView;
3805
3806 private TextWatcher mListener;
3807
3808 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3809 mView = view;
3810 mListener = listener;
3811 }
3812
3813 @Override
3814 public void afterTextChanged(Editable s) {
3815 if (hasChanged()) {
3816 mListener.afterTextChanged(s);
3817 }
3818 }
3819
3820 private boolean hasChanged() {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003821 final ArrayList<String> currRecips = buildEmailAddressList(getAddressesFromList(mView));
3822 int totalCount = currRecips.size();
Mindy Pereira75f66632012-01-11 11:42:02 -08003823 int totalPrevCount = 0;
3824 for (Entry<String, Integer> entry : mContent.entrySet()) {
3825 totalPrevCount += entry.getValue();
3826 }
3827 if (totalCount != totalPrevCount) {
3828 return true;
3829 }
3830
3831 for (String recip : currRecips) {
3832 if (!mContent.containsKey(recip)) {
3833 return true;
3834 } else {
3835 int count = mContent.get(recip) - 1;
3836 if (count < 0) {
3837 return true;
3838 } else {
3839 mContent.put(recip, count);
3840 }
3841 }
3842 }
3843 return false;
3844 }
3845
Mindy Pereira75f66632012-01-11 11:42:02 -08003846 @Override
3847 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003848 final ArrayList<String> recips = buildEmailAddressList(getAddressesFromList(mView));
Mindy Pereira75f66632012-01-11 11:42:02 -08003849 for (String recip : recips) {
3850 if (!mContent.containsKey(recip)) {
3851 mContent.put(recip, 1);
3852 } else {
3853 mContent.put(recip, (mContent.get(recip)) + 1);
3854 }
3855 }
3856 }
3857
3858 @Override
3859 public void onTextChanged(CharSequence s, int start, int before, int count) {
3860 // Do nothing.
3861 }
3862 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003863
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003864 /**
3865 * Returns a list of email addresses from the recipients. List only contains
3866 * email addresses strips additional info like the recipient's name.
3867 */
3868 private static ArrayList<String> buildEmailAddressList(String[] recips) {
3869 // Tokenize them all and put them in the list.
3870 final ArrayList<String> recipAddresses = Lists.newArrayListWithCapacity(recips.length);
3871 for (int i = 0; i < recips.length; i++) {
3872 recipAddresses.add(Rfc822Tokenizer.tokenize(recips[i])[0].getAddress());
3873 }
3874 return recipAddresses;
3875 }
3876
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003877 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3878 if (sTestSendOrSaveCallback != null && testCallback != null) {
3879 throw new IllegalStateException("Attempting to register more than one test callback");
3880 }
3881 sTestSendOrSaveCallback = testCallback;
3882 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003883
3884 @VisibleForTesting
3885 protected ArrayList<Attachment> getAttachments() {
3886 return mAttachmentsView.getAttachments();
3887 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003888
3889 @Override
3890 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3891 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003892 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3893 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3894 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003895 case REFERENCE_MESSAGE_LOADER:
3896 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3897 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003898 case LOADER_ACCOUNT_CURSOR:
3899 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3900 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003901 }
3902 return null;
3903 }
3904
3905 @Override
3906 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003907 int id = loader.getId();
3908 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003909 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003910 if (data != null && data.moveToFirst()) {
3911 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003912 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003913 initFromRefMessage(mComposeMode);
3914 finishSetup(mComposeMode, intent, null);
3915 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003916 String to = intent.getStringExtra(EXTRA_TO);
3917 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003918 mRefMessage.setTo(null);
3919 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003920 clearChangeListeners();
3921 mTo.append(to);
3922 initChangeListeners();
3923 }
3924 }
3925 } else {
3926 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003927 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003928 break;
Alice Yanga990a712013-03-13 18:37:00 -07003929 case REFERENCE_MESSAGE_LOADER:
3930 // Only populate mRefMessage and leave other fields untouched.
3931 if (data != null && data.moveToFirst()) {
3932 mRefMessage = new Message(data);
3933 }
Andy Huang9f855d62013-05-30 17:15:03 -07003934 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003935 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003936 case LOADER_ACCOUNT_CURSOR:
3937 if (data != null && data.moveToFirst()) {
3938 // there are accounts now!
3939 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003940 final ArrayList<Account> accounts = new ArrayList<Account>();
3941 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003942 do {
Ray Chen4b0c0122014-07-11 15:24:54 +02003943 account = Account.builder().buildFrom(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003944 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003945 initializedAccounts.add(account);
3946 }
3947 accounts.add(account);
3948 } while (data.moveToNext());
3949 if (initializedAccounts.size() > 0) {
3950 findViewById(R.id.wait).setVisibility(View.GONE);
3951 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3952 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003953 mAccounts = initializedAccounts.toArray(
3954 new Account[initializedAccounts.size()]);
3955
Mindy Pereirab199d172012-08-13 11:04:03 -07003956 finishCreate();
3957 invalidateOptionsMenu();
3958 } else {
3959 // Show "waiting"
3960 account = accounts.size() > 0 ? accounts.get(0) : null;
3961 showWaitFragment(account);
3962 }
3963 }
3964 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003965 }
3966 }
3967
Mindy Pereirab199d172012-08-13 11:04:03 -07003968 private void showWaitFragment(Account account) {
3969 WaitFragment fragment = getWaitFragment();
3970 if (fragment != null) {
3971 fragment.updateAccount(account);
3972 } else {
3973 findViewById(R.id.wait).setVisibility(View.VISIBLE);
Andy Huangc96efcc2014-04-09 15:30:42 -07003974 replaceFragment(WaitFragment.newInstance(account, false /* expectingMessages */),
Mindy Pereirab199d172012-08-13 11:04:03 -07003975 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3976 }
3977 }
3978
3979 private WaitFragment getWaitFragment() {
3980 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3981 }
3982
3983 private int replaceFragment(Fragment fragment, int transition, String tag) {
3984 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003985 fragmentTransaction.setTransition(transition);
3986 fragmentTransaction.replace(R.id.wait, fragment, tag);
3987 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3988 return transactionId;
3989 }
3990
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003991 @Override
3992 public void onLoaderReset(Loader<Cursor> arg0) {
3993 // Do nothing.
3994 }
Jin Cao77b4c2c2014-05-20 13:55:53 -07003995
3996 /**
3997 * Background task to convert the message's html to Spanned.
3998 */
3999 private class HtmlToSpannedTask extends AsyncTask<String, Void, Spanned> {
4000
4001 @Override
4002 protected Spanned doInBackground(String... input) {
Andy Huang9ed742c2014-06-18 02:34:50 -07004003 return HtmlUtils.htmlToSpan(input[0], mSpanConverterFactory);
Jin Cao77b4c2c2014-05-20 13:55:53 -07004004 }
4005
4006 @Override
4007 protected void onPostExecute(Spanned spanned) {
4008 mBodyView.removeTextChangedListener(ComposeActivity.this);
Jin Cao738cafe2014-09-02 16:10:29 -07004009 setBody(spanned, false);
Jin Cao77b4c2c2014-05-20 13:55:53 -07004010 mTextChanged = false;
4011 mBodyView.addTextChangedListener(ComposeActivity.this);
4012 }
4013 }
Andrew Sapperstein734718a2014-09-15 17:29:13 -07004014
Andrew Sapperstein734718a2014-09-15 17:29:13 -07004015 @Override
4016 public void onSupportActionModeStarted(ActionMode mode) {
4017 super.onSupportActionModeStarted(mode);
Jin Caof14d8b32014-10-07 18:19:41 -07004018 ViewUtils.setStatusBarColor(this, R.color.action_mode_statusbar_color);
Andrew Sapperstein734718a2014-09-15 17:29:13 -07004019 }
4020
Andrew Sapperstein734718a2014-09-15 17:29:13 -07004021 @Override
4022 public void onSupportActionModeFinished(ActionMode mode) {
4023 super.onSupportActionModeFinished(mode);
Jin Caof14d8b32014-10-07 18:19:41 -07004024 ViewUtils.setStatusBarColor(this, R.color.primary_dark_color);
Andrew Sapperstein734718a2014-09-15 17:29:13 -07004025 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07004026}