blob: 41157eb5f8f7935442d54b6fe5e86f5d04ec050e [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;
Mindy Pereira326c6602012-01-04 15:32:42 -080021import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070022import android.app.ActionBar.OnNavigationListener;
23import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080024import android.app.ActivityManager;
25import android.app.AlertDialog;
26import android.app.Dialog;
Tony Mantler2558b502013-07-09 10:53:34 -070027import android.app.DialogFragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070028import android.app.Fragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070029import android.app.FragmentTransaction;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070030import android.app.LoaderManager;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070031import android.content.ClipData;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080033import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080034import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070035import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080036import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080037import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070038import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.content.pm.ActivityInfo;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -070040import android.content.res.Resources;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080041import android.database.Cursor;
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;
Mindy Pereira82cc5662012-01-09 17:29:30 -080046import android.os.Handler;
47import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070048import android.os.ParcelFileDescriptor;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.provider.BaseColumns;
Alan Lau439aa5d2014-05-27 17:57:13 -070050import android.support.v4.app.RemoteInput;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080051import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080052import android.text.Html;
mindyped9c2f02012-10-12 10:02:08 -070053import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080054import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080055import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080056import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080057import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080058import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070059import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070060import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080061import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080062import android.view.Menu;
63import android.view.MenuInflater;
64import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080065import android.view.View;
66import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070067import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080068import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070069import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080070import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080071import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070072import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080073import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080074import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080075
Mindy Pereirac17d0732011-12-29 10:46:19 -080076import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070077import com.android.common.contacts.DataUsageStatUpdater;
Tony Mantler821e5782014-01-06 15:33:43 -080078import com.android.emailcommon.mail.Address;
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -070079import com.android.ex.chips.BaseRecipientAdapter;
80import com.android.ex.chips.DropdownChipLayouter;
Andy Huang5c5fd572012-04-08 18:19:29 -070081import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080082import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070083import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070084import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070085import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070086import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080087import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080088import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080089import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080090import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080091import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080092import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070093import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080094import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080095import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070096import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080097import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080098import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070099import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -0700100import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -0700101import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700102import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -0700103import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -0700104import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -0700105import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800106import com.android.mail.utils.ContentProviderTask;
Jin Cao77b4c2c2014-05-20 13:55:53 -0700107import com.android.mail.utils.HtmlUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -0700108import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800109import com.android.mail.utils.LogUtils;
Alan Lau15490232014-03-06 14:53:14 -0800110import com.android.mail.utils.NotificationActionUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800111import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800112import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800113import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800114import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800115
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700116import java.io.FileNotFoundException;
117import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700118import java.io.UnsupportedEncodingException;
119import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800120import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700121import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800122import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800123import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800124import java.util.HashSet;
125import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700126import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700127import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800128import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800129
130public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700131 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700132 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
Andrew Sappersteinffd61552014-05-14 15:04:23 -0700133 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
134 RecipientEditTextView.RecipientEntryItemClickedListener {
Scott Kennedya0287a82014-04-07 14:30:13 -0700135 /**
136 * An {@link Intent} action that launches {@link ComposeActivity}, but is handled as if the
137 * {@link Activity} were launched with no special action.
138 */
139 private static final String ACTION_LAUNCH_COMPOSE =
140 "com.android.mail.intent.action.LAUNCH_COMPOSE";
141
Mindy Pereira6349a042012-01-04 11:25:01 -0800142 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700143 public static final int COMPOSE = -1;
144 public static final int REPLY = 0;
145 public static final int REPLY_ALL = 1;
146 public static final int FORWARD = 2;
147 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800148
149 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700150 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800151
Mindy Pereira326689d2012-05-17 10:14:14 -0700152 private static final String EXTRA_SHOW_CC = "showCc";
153 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800154 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800155 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700156
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700157 private static final String UTF8_ENCODING_NAME = "UTF-8";
158
159 private static final String MAIL_TO = "mailto";
160
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700161 private static final String EXTRA_SUBJECT = "subject";
162
163 private static final String EXTRA_BODY = "body";
164
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700165 /**
166 * Expected to be html formatted text.
167 */
168 private static final String EXTRA_QUOTED_TEXT = "quotedText";
169
mindypd27b6ea2012-10-05 09:43:49 -0700170 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700171
Mark Wei62066e42012-09-13 12:07:02 -0700172 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
173
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700174 // Extra that we can get passed from other activities
Tony Mantler184ec732013-10-24 13:13:49 -0700175 @VisibleForTesting
176 protected static final String EXTRA_TO = "to";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700177 private static final String EXTRA_CC = "cc";
178 private static final String EXTRA_BCC = "bcc";
179
Scott Kennedy60847252013-08-15 15:55:42 -0700180 /**
181 * An optional extra containing a {@link ContentValues} of values to be added to
182 * {@link SendOrSaveMessage#mValues}.
183 */
184 public static final String EXTRA_VALUES = "extra-values";
185
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700186 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700187 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
188 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700189
Alan Lau439aa5d2014-05-27 17:57:13 -0700190 private static final String LEGACY_WEAR_EXTRA = "com.google.android.wearable.extras";
191
Andrew Sapperstein09da9422014-05-30 09:48:08 -0700192 /**
193 * Constant value for the threshold to use for auto-complete suggestions
194 * for the to/cc/bcc fields.
195 */
196 private static final int COMPLETION_THRESHOLD = 1;
197
Mindy Pereira82cc5662012-01-09 17:29:30 -0800198 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
199 // Map containing information about requests to create new messages, and the id of the
200 // messages that were the result of those requests.
201 //
202 // This map is used when the activity that initiated the save a of a new message, is killed
203 // before the save has completed (and when we know the id of the newly created message). When
204 // a save is completed, the service that is running in the background, will update the map
205 //
206 // When a new ComposeActivity instance is created, it will attempt to use the information in
207 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
208 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
209 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700210 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800211 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800212 /**
213 * Notifies the {@code Activity} that the caller is an Email
214 * {@code Activity}, so that the back behavior may be modified accordingly.
215 *
216 * @see #onAppUpPressed
217 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700218 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800219
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700220 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700221
Scott Kennedy5680ec22013-01-07 13:15:20 -0800222 /** If set, we will clear notifications for this folder. */
223 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
Alan Laue806c942014-06-06 16:19:15 -0700224 public static final String EXTRA_NOTIFICATION_CONVERSATION = "extra-notification-conversation";
Scott Kennedy5680ec22013-01-07 13:15:20 -0800225
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800226 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700227 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700228 // If this is a reply/forward then this extra will hold a uri we must query
229 // to get the original message.
230 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700231 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700232 // draft message
233 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800234 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700235 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800236 // Request numbers for activities we start
237 private static final int RESULT_PICK_ATTACHMENT = 1;
238 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700239 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700240 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700241
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700242 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
243 private static final String EXTRA_REQUEST_ID = "requestId";
244 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700245 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700246 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700247 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700248 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700249 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700250 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700251 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700252 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700253 private static final String MIME_TYPE_PHOTO = "image/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800254
Andy Huang9f855d62013-05-30 17:15:03 -0700255 private static final String KEY_INNER_SAVED_STATE = "compose_state";
256
Mindy Pereira82cc5662012-01-09 17:29:30 -0800257 /**
258 * A single thread for running tasks in the background.
259 */
Jin Cao5134be52014-05-06 19:18:38 -0700260 private final static Handler SEND_SAVE_TASK_HANDLER;
261 static {
262 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
263 handlerThread.start();
264
265 SEND_SAVE_TASK_HANDLER = new Handler(handlerThread.getLooper());
266 }
267
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700268 private boolean mUseNewChips = false;
269
Mindy Pereirac17d0732011-12-29 10:46:19 -0800270 private RecipientEditTextView mTo;
271 private RecipientEditTextView mCc;
272 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800273 private Button mCcBccButton;
274 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800275 private AttachmentsView mAttachmentsView;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700276 protected Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700277 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800278 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800279 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800280 private TextView mSubject;
281
Mindy Pereira326c6602012-01-04 15:32:42 -0800282 private ComposeModeAdapter mComposeModeAdapter;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700283 protected int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800284 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800285 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700286 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800287 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800288 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800289 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700290 @VisibleForTesting
291 protected FromAddressSpinner mFromSpinner;
Andy Huang5f082212014-06-11 22:19:21 -0700292 protected boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800293 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800294 private boolean mTextChanged;
295 private boolean mReplyFromChanged;
296 private MenuItem mSave;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700297 @VisibleForTesting
298 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800299 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
300 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800301 private ReplyFromAccount mDraftAccount;
Tony Mantler581edd42014-02-18 15:41:22 -0800302 private final Object mDraftLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800303
Mindy Pereira326c6602012-01-04 15:32:42 -0800304 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700305 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
306 */
307 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700308 private RecipientTextWatcher mToListener;
309 private RecipientTextWatcher mCcListener;
310 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700311 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700312 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700313 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700314 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700315
mindyp1623f9b2012-11-21 12:41:16 -0800316 // Array of the outstanding send or save tasks. Access is synchronized
317 // with the object itself
318 /* package for testing */
319 @VisibleForTesting
Tony Mantler581edd42014-02-18 15:41:22 -0800320 public final ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
mindyp1623f9b2012-11-21 12:41:16 -0800321 // FIXME: this variable is never read. related to sRequestMessageIdMap.
322 private int mRequestId;
323 private String mSignature;
324 private Account[] mAccounts;
325 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700326 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800327
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700328 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800329 * Can be called from a non-UI thread.
330 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800331 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700332 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
333 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800334 }
335
Mindy Pereira6349a042012-01-04 11:25:01 -0800336 /**
337 * Can be called from a non-UI thread.
338 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800339 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700340 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800341 }
342
343 /**
344 * Can be called from a non-UI thread.
345 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700346 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700347 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
348 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700349 }
350
351 /**
352 * Can be called from a non-UI thread.
353 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700354 public static void composeWithExtraValues(Context launcher, Account account,
355 String subject, final ContentValues extraValues) {
356 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
357 }
358
359 /**
360 * Can be called from a non-UI thread.
361 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800362 public static Intent createReplyIntent(final Context launcher, final Account account,
363 final Uri messageUri, final boolean isReplyAll) {
364 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
365 }
366
367 /**
368 * Can be called from a non-UI thread.
369 */
370 public static Intent createForwardIntent(final Context launcher, final Account account,
371 final Uri messageUri) {
372 return createActionIntent(launcher, account, messageUri, FORWARD);
373 }
374
Scott Kennedya0287a82014-04-07 14:30:13 -0700375 private static Intent createActionIntent(final Context context, final Account account,
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800376 final Uri messageUri, final int action) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700377 final Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
378 intent.setPackage(context.getPackageName());
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800379
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700380 updateActionIntent(account, messageUri, action, intent);
381
382 return intent;
383 }
384
385 @VisibleForTesting
386 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800387 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
388 intent.putExtra(EXTRA_ACTION, action);
389 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
390 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
391
392 return intent;
393 }
394
395 /**
396 * Can be called from a non-UI thread.
397 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800398 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700399 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800400 }
401
402 /**
403 * Can be called from a non-UI thread.
404 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800405 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700406 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
407 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800408 }
409
410 /**
411 * Can be called from a non-UI thread.
412 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800413 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700414 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800415 }
416
Alice Yang1ebc2db2013-03-14 21:21:44 -0700417 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
418 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700419 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700420 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700421 }
422
Scott Kennedya0287a82014-04-07 14:30:13 -0700423 private static void launch(Context context, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700424 String toAddress, String body, String quotedText, String subject,
425 final ContentValues extraValues) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700426 Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
427 intent.setPackage(context.getPackageName());
Mindy Pereira6349a042012-01-04 11:25:01 -0800428 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
429 intent.putExtra(EXTRA_ACTION, action);
430 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700431 if (action == EDIT_DRAFT) {
432 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
433 } else {
434 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
435 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700436 if (toAddress != null) {
437 intent.putExtra(EXTRA_TO, toAddress);
438 }
439 if (body != null) {
440 intent.putExtra(EXTRA_BODY, body);
441 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700442 if (quotedText != null) {
443 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
444 }
445 if (subject != null) {
446 intent.putExtra(EXTRA_SUBJECT, subject);
447 }
Scott Kennedy60847252013-08-15 15:55:42 -0700448 if (extraValues != null) {
449 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
450 intent.putExtra(EXTRA_VALUES, extraValues);
451 }
Andy Huange0f03202014-06-13 17:34:49 -0700452 if (action == COMPOSE) {
453 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
454 } else if (message != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700455 intent.setData(Utils.normalizeUri(message.uri));
Andy Huange0f03202014-06-13 17:34:49 -0700456 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700457 context.startActivity(intent);
Mindy Pereira6349a042012-01-04 11:25:01 -0800458 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800459
Scott Kennedya0287a82014-04-07 14:30:13 -0700460 public static void composeMailto(Context context, Account account, Uri mailto) {
461 final Intent intent = new Intent(Intent.ACTION_VIEW, mailto);
462 intent.setPackage(context.getPackageName());
Andy Huang0a2a3462013-12-20 15:56:13 -0800463 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
464 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Andy Huange0f03202014-06-13 17:34:49 -0700465 if (mailto != null) {
James Lemieuxcb1018a2014-06-18 11:09:18 -0700466 intent.setData(Utils.normalizeUri(mailto));
Andy Huange0f03202014-06-13 17:34:49 -0700467 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700468 context.startActivity(intent);
Andy Huang0a2a3462013-12-20 15:56:13 -0800469 }
470
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800471 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700472 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800473 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800474 setContentView(R.layout.compose);
Andy Huang9f855d62013-05-30 17:15:03 -0700475 mInnerSavedState = (savedInstanceState != null) ?
476 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700477 checkValidAccounts();
478 }
479
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700480 private boolean shouldUseNewChips() {
481 // Get the Android ID from this device
482 String androidId = android.provider.Settings.Secure
483 .getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
484
485 // If we don't have a valid android id, just use account name hash code
486 if (TextUtils.isEmpty(androidId)) {
487 LogUtils.d(LOG_TAG, "Fallback to email address");
488 androidId = mAccount.getEmailAddress();
489 }
490
491 // randomly cut our userbase in half
492 return (androidId.hashCode() % 2) == 1;
493 }
494
Mindy Pereirab199d172012-08-13 11:04:03 -0700495 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700496 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800497 findViews();
Tony Mantler581edd42014-02-18 15:41:22 -0800498 final Intent intent = getIntent();
499 final Message message;
500 final ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700501 mShowQuotedText = false;
Tony Mantler581edd42014-02-18 15:41:22 -0800502 final CharSequence quotedText;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700503 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700504 // Check for any of the possibly supplied accounts.;
Tony Mantler581edd42014-02-18 15:41:22 -0800505 final Account account;
Andy Huang9f855d62013-05-30 17:15:03 -0700506 if (hadSavedInstanceStateMessage(savedState)) {
507 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
508 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
Tony Mantler581edd42014-02-18 15:41:22 -0800509 message = savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700510
Andy Huang9f855d62013-05-30 17:15:03 -0700511 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800512 mRefMessage = savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700513 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700514
515 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700516 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700517 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700518 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
519 // Initialize the message from the message in the intent
Tony Mantler581edd42014-02-18 15:41:22 -0800520 message = intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700521 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800522 mRefMessage = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
523 mRefMessageUri = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
524 quotedText = null;
Andy Huang4fe0af82013-08-20 17:24:51 -0700525
526 if (Analytics.isLoggable()) {
527 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
528 Analytics.getInstance().sendEvent(
529 "notification_action", "compose", getActionString(action), 0);
530 }
531 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700532 }
Mark Wei62066e42012-09-13 12:07:02 -0700533 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700534
535 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800536 if (mAccount == null) {
537 return;
538 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700539
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700540 mUseNewChips = shouldUseNewChips();
Scott Kennedyfe853d32013-06-19 11:47:35 -0700541 initRecipients();
542
Scott Kennedy5680ec22013-01-07 13:15:20 -0800543 // Clear the notification and mark the conversation as seen, if necessary
544 final Folder notificationFolder =
545 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800546
Alan Laue806c942014-06-06 16:19:15 -0700547 if (notificationFolder != null) {
548 final Uri conversationUri = intent.getParcelableExtra(EXTRA_NOTIFICATION_CONVERSATION);
549 Intent actionIntent;
550 if (conversationUri != null) {
551 actionIntent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS_WEAR);
552 actionIntent.putExtra(Utils.EXTRA_CONVERSATION, conversationUri);
553 } else {
554 actionIntent = new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
555 actionIntent.setData(Utils.appendVersionQueryParameter(this,
556 notificationFolder.folderUri.fullUri));
557 }
558 actionIntent.setPackage(getPackageName());
559 actionIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
560 actionIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
561
562 startService(actionIntent);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800563 }
564
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700565 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
566 mLaunchedFromEmail = true;
567 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
568 final Uri dataUri = intent.getData();
569 if (dataUri != null) {
570 final String dataScheme = intent.getData().getScheme();
571 final String accountScheme = mAccount.composeIntentUri.getScheme();
572 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
573 }
574 }
575
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700576 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700577 mShowQuotedText = true;
578 mComposeMode = action;
Alan Lau15490232014-03-06 14:53:14 -0800579
580 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Alan Lau575255c2014-05-16 11:44:27 -0700581 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
Alan Lau439aa5d2014-05-27 17:57:13 -0700582 String wearReply = null;
Alan Lau575255c2014-05-16 11:44:27 -0700583 if (remoteInput != null) {
Alan Lau439aa5d2014-05-27 17:57:13 -0700584 LogUtils.d(LOG_TAG, "Got remote input from new api");
585 CharSequence input = remoteInput.getCharSequence(
Alan Lau575255c2014-05-16 11:44:27 -0700586 NotificationActionUtils.WEAR_REPLY_INPUT);
Alan Lau439aa5d2014-05-27 17:57:13 -0700587 if (input != null) {
588 wearReply = input.toString();
Alan Lau15490232014-03-06 14:53:14 -0800589 }
Alan Lau575255c2014-05-16 11:44:27 -0700590 } else {
Alan Lau439aa5d2014-05-27 17:57:13 -0700591 // TODO: remove after legacy code has been removed.
592 LogUtils.d(LOG_TAG,
593 "No remote input from new api, falling back to compatibility mode");
594 ClipData clipData = intent.getClipData();
595 if (clipData != null
596 && LEGACY_WEAR_EXTRA.equals(clipData.getDescription().getLabel())) {
597 Bundle extras = clipData.getItemAt(0).getIntent().getExtras();
598 if (extras != null) {
599 wearReply = extras.getString(NotificationActionUtils.WEAR_REPLY_INPUT);
600 }
601 }
602 }
603
604 if (!TextUtils.isEmpty(wearReply)) {
605 createWearReplyTask(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION,
606 mComposeMode, wearReply).execute();
607 finish();
608 return;
609 } else {
610 LogUtils.w(LOG_TAG, "remote input string is null");
Alan Lau15490232014-03-06 14:53:14 -0800611 }
612 }
613
Alice Yanga990a712013-03-13 18:37:00 -0700614 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700615 return;
616 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700617 initFromDraftMessage(message);
618 initQuotedTextFromRefMessage(mRefMessage, action);
Alice Yanga990a712013-03-13 18:37:00 -0700619 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700620 // if we should be showing quoted text but mRefMessage is null
621 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700622 if (mShowQuotedText && mRefMessage == null) {
623 if (quotedText != null) {
624 initQuotedText(quotedText, false /* shouldQuoteText */);
625 } else if (mExtraValues != null) {
626 initExtraValues(mExtraValues);
627 return;
628 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700629 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700630 } else if (action == EDIT_DRAFT) {
Tony Mantler581edd42014-02-18 15:41:22 -0800631 if (message == null) {
632 throw new IllegalStateException("Message must not be null to edit draft");
633 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700634 initFromDraftMessage(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700635 // Update the action to the draft type of the previous draft
636 switch (message.draftType) {
637 case UIProvider.DraftType.REPLY:
638 action = REPLY;
639 break;
640 case UIProvider.DraftType.REPLY_ALL:
641 action = REPLY_ALL;
642 break;
643 case UIProvider.DraftType.FORWARD:
644 action = FORWARD;
645 break;
646 case UIProvider.DraftType.COMPOSE:
647 default:
648 action = COMPOSE;
649 break;
650 }
Alice Yanga990a712013-03-13 18:37:00 -0700651 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
652
653 mShowQuotedText = message.appendRefMessageContent;
654 if (message.refMessageUri != null) {
655 // If we're editing an existing draft that was in reference to an existing message,
656 // still need to load that original message since we might need to refer to the
657 // original sender and recipients if user switches "reply <-> reply-all".
658 mRefMessageUri = message.refMessageUri;
659 mComposeMode = action;
660 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
661 return;
662 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700663 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
664 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800665 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700666 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700667 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700668 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700669 if (initFromExtras(intent)) {
670 return;
671 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700672 }
Alice Yanga990a712013-03-13 18:37:00 -0700673
674 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700675 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700676 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700677
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -0700678 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
Alan Lau15490232014-03-06 14:53:14 -0800679 private static AsyncTask<Void, Void, Message> createWearReplyTask(
680 final ComposeActivity composeActivity,
681 final Uri refMessageUri, final String[] projection, final int action,
682 final String wearReply) {
683 return new AsyncTask<Void, Void, Message>() {
684 private Intent mEmptyServiceIntent = new Intent(composeActivity, EmptyService.class);
685
686 @Override
687 protected void onPreExecute() {
688 // Start service so we won't be killed if this app is put in the background.
689 composeActivity.startService(mEmptyServiceIntent);
690 }
691
692 @Override
693 protected Message doInBackground(Void... params) {
694 Cursor cursor = composeActivity.getContentResolver()
695 .query(refMessageUri, projection, null, null, null, null);
696 if (cursor != null) {
697 try {
698 cursor.moveToFirst();
699 return new Message(cursor);
700 } finally {
701 cursor.close();
702 }
703 }
704 return null;
705 }
706
707 @Override
708 protected void onPostExecute(Message message) {
709 composeActivity.stopService(mEmptyServiceIntent);
710
711 composeActivity.mRefMessage = message;
712 composeActivity.initFromRefMessage(action);
713 composeActivity.setBody(wearReply, false);
714 composeActivity.finishSetup(action, composeActivity.getIntent(), null);
715 composeActivity.sendOrSaveWithSanityChecks(false /* save */, true /* show toast */,
716 false /* orientationChanged */, true /* autoSend */);
717 }
718 };
719 }
720
Mindy Pereirab199d172012-08-13 11:04:03 -0700721 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700722 final Account[] allAccounts = AccountUtils.getAccounts(this);
723 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700724 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
725 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700726 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700727 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
728 }
729 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700730 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700731 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700732 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700733 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700734 anySyncing = true;
735 break;
736 }
737 }
738 if (!anySyncing) {
739 // There are accounts, but none are sync'd, which is just like having no accounts.
740 mAccounts = null;
741 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
742 return;
743 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700744 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700745 finishCreate();
746 }
747 }
748
Mindy Pereira47d0e652012-07-23 09:45:07 -0700749 private Account obtainAccount(Intent intent) {
750 Account account = null;
751 Object accountExtra = null;
752 if (intent != null && intent.getExtras() != null) {
753 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
754 if (accountExtra instanceof Account) {
755 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700756 } else if (accountExtra instanceof String) {
757 // This is the Account attached to the widget compose intent.
Tony Mantler26a20752014-02-28 16:44:24 -0800758 account = Account.newInstance((String) accountExtra);
mindyp7ae042e2012-08-27 13:27:37 -0700759 if (account != null) {
760 return account;
761 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700762 }
mindyp5ee9dc42013-01-08 09:54:54 -0800763 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
764 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
765 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700766 }
Tony Mantler581edd42014-02-18 15:41:22 -0800767
768 MailAppProvider provider = MailAppProvider.getInstance();
769 String lastAccountUri = provider.getLastSentFromAccount();
770 if (TextUtils.isEmpty(lastAccountUri)) {
771 lastAccountUri = provider.getLastViewedAccount();
Mindy Pereira47d0e652012-07-23 09:45:07 -0700772 }
Tony Mantler581edd42014-02-18 15:41:22 -0800773 if (!TextUtils.isEmpty(lastAccountUri)) {
774 accountExtra = Uri.parse(lastAccountUri);
775 }
776
Mindy Pereirab199d172012-08-13 11:04:03 -0700777 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700778 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
779 // For backwards compatibility, we need to check account
780 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700781 for (Account a : mAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -0700782 if (a.getEmailAddress().equals(accountExtra)) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700783 account = a;
784 }
785 }
786 } else if (accountExtra instanceof Uri) {
787 // The uri of the last viewed account is what is stored in
788 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700789 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700790 if (a.uri.equals(accountExtra)) {
791 account = a;
792 }
793 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700794 }
795 if (account == null) {
796 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700797 }
798 }
799 return account;
800 }
801
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700802 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800803 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700804 // Don't bother with the intent if we have procured a message from the
805 // intent already.
806 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
807 initAttachmentsFromIntent(intent);
808 }
Alice Yanga990a712013-03-13 18:37:00 -0700809 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700810 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
811 action);
mindypd4a48662012-11-08 17:13:49 -0800812
813 // If this is a draft message, the draft account is whatever account was
814 // used to open the draft message in Compose.
815 if (mDraft != null) {
816 mDraftAccount = mReplyFromAccount;
817 }
818
Mindy Pereira75f66632012-01-11 11:42:02 -0800819 initChangeListeners();
Jin Cao32973b42014-05-06 16:12:11 -0700820
821 // These two should be identical since we check CC and BCC the same way
822 boolean showCc = !TextUtils.isEmpty(mCc.getText()) || (savedInstanceState != null &&
823 savedInstanceState.getBoolean(EXTRA_SHOW_CC));
824 boolean showBcc = !TextUtils.isEmpty(mBcc.getText()) || (savedInstanceState != null &&
825 savedInstanceState.getBoolean(EXTRA_SHOW_BCC));
826 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereira326689d2012-05-17 10:14:14 -0700827 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700828 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800829
Tony Mantler581edd42014-02-18 15:41:22 -0800830 mRespondedInline = mInnerSavedState != null &&
831 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE);
mindyp1623f9b2012-11-21 12:41:16 -0800832 if (mRespondedInline) {
833 mQuotedTextView.setVisibility(View.GONE);
834 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700835 }
836
Scott Kennedyff8553f2013-04-05 20:57:44 -0700837 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700838 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
839 }
840
Mindy Pereira71c9e562012-05-17 11:01:02 -0700841 private void updateHideOrShowQuotedText(boolean showQuotedText) {
842 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700843 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700844 }
845
846 private void setFocus(int action) {
847 if (action == EDIT_DRAFT) {
848 int type = mDraft.draftType;
849 switch (type) {
850 case UIProvider.DraftType.COMPOSE:
851 case UIProvider.DraftType.FORWARD:
852 action = COMPOSE;
853 break;
854 case UIProvider.DraftType.REPLY:
855 case UIProvider.DraftType.REPLY_ALL:
856 default:
857 action = REPLY;
858 break;
859 }
860 }
861 switch (action) {
862 case FORWARD:
863 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800864 if (TextUtils.isEmpty(mTo.getText())) {
865 mTo.requestFocus();
866 break;
867 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700868 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700869 case REPLY:
870 case REPLY_ALL:
871 default:
872 focusBody();
873 break;
874 }
875 }
876
877 /**
878 * Focus the body of the message.
879 */
Tony Mantler6a7ac782014-02-19 15:22:02 -0800880 private void focusBody() {
Mindy Pereira433b1982012-04-03 11:53:07 -0700881 mBodyView.requestFocus();
Tony Mantler6a7ac782014-02-19 15:22:02 -0800882 resetBodySelection();
883 }
Mindy Pereira433b1982012-04-03 11:53:07 -0700884
Tony Mantler6a7ac782014-02-19 15:22:02 -0800885 private void resetBodySelection() {
886 int length = mBodyView.getText().length();
Mindy Pereira433b1982012-04-03 11:53:07 -0700887 int signatureStartPos = getSignatureStartPosition(
888 mSignature, mBodyView.getText().toString());
889 if (signatureStartPos > -1) {
890 // In case the user deleted the newlines...
891 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700892 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700893 // Move cursor to the end.
894 mBodyView.setSelection(length);
895 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800896 }
897
898 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700899 protected void onStart() {
900 super.onStart();
901
902 Analytics.getInstance().activityStart(this);
903 }
904
905 @Override
906 protected void onStop() {
907 super.onStop();
908
909 Analytics.getInstance().activityStop(this);
910 }
911
912 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800913 protected void onResume() {
914 super.onResume();
915 // Update the from spinner as other accounts
916 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800917 if (mFromSpinner != null && mAccount != null) {
Andrew Sappersteina01ddca2014-03-04 10:59:56 -0800918 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800919 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800920 }
921
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800922 @Override
923 protected void onPause() {
924 super.onPause();
925
Mindy Pereiraa2148332012-07-02 13:54:14 -0700926 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700927 // Don't save unnecessary drafts if we are only changing the orientation.
928 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700929 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700930
Andy Huange003b4c2013-08-16 10:32:05 -0700931 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700932 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
933 // because that method can be invoked many times in a single compose session.)
934 logSendOrSave(true /* save */);
935 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700936 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800937 }
938
939 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700940 protected void onActivityResult(int request, int result, Intent data) {
Andy Huang5f082212014-06-11 22:19:21 -0700941 if (request == RESULT_PICK_ATTACHMENT) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700942 mAddingAttachment = false;
Andy Huang5f082212014-06-11 22:19:21 -0700943 if (result == RESULT_OK) {
944 addAttachmentAndUpdateView(data);
945 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700946 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700947 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700948 if (result != RESULT_OK) {
949 finish();
950 } else {
951 // Watch for accounts to show up!
952 // restart the loader to get the updated list of accounts
953 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
954 showWaitFragment(null);
955 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800956 }
957 }
958
959 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700960 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700961 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
962 if (hasAccounts) {
963 clearChangeListeners();
964 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700965 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700966 if (mInnerSavedState != null) {
967 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
968 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
969 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700970 // There should be a focus and it should be an EditText since we
971 // only save these extras if these conditions are true.
972 EditText focusEditText = (EditText) getCurrentFocus();
973 final int length = focusEditText.getText().length();
974 if (selectionStart < length && selectionEnd < length) {
975 focusEditText.setSelection(selectionStart, selectionEnd);
976 }
977 }
978 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700979 if (hasAccounts) {
980 initChangeListeners();
981 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700982 }
983
984 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700985 protected void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800986 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700987 final Bundle inner = new Bundle();
988 saveState(inner);
989 state.putBundle(KEY_INNER_SAVED_STATE, inner);
990 }
991
992 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700993 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
994 if (mAccounts == null || mAccounts.length == 0) {
995 return;
996 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700997 // The framework is happy to save and restore the selection but only if it also saves and
998 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
999 // this manually.
1000 View focus = getCurrentFocus();
1001 if (focus != null && focus instanceof EditText) {
1002 EditText focusEditText = (EditText) focus;
1003 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
1004 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
1005 }
Paul Westbrook6273e962012-04-23 10:44:15 -07001006
1007 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -07001008 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -07001009 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
1010 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
1011 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001012 if (selectedReplyFromAccount != null) {
1013 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
1014 .toString());
1015 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
1016 } else {
1017 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
1018 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001019
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001020 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
1021 // We don't have a draft id, and we have a request id,
1022 // save the request id.
1023 state.putInt(EXTRA_REQUEST_ID, mRequestId);
1024 }
1025
1026 // We want to restore the current mode after a pause
1027 // or rotation.
1028 int mode = getMode();
1029 state.putInt(EXTRA_ACTION, mode);
1030
Jin Cao77b4c2c2014-05-20 13:55:53 -07001031 final Message message = createMessage(selectedReplyFromAccount, mRefMessage, mode,
1032 removeComposingSpans(mBodyView.getText()));
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001033 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -08001034 message.id = mDraft.id;
1035 message.serverId = mDraft.serverId;
1036 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001037 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001038 state.putParcelable(EXTRA_MESSAGE, message);
1039
1040 if (mRefMessage != null) {
1041 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001042 } else if (message.appendRefMessageContent) {
1043 // If we have no ref message but should be appending
1044 // ref message content, we have orphaned quoted text. Save it.
1045 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001046 }
Mindy Pereira326689d2012-05-17 10:14:14 -07001047 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
1048 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -08001049 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -08001050 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -07001051 state.putParcelableArrayList(
1052 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -07001053
1054 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001055 }
1056
1057 private int getMode() {
1058 int mode = ComposeActivity.COMPOSE;
1059 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001060 if (actionBar != null
1061 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001062 mode = actionBar.getSelectedNavigationIndex();
1063 }
1064 return mode;
1065 }
1066
Jin Cao77b4c2c2014-05-20 13:55:53 -07001067 /**
1068 * This function might be called from a background thread, so be sure to move everything that
1069 * can potentially modify the UI to the main thread (e.g. removeComposingSpans for body).
1070 */
Anthony Lee2a3cc132014-04-22 14:15:25 -07001071 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, Message refMessage,
Jin Cao77b4c2c2014-05-20 13:55:53 -07001072 int mode, Spanned body) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001073 Message message = new Message();
1074 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -07001075 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001076 message.uri = null;
1077 message.conversationUri = null;
1078 message.subject = mSubject.getText().toString();
1079 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001080 message.setTo(formatSenders(mTo.getText().toString()));
1081 message.setCc(formatSenders(mCc.getText().toString()));
1082 message.setBcc(formatSenders(mBcc.getText().toString()));
1083 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001084 message.dateReceivedMs = 0;
Jin Cao77b4c2c2014-05-20 13:55:53 -07001085 message.bodyHtml = spannedBodyToHtml(body, true);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001086 message.bodyText = mBodyView.getText().toString();
1087 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -07001088 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001089 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
1090 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
1091 message.hasAttachments = attachments != null && attachments.size() > 0;
1092 message.attachmentListUri = null;
1093 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001094 message.alwaysShowImages = false;
1095 message.attachmentsJson = Attachment.toJSONArray(attachments);
1096 CharSequence quotedText = mQuotedTextView.getQuotedText();
Anthony Lee2a3cc132014-04-22 14:15:25 -07001097 message.quotedTextOffset = -1; // Just a default value.
1098 if (refMessage != null && !TextUtils.isEmpty(quotedText)) {
1099 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1100 // We want the index to point to just the quoted text and not the
1101 // "On December 25, 2014..." part of it.
1102 message.quotedTextOffset =
1103 QuotedTextView.getQuotedTextOffset(quotedText.toString());
1104 } else if (!TextUtils.isEmpty(refMessage.bodyText)) {
1105 // We want to point to the entire quoted text.
1106 message.quotedTextOffset = QuotedTextView.findQuotedTextIndex(quotedText);
1107 }
1108 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001109 message.accountUri = null;
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001110 final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
1111 : mAccount != null ? mAccount.getEmailAddress() : null;
Tony Mantlerf441d142013-10-22 11:46:00 -07001112 final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
1113 : mAccount != null ? mAccount.getSenderName() : null;
Tony Mantler821e5782014-01-06 15:33:43 -08001114 final Address address = new Address(email, senderName);
Tony Mantlerf441d142013-10-22 11:46:00 -07001115 message.setFrom(address.toHeader());
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001116 message.draftType = getDraftType(mode);
mindype7b76aa2012-11-14 16:19:13 -08001117 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001118 }
1119
Scott Kennedyff8553f2013-04-05 20:57:44 -07001120 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -07001121 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
1122 return string.substring(0, string.length() - 1);
1123 }
1124 return string;
1125 }
1126
Mindy Pereira818143e2012-01-11 13:59:49 -08001127 @VisibleForTesting
Andy Huang91ede362014-01-21 19:16:00 -08001128 protected void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -07001129 if (account == null) {
1130 return;
1131 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001132 if (!account.equals(mAccount)) {
1133 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001134 mCachedSettings = mAccount.settings;
1135 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001136 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001137 if (mAccount != null) {
Tony Mantler79b11562013-10-09 15:31:50 -07001138 MailActivity.setNfcMessage(mAccount.getEmailAddress());
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001139 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001140 }
1141
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001142 private void initFromSpinner(Bundle bundle, int action) {
1143 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001144 action = COMPOSE;
1145 }
Andrew Sappersteina01ddca2014-03-04 10:59:56 -08001146 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001147
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001148 if (bundle != null) {
1149 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
1150 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
1151 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
1152 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001153 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001154 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
1155 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001156 }
1157 if (mReplyFromAccount == null) {
1158 if (mDraft != null) {
1159 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
1160 } else if (mRefMessage != null) {
1161 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
1162 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001163 }
1164 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -07001165 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001166 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001167
Mindy Pereira62de1b12012-04-06 12:17:56 -07001168 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001169
Mindy Pereira62de1b12012-04-06 12:17:56 -07001170 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001171 // If there is only 1 account, just show that account.
1172 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001173 // send mail from / save drafts to.
1174 mFromStatic.setVisibility(View.GONE);
Andy Huangca4676f2014-01-16 13:22:20 -08001175 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001176 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001177 } else {
1178 mFromStatic.setVisibility(View.VISIBLE);
Andy Huangca4676f2014-01-16 13:22:20 -08001179 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001180 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001181 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001182 }
1183
Mindy Pereira62de1b12012-04-06 12:17:56 -07001184 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1185 if (refMessage.accountUri != null) {
1186 // This must be from combined inbox.
1187 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1188 for (ReplyFromAccount from : replyFromAccounts) {
1189 if (from.account.uri.equals(refMessage.accountUri)) {
1190 return from;
1191 }
1192 }
1193 return null;
1194 } else {
1195 return getReplyFromAccount(account, refMessage);
1196 }
1197 }
1198
1199 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001200 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001201 * return who the message should be sent from.
1202 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001203 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001204 * @return the address from which to reply.
1205 */
1206 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1207 // First see if we are supposed to use the default address or
1208 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001209 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001210 return getDefaultReplyFromAccount(account);
1211 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001212 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001213 // all the message recipients and find one that matches
1214 // a custom from or account.
1215 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001216 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1217 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001218 return getMatchingRecipient(account, allRecipients);
1219 }
1220 }
1221
1222 /**
1223 * Compare all the recipients of an email to the current account and all
1224 * custom addresses associated with that account. Return the match if there
1225 * is one, or the default account if there isn't.
1226 */
1227 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1228 // Tokenize the list and place in a hashmap.
1229 ReplyFromAccount matchingReplyFrom = null;
1230 Rfc822Token[] tokens;
1231 HashSet<String> recipientsMap = new HashSet<String>();
1232 for (String address : sentTo) {
1233 tokens = Rfc822Tokenizer.tokenize(address);
Tony Mantler581edd42014-02-18 15:41:22 -08001234 for (final Rfc822Token token : tokens) {
1235 recipientsMap.add(token.getAddress());
Mindy Pereira62de1b12012-04-06 12:17:56 -07001236 }
1237 }
1238
1239 int matchingAddressCount = 0;
1240 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001241 customFroms = account.getReplyFroms();
1242 if (customFroms != null) {
1243 for (ReplyFromAccount entry : customFroms) {
1244 if (recipientsMap.contains(entry.address)) {
1245 matchingReplyFrom = entry;
1246 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001247 }
1248 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001249 }
1250 if (matchingAddressCount > 1) {
1251 matchingReplyFrom = getDefaultReplyFromAccount(account);
1252 }
1253 return matchingReplyFrom;
1254 }
1255
Scott Kennedyff8553f2013-04-05 20:57:44 -07001256 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1257 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001258 if (from.isDefault) {
1259 return from;
1260 }
1261 }
Tony Mantlerf441d142013-10-22 11:46:00 -07001262 return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1263 account.getSenderName(), account.getEmailAddress(), true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001264 }
1265
Tony Mantlerf441d142013-10-22 11:46:00 -07001266 private ReplyFromAccount getReplyFromAccountFromDraft(final Account account,
1267 final Message msg) {
1268 final Address[] draftFroms = Address.parse(msg.getFrom());
1269 final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
Mindy Pereira62de1b12012-04-06 12:17:56 -07001270 ReplyFromAccount replyFromAccount = null;
1271 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Tony Mantler79b11562013-10-09 15:31:50 -07001272 if (TextUtils.equals(account.getEmailAddress(), sender)) {
Tony Mantlerf441d142013-10-22 11:46:00 -07001273 replyFromAccount = getDefaultReplyFromAccount(account);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001274 } else {
1275 for (ReplyFromAccount fromAccount : replyFromAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -07001276 if (TextUtils.equals(fromAccount.address, sender)) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001277 replyFromAccount = fromAccount;
1278 break;
1279 }
1280 }
1281 }
1282 return replyFromAccount;
1283 }
1284
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001285 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001286 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001287 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001288 if (mCcBccButton != null) {
1289 mCcBccButton.setOnClickListener(this);
1290 }
1291 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001292 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Tony Mantler581edd42014-02-18 15:41:22 -08001293 final View addAttachmentsButton = findViewById(R.id.add_attachment);
1294 if (addAttachmentsButton != null) {
1295 addAttachmentsButton.setOnClickListener(this);
mindypcd0b0b92012-08-23 14:33:17 -07001296 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001297 mTo = (RecipientEditTextView) findViewById(R.id.to);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001298 initializeRecipientEditTextView(mTo);
Mindy Pereira818143e2012-01-11 13:59:49 -08001299 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001300 initializeRecipientEditTextView(mCc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001301 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001302 initializeRecipientEditTextView(mBcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001303 // TODO: add special chips text change watchers before adding
1304 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001305 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001306 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001307 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1308 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001309 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001310 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001311 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001312 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001313 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001314 }
1315
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001316 private void initializeRecipientEditTextView(RecipientEditTextView view) {
1317 view.setTokenizer(new Rfc822Tokenizer());
1318 view.setThreshold(COMPLETION_THRESHOLD);
1319 }
1320
mindyp62d3ec72012-08-24 13:04:09 -07001321 @Override
1322 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1323 if (action == EditorInfo.IME_ACTION_DONE) {
1324 focusBody();
1325 return true;
1326 }
1327 return false;
1328 }
1329
Andy Huang91ede362014-01-21 19:16:00 -08001330 /**
1331 * Convert the body text (in {@link Spanned} form) to ready-to-send HTML format as a plain
1332 * String.
1333 *
1334 * @param body the body text including fancy style spans
Jin Cao77b4c2c2014-05-20 13:55:53 -07001335 * @param removedComposing whether the function already removed composingSpans. Necessary
1336 * because we cannot call removeComposingSpans from a background thread.
Andy Huang91ede362014-01-21 19:16:00 -08001337 * @return HTML formatted body that's suitable for sending or saving
1338 */
Jin Cao77b4c2c2014-05-20 13:55:53 -07001339 private String spannedBodyToHtml(Spanned body, boolean removedComposing) {
1340 if (!removedComposing) {
1341 body = removeComposingSpans(body);
1342 }
1343 final HtmlifyBeginResult r = onHtmlifyBegin(body);
Andy Huang91ede362014-01-21 19:16:00 -08001344 return onHtmlifyEnd(Html.toHtml(r.result), r.extras);
1345 }
1346
1347 /**
1348 * A hook for subclasses to convert custom spans in the body text prior to system HTML
1349 * conversion. That HTML conversion is lossy, so anything above and beyond its capability
1350 * has to be handled here.
1351 *
1352 * @param body
1353 * @return a copy of the body text with custom spans replaced with HTML
1354 */
1355 protected HtmlifyBeginResult onHtmlifyBegin(Spanned body) {
1356 return new HtmlifyBeginResult(body, null /* extras */);
1357 }
1358
1359 protected String onHtmlifyEnd(String html, Object extras) {
1360 return html;
1361 }
1362
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001363 protected TextView getBody() {
1364 return mBodyView;
1365 }
1366
1367 @VisibleForTesting
Andy Huang0a2a3462013-12-20 15:56:13 -08001368 public String getBodyHtml() {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001369 return spannedBodyToHtml(mBodyView.getText(), false);
Andy Huang0a2a3462013-12-20 15:56:13 -08001370 }
1371
1372 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001373 public Account getFromAccount() {
1374 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1375 mReplyFromAccount.account : mAccount;
1376 }
1377
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001378 private void clearChangeListeners() {
1379 mSubject.removeTextChangedListener(this);
1380 mBodyView.removeTextChangedListener(this);
1381 mTo.removeTextChangedListener(mToListener);
1382 mCc.removeTextChangedListener(mCcListener);
1383 mBcc.removeTextChangedListener(mBccListener);
1384 mFromSpinner.setOnAccountChangedListener(null);
1385 mAttachmentsView.setAttachmentChangesListener(null);
1386 }
1387
Mindy Pereira75f66632012-01-11 11:42:02 -08001388 // Now that the message has been initialized from any existing draft or
1389 // ref message data, set up listeners for any changes that occur to the
1390 // message.
1391 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001392 // Make sure we only add text changed listeners once!
1393 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001394 mSubject.addTextChangedListener(this);
1395 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001396 if (mToListener == null) {
1397 mToListener = new RecipientTextWatcher(mTo, this);
1398 }
1399 mTo.addTextChangedListener(mToListener);
1400 if (mCcListener == null) {
1401 mCcListener = new RecipientTextWatcher(mCc, this);
1402 }
1403 mCc.addTextChangedListener(mCcListener);
1404 if (mBccListener == null) {
1405 mBccListener = new RecipientTextWatcher(mBcc, this);
1406 }
1407 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001408 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001409 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001410 }
1411
Alice Yanga990a712013-03-13 18:37:00 -07001412 private void initActionBar() {
1413 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001414 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001415 if (actionBar == null) {
1416 return;
1417 }
Alice Yanga990a712013-03-13 18:37:00 -07001418 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001419 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1420 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001421 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001422 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001423 if (mComposeModeAdapter == null) {
1424 mComposeModeAdapter = new ComposeModeAdapter(this);
1425 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001426 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1427 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001428 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001429 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001430 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001431 break;
1432 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001433 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001434 break;
1435 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001436 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001437 break;
1438 }
1439 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001440 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1441 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1442 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001443 }
1444
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001445 private void initFromRefMessage(int action) {
1446 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001447
1448 // Check if To: address and email body needs to be prefilled based on extras.
1449 // This is used for reporting rendering feedback.
1450 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1451 Intent intent = getIntent();
1452 if (intent.getExtras() != null) {
1453 String toAddresses = intent.getStringExtra(EXTRA_TO);
1454 if (toAddresses != null) {
1455 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1456 }
1457 String body = intent.getStringExtra(EXTRA_BODY);
1458 if (body != null) {
1459 setBody(body, false /* withSignature */);
1460 }
1461 }
1462 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001463 }
1464
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001465 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001466 setSubject(mRefMessage, action);
1467 // Setup recipients
1468 if (action == FORWARD) {
1469 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001470 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001471 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001472 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001473 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1474 initAttachments(mRefMessage);
1475 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001476 }
1477
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001478 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001479 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001480
1481 mDraft = message;
1482 mDraftId = message.id;
1483 mSubject.setText(message.subject);
1484 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001485 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001486 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001487 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1488 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001489 if (message.hasAttachments) {
1490 List<Attachment> attachments = message.getAttachments();
1491 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001492 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001493 }
1494 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07001495 int quotedTextIndex = message.appendRefMessageContent ? message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001496 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001497 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001498 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira002ff522012-05-30 10:31:26 -07001499 if (quotedTextIndex > -1) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07001500 // Find the offset in the html text of the actual quoted text and strip it out.
1501 // Note that the actual quotedTextOffset in the message has not changed as
1502 // this different offset is used only for display purposes. They point to different
1503 // parts of the original message. Please see the comments in QuoteTextView
1504 // to see the differences.
Mindy Pereira752222d2012-07-19 09:58:53 -07001505 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1506 if (quotedTextIndex > -1) {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001507 new HtmlToSpannedTask().execute(message.bodyHtml.substring(0, quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001508 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1509 message.bodyHtml.length());
1510 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001511 } else {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001512 new HtmlToSpannedTask().execute(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001513 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001514 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001515 final String body = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07001516 final CharSequence bodyText;
1517 if (TextUtils.isEmpty(body)) {
1518 bodyText = "";
1519 quotedText = null;
1520 } else {
1521 if (quotedTextIndex > body.length()) {
1522 // Sanity check to guarantee that we will not over index the String.
1523 // If this happens there is a bigger problem. This should never happen hence
1524 // the wtf logging.
1525 quotedTextIndex = -1;
1526 LogUtils.wtf(LOG_TAG, "quotedTextIndex (%d) > body.length() (%d)",
1527 quotedTextIndex, body.length());
1528 }
1529 bodyText = quotedTextIndex > -1 ? body.substring(0, quotedTextIndex) : body;
1530 if (quotedTextIndex > -1) {
1531 quotedText = body.substring(quotedTextIndex);
1532 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001533 }
1534 mBodyView.setText(bodyText);
1535 }
1536 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001537 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001538 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001539 }
1540
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001541 /**
1542 * Fill all the widgets with the content found in the Intent Extra, if any.
1543 * Also apply the same style to all widgets. Note: if initFromExtras is
1544 * called as a result of switching between reply, reply all, and forward per
1545 * the latest revision of Gmail, and the user has already made changes to
1546 * attachments on a previous incarnation of the message (as a reply, reply
1547 * all, or forward), the original attachments from the message will not be
1548 * re-instantiated. The user's changes will be respected. This follows the
1549 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001550 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001551 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001552 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001553 // If we were invoked with a SENDTO intent, the value
1554 // should take precedence
1555 final Uri dataUri = intent.getData();
1556 if (dataUri != null) {
1557 if (MAIL_TO.equals(dataUri.getScheme())) {
1558 initFromMailTo(dataUri.toString());
1559 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001560 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001561 String toText = dataUri.getSchemeSpecificPart();
1562 if (toText != null) {
1563 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001564 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001565 }
1566 }
1567 }
1568 }
1569
1570 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1571 if (extraStrings != null) {
1572 addToAddresses(Arrays.asList(extraStrings));
1573 }
1574 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1575 if (extraStrings != null) {
1576 addCcAddresses(Arrays.asList(extraStrings), null);
1577 }
1578 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1579 if (extraStrings != null) {
1580 addBccAddresses(Arrays.asList(extraStrings));
1581 }
1582
1583 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1584 if (extraString != null) {
1585 mSubject.setText(extraString);
1586 }
1587
1588 for (String extra : ALL_EXTRAS) {
1589 if (intent.hasExtra(extra)) {
1590 String value = intent.getStringExtra(extra);
1591 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001592 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001593 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001594 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001595 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001596 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001597 } else if (EXTRA_SUBJECT.equals(extra)) {
1598 mSubject.setText(value);
1599 } else if (EXTRA_BODY.equals(extra)) {
1600 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001601 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1602 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001603 }
1604 }
1605 }
1606
1607 Bundle extras = intent.getExtras();
1608 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001609 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1610 if (text != null) {
1611 setBody(text, true /* with signature */);
1612 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001613
1614 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001615 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001616
1617 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1618 if (mExtraValues != null) {
1619 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1620 initExtraValues(mExtraValues);
1621 return true;
1622 }
1623
1624 return false;
1625 }
1626
1627 protected void initExtraValues(ContentValues extraValues) {
1628 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001629 }
1630
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001631
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001632 @VisibleForTesting
1633 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001634 // TODO: handle the case where there are spaces in the display name as
1635 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1636 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001637 // Since URLDecode.decode changes + into ' ', and + is a valid
1638 // email character, we need to find/ replace these ourselves before
1639 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001640 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001641 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001642 } catch (IllegalArgumentException e) {
1643 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1644 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1645 } else {
1646 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1647 }
1648 return null;
1649 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001650 }
1651
1652 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001653 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1654 * changing '+' into ' '
1655 *
1656 * @param toReplace Input string
1657 * @return The string with all "+" characters replaced with "%2B"
1658 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001659 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001660 return toReplace.replace("+", "%2B");
1661 }
1662
1663 /**
Jin Caod67d7e32014-03-26 16:49:48 -07001664 * Replaces all occurrences of '%' with "%25", to prevent URLDecode.decode from
1665 * crashing on decoded '%' symbols
1666 *
1667 * @param toReplace Input string
1668 * @return The string with all "%" characters replaced with "%25"
1669 */
1670 private static String replacePercent(String toReplace) {
1671 return toReplace.replace("%", "%25");
1672 }
1673
1674 /**
1675 * Helper function to encapsulate encoding/decoding string from Uri.getQueryParameters
1676 * @param content Input string
1677 * @return The string that's properly escaped to be shown in mail subject/content
1678 */
1679 private static String decodeContentFromQueryParam(String content) {
1680 try {
1681 return URLDecoder.decode(replacePlus(replacePercent(content)), UTF8_ENCODING_NAME);
1682 } catch (UnsupportedEncodingException e) {
1683 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), content);
1684 return ""; // Default to empty string so setText/setBody has same behavior as before.
1685 }
1686 }
1687
1688 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001689 * Initialize the compose view from a String representing a mailTo uri.
1690 * @param mailToString The uri as a string.
1691 */
1692 public void initFromMailTo(String mailToString) {
1693 // We need to disguise this string as a URI in order to parse it
1694 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1695 Uri uri = Uri.parse("foo://" + mailToString);
1696 int index = mailToString.indexOf("?");
1697 int length = "mailto".length() + 1;
1698 String to;
1699 try {
1700 // Extract the recipient after mailto:
1701 if (index == -1) {
1702 to = decodeEmailInUri(mailToString.substring(length));
1703 } else {
1704 to = decodeEmailInUri(mailToString.substring(length, index));
1705 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001706 if (!TextUtils.isEmpty(to)) {
1707 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1708 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001709 } catch (UnsupportedEncodingException e) {
1710 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1711 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1712 } else {
1713 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1714 }
1715 }
1716
1717 List<String> cc = uri.getQueryParameters("cc");
1718 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1719
1720 List<String> otherTo = uri.getQueryParameters("to");
1721 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1722
1723 List<String> bcc = uri.getQueryParameters("bcc");
1724 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1725
Jin Caod67d7e32014-03-26 16:49:48 -07001726 // NOTE: Uri.getQueryParameters already decodes % encoded characters
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001727 List<String> subject = uri.getQueryParameters("subject");
1728 if (subject.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001729 mSubject.setText(decodeContentFromQueryParam(subject.get(0)));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001730 }
1731
1732 List<String> body = uri.getQueryParameters("body");
1733 if (body.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001734 setBody(decodeContentFromQueryParam(body.get(0)), true /* with signature */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001735 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001736 }
1737
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001738 @VisibleForTesting
1739 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001740 addAttachments(refMessage.getAttachments());
1741 }
1742
1743 public long addAttachments(List<Attachment> attachments) {
1744 long size = 0;
1745 AttachmentFailureException error = null;
1746 for (Attachment a : attachments) {
1747 try {
1748 size += mAttachmentsView.addAttachment(mAccount, a);
1749 } catch (AttachmentFailureException e) {
1750 error = e;
1751 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001752 }
Mark Wei434f2942012-08-24 11:54:02 -07001753 if (error != null) {
1754 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1755 if (attachments.size() > 1) {
1756 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1757 } else {
1758 showAttachmentTooBigToast(error.getErrorRes());
1759 }
1760 }
1761 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001762 }
1763
1764 /**
1765 * When an attachment is too large to be added to a message, show a toast.
1766 * This method also updates the position of the toast so that it is shown
1767 * clearly above they keyboard if it happens to be open.
1768 */
Mark Wei434f2942012-08-24 11:54:02 -07001769 private void showAttachmentTooBigToast(int errorRes) {
1770 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1771 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1772 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001773 }
1774
Mark Wei434f2942012-08-24 11:54:02 -07001775 private void showErrorToast(String message) {
1776 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1777 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001778 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1779 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1780 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001781 }
1782
Paul Westbrookf97588b2012-03-20 11:11:37 -07001783 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001784 Bundle extras = intent.getExtras();
1785 if (extras == null) {
1786 extras = Bundle.EMPTY;
1787 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001788 final String action = intent.getAction();
1789 if (!mAttachmentsChanged) {
1790 long totalSize = 0;
1791 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1792 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1793 for (String uriString : uris) {
1794 final Uri uri = Uri.parse(uriString);
1795 long size = 0;
1796 try {
Andy Huang91ede362014-01-21 19:16:00 -08001797 if (handleSpecialAttachmentUri(uri)) {
1798 continue;
1799 }
1800
Andy Huange003b4c2013-08-16 10:32:05 -07001801 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1802 size = mAttachmentsView.addAttachment(mAccount, a);
1803
1804 Analytics.getInstance().sendEvent("send_intent_attachment",
1805 Utils.normalizeMimeType(a.getContentType()), null, size);
1806
Paul Westbrookf97588b2012-03-20 11:11:37 -07001807 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001808 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001809 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001810 }
1811 totalSize += size;
1812 }
1813 }
mindyp9a9e8d62012-10-03 12:24:07 -07001814 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1815 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
Andy Huang91ede362014-01-21 19:16:00 -08001816 final ArrayList<Uri> uris = extras
mindyp9a9e8d62012-10-03 12:24:07 -07001817 .getParcelableArrayList(Intent.EXTRA_STREAM);
1818 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
Andy Huang91ede362014-01-21 19:16:00 -08001819 for (Uri uri : uris) {
Andy Huang1a438aa2014-06-03 19:04:06 -07001820 if (uri == null) {
Tony Mantler1877c5f2014-05-12 16:02:16 -07001821 continue;
1822 }
mindyp9a9e8d62012-10-03 12:24:07 -07001823 try {
Andy Huang91ede362014-01-21 19:16:00 -08001824 if (handleSpecialAttachmentUri(uri)) {
1825 continue;
1826 }
1827
1828 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
Andy Huange003b4c2013-08-16 10:32:05 -07001829 attachments.add(a);
1830
1831 Analytics.getInstance().sendEvent("send_intent_attachment",
1832 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1833
mindyp9a9e8d62012-10-03 12:24:07 -07001834 } catch (AttachmentFailureException e) {
1835 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1836 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1837 getApplicationContext(),
1838 mAccount.settings.getMaxAttachmentSize());
1839 showErrorToast(getString
1840 (R.string.generic_attachment_problem, maxSize));
1841 }
1842 }
1843 totalSize += addAttachments(attachments);
1844 } else {
Tony Mantler581edd42014-02-18 15:41:22 -08001845 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
Andy Huang1a438aa2014-06-03 19:04:06 -07001846 if (uri != null) {
Tony Mantler1877c5f2014-05-12 16:02:16 -07001847 long size = 0;
1848 try {
Andy Huang1a438aa2014-06-03 19:04:06 -07001849 if (!handleSpecialAttachmentUri(uri)) {
1850 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1851 size = mAttachmentsView.addAttachment(mAccount, a);
Andy Huange003b4c2013-08-16 10:32:05 -07001852
Andy Huang1a438aa2014-06-03 19:04:06 -07001853 Analytics.getInstance().sendEvent("send_intent_attachment",
1854 Utils.normalizeMimeType(a.getContentType()), null, size);
1855 }
Andy Huange003b4c2013-08-16 10:32:05 -07001856
Tony Mantler1877c5f2014-05-12 16:02:16 -07001857 } catch (AttachmentFailureException e) {
1858 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1859 showAttachmentTooBigToast(e.getErrorRes());
1860 }
1861 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001862 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001863 }
1864 }
1865
1866 if (totalSize > 0) {
1867 mAttachmentsChanged = true;
1868 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001869
1870 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1871 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001872 }
1873 }
1874 }
1875
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001876 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001877 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1878 mShowQuotedText = true;
1879 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001880
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001881 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1882 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001883 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1884 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001885 }
1886
1887 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001888 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001889 boolean ccVisible = mCcBccView.isCcVisible();
1890 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001891 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001892 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001893 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001894 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001895 : R.string.add_bcc_label));
1896 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001897 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001898 }
1899 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001900 }
1901
Mindy Pereira013194c2012-01-06 15:09:33 -08001902 /**
1903 * Add attachment and update the compose area appropriately.
Mindy Pereira013194c2012-01-06 15:09:33 -08001904 */
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001905 private void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07001906 if (data == null) {
1907 return;
1908 }
1909
1910 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1911 final ClipData clipData = data.getClipData();
1912 if (clipData != null) {
1913 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1914 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1915 }
1916 return;
1917 }
1918 }
1919
1920 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07001921 }
1922
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001923 private void addAttachmentAndUpdateView(Uri contentUri) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001924 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001925 return;
1926 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001927 try {
Andy Huang91ede362014-01-21 19:16:00 -08001928
1929 if (handleSpecialAttachmentUri(contentUri)) {
1930 return;
1931 }
1932
Andy Huang5c5fd572012-04-08 18:19:29 -07001933 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1934 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001935 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001936 showErrorToast(getResources().getString(
1937 e.getErrorRes(),
1938 AttachmentUtils.convertToHumanReadableSize(
1939 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001940 }
1941 }
1942
Andy Huang91ede362014-01-21 19:16:00 -08001943 /**
1944 * Allow subclasses to implement custom handling of attachments.
1945 *
1946 * @param contentUri a passed-in URI from a pick intent
1947 * @return true iff handled
1948 */
1949 protected boolean handleSpecialAttachmentUri(final Uri contentUri) {
1950 return false;
1951 }
1952
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001953 private void addAttachmentAndUpdateView(Attachment attachment) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001954 try {
Mark Wei434f2942012-08-24 11:54:02 -07001955 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001956 if (size > 0) {
1957 mAttachmentsChanged = true;
1958 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001959 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001960 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001961 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001962 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001963 }
1964 }
1965
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001966 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001967 // Don't populate the address if this is a forward.
1968 if (action == ComposeActivity.FORWARD) {
1969 return;
1970 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001971 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001972 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001973
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001974 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1975 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001976 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001977 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001978 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001979 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001980 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1981 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
Andy Huange2af8872014-01-16 12:36:27 -08001982 final String[] replyToAddresses = getReplyToAddresses(
1983 refMessage.getReplyToAddressesUnescaped(), fromAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001984
1985 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1986 // Cc list is the union of the To and Cc recipients of the original
1987 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001988 // already on the To list.
1989 if (action == ComposeActivity.REPLY) {
Tony Mantler24f116f2014-01-16 10:20:50 -08001990 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001991 addToAddresses(toAddresses);
1992 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001993 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08001994 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001995 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001996 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001997 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001998 addCcAddresses(ccAddresses, toAddresses);
1999 }
2000 }
2001
Andy Huange2af8872014-01-16 12:36:27 -08002002 // If there is no reply to address, the reply to address is the sender.
2003 private static String[] getReplyToAddresses(String[] replyTo, String from) {
2004 boolean hasReplyTo = false;
2005 for (final String replyToAddress : replyTo) {
2006 if (!TextUtils.isEmpty(replyToAddress)) {
2007 hasReplyTo = true;
2008 }
2009 }
2010 return hasReplyTo ? replyTo : new String[] {from};
2011 }
2012
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002013 private void addToAddresses(Collection<String> addresses) {
2014 addAddressesToList(addresses, mTo);
2015 }
2016
2017 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002018 addCcAddressesToList(tokenizeAddressList(addresses),
2019 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002020 }
2021
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07002022 private void addBccAddresses(Collection<String> addresses) {
2023 addAddressesToList(addresses, mBcc);
2024 }
2025
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002026 @VisibleForTesting
2027 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
2028 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
2029 String address;
2030
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002031 if (compareToList == null) {
Tony Mantler581edd42014-02-18 15:41:22 -08002032 for (final Rfc822Token[] tokens : addresses) {
2033 for (final Rfc822Token token : tokens) {
2034 address = token.toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002035 list.append(address + END_TOKEN);
2036 }
2037 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002038 } else {
2039 HashSet<String> compareTo = convertToHashSet(compareToList);
Tony Mantler581edd42014-02-18 15:41:22 -08002040 for (final Rfc822Token[] tokens : addresses) {
2041 for (final Rfc822Token token : tokens) {
2042 address = token.toString();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002043 // Check if this is a duplicate:
Tony Mantler581edd42014-02-18 15:41:22 -08002044 if (!compareTo.contains(token.getAddress())) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002045 // Get the address here
2046 list.append(address + END_TOKEN);
2047 }
2048 }
2049 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002050 }
2051 }
2052
Scott Kennedyff8553f2013-04-05 20:57:44 -07002053 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
2054 final HashSet<String> hash = new HashSet<String>();
2055 for (final Rfc822Token[] tokens : list) {
Tony Mantler581edd42014-02-18 15:41:22 -08002056 for (final Rfc822Token token : tokens) {
2057 hash.add(token.getAddress());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002058 }
2059 }
2060 return hash;
2061 }
2062
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002063 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
2064 @VisibleForTesting
2065 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
2066
2067 for (String address: addresses) {
2068 tokenized.add(Rfc822Tokenizer.tokenize(address));
2069 }
2070 return tokenized;
2071 }
2072
2073 @VisibleForTesting
2074 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
2075 for (String address : addresses) {
2076 addAddressToList(address, list);
2077 }
2078 }
2079
Scott Kennedyff8553f2013-04-05 20:57:44 -07002080 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002081 if (address == null || list == null)
2082 return;
2083
Scott Kennedyff8553f2013-04-05 20:57:44 -07002084 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002085
Tony Mantler581edd42014-02-18 15:41:22 -08002086 for (final Rfc822Token token : tokens) {
2087 list.append(token + END_TOKEN);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002088 }
2089 }
2090
2091 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07002092 protected Collection<String> initToRecipients(final String fullSenderAddress,
Tony Mantler24f116f2014-01-16 10:20:50 -08002093 final String[] replyToAddresses, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002094 // The To recipient is the reply-to address specified in the original
2095 // message, unless it is:
2096 // the current user OR a custom from of the current user, in which case
2097 // it's the To recipient list of the original message.
2098 // OR missing, in which case use the sender of the original message
2099 Set<String> toAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002100 for (final String replyToAddress : replyToAddresses) {
2101 if (!TextUtils.isEmpty(replyToAddress)
2102 && !recipientMatchesThisAccount(replyToAddress)) {
2103 toAddresses.add(replyToAddress);
2104 }
2105 }
2106 if (toAddresses.size() == 0) {
mindyp65b06f52012-11-21 10:35:08 -08002107 // In this case, the user is replying to a message in which their
Tony Mantler24f116f2014-01-16 10:20:50 -08002108 // current account or some of their custom from addresses are the only
2109 // recipients and they sent the original message.
mindyp65b06f52012-11-21 10:35:08 -08002110 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
2111 && recipientMatchesThisAccount(inToAddresses[0])) {
2112 toAddresses.add(inToAddresses[0]);
2113 return toAddresses;
2114 }
2115 // This happens if the user replies to a message they originally
2116 // wrote. In this case, "reply" really means "re-send," so we
2117 // target the original recipients. This works as expected even
2118 // if the user sent the original message to themselves.
2119 for (String address : inToAddresses) {
2120 if (!recipientMatchesThisAccount(address)) {
2121 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08002122 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07002123 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002124 }
2125 return toAddresses;
2126 }
2127
Scott Kennedyff8553f2013-04-05 20:57:44 -07002128 private void addRecipients(final Set<String> recipients, final String[] addresses) {
2129 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002130 // Do not add this account, or any of its custom from addresses, to
2131 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08002132 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08002133 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002134 recipients.add(email.replace("\"\"", ""));
2135 }
2136 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002137 }
2138
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002139 /**
2140 * A recipient matches this account if it has the same address as the
2141 * currently selected account OR one of the custom from addresses associated
2142 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002143 * @param recipientAddress address we are comparing with the currently selected account
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002144 */
mindyp5ee5d692012-11-19 16:02:16 -08002145 protected boolean recipientMatchesThisAccount(String recipientAddress) {
2146 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08002147 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002148 }
2149
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002150 /**
2151 * Returns a formatted subject string with the appropriate prefix for the action type.
2152 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
2153 */
Andrew Sapperstein7e04f142014-06-11 13:43:07 -07002154 public static String buildFormattedSubject(Resources res, String subject, int action) {
2155 String prefix;
2156 String correctedSubject = null;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002157 if (action == ComposeActivity.COMPOSE) {
2158 prefix = "";
2159 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002160 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002161 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002162 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002163 }
2164
2165 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07002166 if (!TextUtils.isEmpty(subject)
2167 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002168 correctedSubject = subject;
2169 } else {
Andrew Sapperstein7e04f142014-06-11 13:43:07 -07002170 correctedSubject = String.format(
2171 res.getString(R.string.formatted_subject), prefix, subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002172 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002173
2174 return correctedSubject;
2175 }
2176
2177 private void setSubject(Message refMessage, int action) {
2178 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002179 }
2180
Mindy Pereira818143e2012-01-11 13:59:49 -08002181 private void initRecipients() {
2182 setupRecipients(mTo);
2183 setupRecipients(mCc);
2184 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002185 }
2186
Mindy Pereira818143e2012-01-11 13:59:49 -08002187 private void setupRecipients(RecipientEditTextView view) {
Andrew Sapperstein47c30c22014-06-04 10:26:22 -07002188 // todo - remove this experiment
2189 if (LogUtils.isLoggable("NewChips", LogUtils.DEBUG) || mUseNewChips) {
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002190 final DropdownChipLayouter layouter = getDropdownChipLayouter();
2191 if (layouter != null) {
2192 view.setDropdownChipLayouter(layouter);
2193 }
2194 view.setAdapter(getRecipientAdapter());
2195 } else {
2196 view.setAdapter(new RecipientAdapter(this, mAccount));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002197 }
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002198 view.setRecipientEntryItemClickedListener(this);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002199 if (mValidator == null) {
Tony Mantler79b11562013-10-09 15:31:50 -07002200 final String accountName = mAccount.getEmailAddress();
Mindy Pereira33fe9082012-01-09 16:24:30 -08002201 int offset = accountName.indexOf("@") + 1;
2202 String account = accountName;
Tony Mantler79b11562013-10-09 15:31:50 -07002203 if (offset > 0) {
2204 account = account.substring(offset);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002205 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002206 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002207 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002208 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002209 }
2210
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002211 /**
2212 * Derived classes should override if they wish to provide their own autocomplete behavior.
2213 */
2214 public BaseRecipientAdapter getRecipientAdapter() {
2215 return new RecipientAdapter(this, mAccount);
2216 }
2217
2218 /**
2219 * Derived classes should override this to provide their own dropdown behavior.
2220 * If the result is null, the default {@link com.android.ex.chips.DropdownChipLayouter}
2221 * is used.
2222 */
2223 public DropdownChipLayouter getDropdownChipLayouter() {
2224 return null;
2225 }
2226
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002227 @Override
2228 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002229 final int id = v.getId();
2230 if (id == R.id.add_cc_bcc) {
2231 // Verify that cc/ bcc aren't showing.
2232 // Animate in cc/bcc.
2233 showCcBccViews();
Andrew Sapperstein6aea7862013-10-24 19:59:51 -07002234 } else if (id == R.id.add_attachment) {
2235 doAttach(Utils.isRunningKitkatOrLater() ? MIME_TYPE_ALL : MIME_TYPE_PHOTO);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002236 }
2237 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002238
2239 @Override
2240 public boolean onCreateOptionsMenu(Menu menu) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002241 final boolean superCreated = super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07002242 // Don't render any menu items when there are no accounts.
2243 if (mAccounts == null || mAccounts.length == 0) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002244 return superCreated;
Mindy Pereirab199d172012-08-13 11:04:03 -07002245 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002246 MenuInflater inflater = getMenuInflater();
2247 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08002248
2249 /*
2250 * Start save in the correct enabled state.
2251 * 1) If a user launches compose from within gmail, save is disabled
2252 * until they add something, at which point, save is enabled, auto save
2253 * on exit; if the user empties everything, save is disabled, exiting does not
2254 * auto-save
2255 * 2) if a user replies/ reply all/ forwards from within gmail, save is
2256 * disabled until they change something, at which point, save is
2257 * enabled, auto save on exit; if the user empties everything, save is
2258 * disabled, exiting does not auto-save.
2259 * 3) If a user launches compose from another application and something
2260 * gets populated (attachments, recipients, body, subject, etc), save is
2261 * enabled, auto save on exit; if the user empties everything, save is
2262 * disabled, exiting does not auto-save
2263 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08002264 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08002265 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07002266 enableSave(mInnerSavedState != null ?
2267 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08002268 : (Intent.ACTION_SEND.equals(action)
2269 || Intent.ACTION_SEND_MULTIPLE.equals(action)
2270 || Intent.ACTION_SENDTO.equals(action)
2271 || shouldSave()));
2272
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07002273 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2274 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2275 if (helpItem != null) {
2276 helpItem.setVisible(mAccount != null
2277 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2278 }
2279 if (sendFeedbackItem != null) {
2280 sendFeedbackItem.setVisible(mAccount != null
2281 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2282 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002283
Andrew Sapperstein8809f9f2013-10-11 16:13:35 -07002284 // Show attach picture on pre-K devices.
2285 menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002286
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002287 return true;
2288 }
2289
2290 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002291 public boolean onPrepareOptionsMenu(Menu menu) {
2292 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08002293 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002294 // Its possible there is a menu item OR a button.
2295 boolean ccFieldVisible = mCc.isShown();
2296 boolean bccFieldVisible = mBcc.isShown();
2297 if (!ccFieldVisible || !bccFieldVisible) {
2298 ccBcc.setVisible(true);
2299 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
2300 : R.string.add_bcc_label));
2301 } else {
2302 ccBcc.setVisible(false);
2303 }
2304 }
2305 return true;
2306 }
2307
2308 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002309 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002310 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002311
Andy Huangf8c59b02014-03-19 20:00:53 -07002312 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id,
2313 "compose", 0);
Andy Huangdc97bf42013-08-15 16:52:45 -07002314
Mindy Pereira75f66632012-01-11 11:42:02 -08002315 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002316 if (id == R.id.add_file_attachment) {
2317 doAttach(MIME_TYPE_ALL);
2318 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002319 doAttach(MIME_TYPE_PHOTO);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002320 } else if (id == R.id.add_cc_bcc) {
2321 showCcBccViews();
2322 } else if (id == R.id.save) {
2323 doSave(true);
2324 } else if (id == R.id.send) {
2325 doSend();
2326 } else if (id == R.id.discard) {
2327 doDiscard();
2328 } else if (id == R.id.settings) {
2329 Utils.showSettings(this, mAccount);
2330 } else if (id == android.R.id.home) {
2331 onAppUpPressed();
2332 } else if (id == R.id.help_info_menu_item) {
2333 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2334 } else if (id == R.id.feedback_menu_item) {
2335 Utils.sendFeedback(this, mAccount, false);
2336 } else {
2337 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002338 }
Tony Mantler581edd42014-02-18 15:41:22 -08002339 return handled || super.onOptionsItemSelected(item);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002340 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002341
Mindy Pereirab199d172012-08-13 11:04:03 -07002342 @Override
2343 public void onBackPressed() {
2344 // If we are showing the wait fragment, just exit.
2345 if (getWaitFragment() != null) {
2346 finish();
2347 } else {
2348 super.onBackPressed();
2349 }
2350 }
2351
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002352 /**
2353 * Carries out the "up" action in the action bar.
2354 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002355 private void onAppUpPressed() {
2356 if (mLaunchedFromEmail) {
2357 // If this was started from Gmail, simply treat app up as the system back button, so
2358 // that the last view is restored.
2359 onBackPressed();
2360 return;
2361 }
2362
2363 // Fire the main activity to ensure it launches the "top" screen of mail.
2364 // Since the main Activity is singleTask, it should revive that task if it was already
2365 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002366 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002367 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2368 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2369 startActivity(mailIntent);
2370 finish();
2371 }
2372
Mindy Pereira33fe9082012-01-09 16:24:30 -08002373 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002374 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002375 logSendOrSave(false /* save */);
2376 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002377 }
2378
Mindy Pereira48e31b02012-05-30 13:12:24 -07002379 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002380 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002381 }
2382
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002383 @Override
2384 public void onRecipientEntryItemClicked(int charactersTyped, int position) {
2385 // Send analytics of characters typed and position in dropdown selected.
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002386 final String category = mUseNewChips ? "suggest_click_new" : "suggest_click_old";
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002387 Analytics.getInstance().sendEvent(
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002388 category, Integer.toString(charactersTyped), Integer.toString(position), 0);
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002389 }
2390
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002391 @VisibleForTesting
2392 public interface SendOrSaveCallback {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002393 void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
2394 void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2395 Message getMessage();
2396 void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2397 void incrementRecipientsTimesContacted(List<String> recipients);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002398 }
2399
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002400 @VisibleForTesting
2401 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002402 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002403 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002404 public final SendOrSaveCallback mSendOrSaveCallback;
2405 @VisibleForTesting
2406 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002407 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002408
2409 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002410 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002411 mContext = context;
2412 mSendOrSaveCallback = callback;
2413 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002414 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002415 }
2416
2417 @Override
2418 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002419 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002420
Mindy Pereira92551d02012-04-05 11:31:12 -07002421 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002422 Message message = mSendOrSaveCallback.getMessage();
2423 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002424 // If a previous draft has been saved, in an account that is different
2425 // than what the user wants to send from, remove the old draft, and treat this
2426 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002427 if (mExistingDraftAccount != null
2428 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002429 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2430 ContentResolver resolver = mContext.getContentResolver();
2431 ContentValues values = new ContentValues();
2432 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002433 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2434 new ContentProviderTask.UpdateTask()
2435 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2436 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002437 } else {
2438 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002439 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002440 // reset messageId to 0, so a new message will be created
2441 messageId = UIProvider.INVALID_MESSAGE_ID;
2442 }
2443 }
2444
2445 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002446 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002447
2448 if (!sendOrSaveMessage.mSave) {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002449 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002450 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002451 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002452 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002453 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002454 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2455 }
2456 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2457 }
2458
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002459 private void incrementRecipientsTimesContacted(final String addressString) {
Tony Mantler9f324232013-08-08 14:24:30 -07002460 if (TextUtils.isEmpty(addressString)) {
2461 return;
2462 }
2463 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2464 final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
Tony Mantler581edd42014-02-18 15:41:22 -08002465 for (final Rfc822Token token : tokens) {
2466 recipients.add(token.getAddress());
Tony Mantler9f324232013-08-08 14:24:30 -07002467 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002468 mSendOrSaveCallback.incrementRecipientsTimesContacted(recipients);
Tony Mantler9f324232013-08-08 14:24:30 -07002469 }
2470
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002471 /**
2472 * Send or Save a message.
2473 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002474 private void sendOrSaveMessage(final long messageIdToSave,
2475 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002476 final ContentResolver resolver = mContext.getContentResolver();
2477 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2478
2479 final String accountMethod = sendOrSaveMessage.mSave ?
2480 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2481 UIProvider.AccountCallMethods.SEND_MESSAGE;
2482
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002483 try {
2484 if (updateExistingMessage) {
2485 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002486
Paul Westbrook013a23c2013-02-22 10:37:41 -08002487 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002488 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002489 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002490 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002491 final Bundle result = callAccountSendSaveMethod(resolver,
2492 selectedAccount.account, accountMethod, sendOrSaveMessage);
2493 if (result != null) {
2494 // If a non-null value was returned, then the provider handled the call
2495 // method
2496 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002497 }
2498 if (sendOrSaveMessage.mSave && messageUri != null) {
2499 final Cursor messageCursor = resolver.query(messageUri,
2500 UIProvider.MESSAGE_PROJECTION, null, null, null);
2501 if (messageCursor != null) {
2502 try {
2503 if (messageCursor.moveToFirst()) {
2504 // Broadcast notification that a new message has
2505 // been allocated
2506 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2507 new Message(messageCursor));
2508 }
2509 } finally {
2510 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002511 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002512 }
2513 }
2514 }
2515 } finally {
2516 // Close any opened file descriptors
2517 closeOpenedAttachmentFds(sendOrSaveMessage);
2518 }
2519 }
2520
Scott Kennedyff8553f2013-04-05 20:57:44 -07002521 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002522 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2523 if (openedFds != null) {
2524 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002525 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002526 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2527 if (fd != null) {
2528 try {
2529 fd.close();
2530 } catch (IOException e) {
2531 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002532 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002533 }
2534 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002535 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002536 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002537
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002538 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002539 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002540 *
2541 * If this was successful, this method will return an non-null Bundle instance
2542 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002543 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2544 final Account account, final String method,
2545 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002546 // Copy all of the values from the content values to the bundle
2547 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2548 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2549
2550 for (Entry<String, Object> entry : valueSet) {
2551 final Object entryValue = entry.getValue();
2552 final String key = entry.getKey();
2553 if (entryValue instanceof String) {
2554 methodExtras.putString(key, (String)entryValue);
2555 } else if (entryValue instanceof Boolean) {
2556 methodExtras.putBoolean(key, (Boolean)entryValue);
2557 } else if (entryValue instanceof Integer) {
2558 methodExtras.putInt(key, (Integer)entryValue);
2559 } else if (entryValue instanceof Long) {
2560 methodExtras.putLong(key, (Long)entryValue);
2561 } else {
2562 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2563 entryValue.getClass().getName());
2564 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002565 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002566
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002567 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2568 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2569 if (fdMap != null) {
2570 methodExtras.putParcelable(
2571 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2572 }
2573
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002574 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002575 }
2576 }
2577
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002578 /**
2579 * Reports recipients that have been contacted in order to improve auto-complete
2580 * suggestions. Default behavior updates usage statistics in ContactsProvider.
2581 * @param recipients addresses
2582 */
2583 protected void incrementRecipientsTimesContacted(List<String> recipients) {
2584 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(this);
2585 statsUpdater.updateWithAddress(recipients);
2586 }
2587
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002588 @VisibleForTesting
2589 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002590 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002591 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002592 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002593 @VisibleForTesting
2594 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002595 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002596 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002597
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002598 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2599 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002600 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002601 mValues = values;
2602 mRefMessageId = refMessageId;
2603 mSave = save;
2604 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002605
2606 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002607 }
2608
2609 int requestId() {
2610 return mRequestId;
2611 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002612
2613 Bundle attachmentFds() {
2614 return mAttachmentFds;
2615 }
2616
2617 /**
2618 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2619 * called before the ComposeActivity finishes.
2620 * Note: The caller is responsible for closing these file descriptors.
2621 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002622 private static Bundle initializeAttachmentFds(final Context context,
2623 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002624 if (attachments == null || attachments.size() == 0) {
2625 return null;
2626 }
2627
2628 final Bundle result = new Bundle(attachments.size());
2629 final ContentResolver resolver = context.getContentResolver();
2630
2631 for (Attachment attachment : attachments) {
2632 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2633 continue;
2634 }
2635
2636 ParcelFileDescriptor fileDescriptor;
2637 try {
2638 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2639 } catch (FileNotFoundException e) {
2640 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2641 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002642 } catch (SecurityException e) {
2643 // We have encountered a security exception when attempting to open the file
2644 // specified by the content uri. If the attachment has been cached, this
2645 // isn't a problem, as even through the original permission may have been
2646 // revoked, we have cached the file. This will happen when saving/sending
2647 // a previously saved draft.
2648 // TODO(markwei): Expose whether the attachment has been cached through the
2649 // attachment object. This would allow us to limit when the log is made, as
2650 // if the attachment has been cached, this really isn't an error
2651 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2652 // Just set the file descriptor to null, as the underlying provider needs
2653 // to handle the file descriptor not being set.
2654 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002655 }
2656
2657 if (fileDescriptor != null) {
2658 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2659 }
2660 }
2661
2662 return result;
2663 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002664 }
2665
2666 /**
2667 * Get the to recipients.
2668 */
2669 public String[] getToAddresses() {
2670 return getAddressesFromList(mTo);
2671 }
2672
2673 /**
2674 * Get the cc recipients.
2675 */
2676 public String[] getCcAddresses() {
2677 return getAddressesFromList(mCc);
2678 }
2679
2680 /**
2681 * Get the bcc recipients.
2682 */
2683 public String[] getBccAddresses() {
2684 return getAddressesFromList(mBcc);
2685 }
2686
2687 public String[] getAddressesFromList(RecipientEditTextView list) {
2688 if (list == null) {
2689 return new String[0];
2690 }
2691 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2692 int count = tokens.length;
2693 String[] result = new String[count];
2694 for (int i = 0; i < count; i++) {
2695 result[i] = tokens[i].toString();
2696 }
2697 return result;
2698 }
2699
2700 /**
2701 * Check for invalid email addresses.
2702 * @param to String array of email addresses to check.
2703 * @param wrongEmailsOut Emails addresses that were invalid.
2704 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002705 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002706 if (mValidator == null) {
2707 return;
2708 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002709 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002710 if (!mValidator.isValid(email)) {
2711 wrongEmailsOut.add(email);
2712 }
2713 }
2714 }
2715
Tony Mantler2558b502013-07-09 10:53:34 -07002716 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002717 // Public no-args constructor needed for fragment re-instantiation
2718 public RecipientErrorDialogFragment() {}
2719
Tony Mantler2558b502013-07-09 10:53:34 -07002720 public static RecipientErrorDialogFragment newInstance(final String message) {
2721 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2722 final Bundle args = new Bundle(1);
2723 args.putString("message", message);
2724 frag.setArguments(args);
2725 return frag;
2726 }
2727
2728 @Override
2729 public Dialog onCreateDialog(Bundle savedInstanceState) {
2730 final String message = getArguments().getString("message");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002731 return new AlertDialog.Builder(getActivity())
2732 .setMessage(message)
Tony Mantler2558b502013-07-09 10:53:34 -07002733 .setPositiveButton(
2734 R.string.ok, new Dialog.OnClickListener() {
2735 @Override
2736 public void onClick(DialogInterface dialog, int which) {
2737 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2738 }
2739 }).create();
2740 }
2741 }
2742
2743 private void finishRecipientErrorDialog() {
2744 // after the user dismisses the recipient error
2745 // dialog we want to make sure to refocus the
2746 // recipient to field so they can fix the issue
2747 // easily
2748 if (mTo != null) {
2749 mTo.requestFocus();
2750 }
2751 }
2752
Mindy Pereira82cc5662012-01-09 17:29:30 -08002753 /**
2754 * Show an error because the user has entered an invalid recipient.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002755 */
Tony Mantler2558b502013-07-09 10:53:34 -07002756 private void showRecipientErrorDialog(final String message) {
2757 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2758 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002759 }
2760
2761 /**
2762 * Update the state of the UI based on whether or not the current draft
2763 * needs to be saved and the message is not empty.
2764 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002765 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002766 if (mSave != null) {
2767 mSave.setEnabled((shouldSave() && !isBlank()));
2768 }
2769 }
2770
2771 /**
2772 * Returns true if we need to save the current draft.
2773 */
2774 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002775 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002776 // The message should only be saved if:
2777 // It hasn't been sent AND
2778 // Some text has been added to the message OR
2779 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002780 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002781 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002782 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002783 }
2784 }
2785
2786 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002787 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002788 * @return boolean
2789 */
2790 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002791 // Need to check for null since isBlank() can be called from onPause()
2792 // before findViews() is called
2793 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2794 mAttachmentsView == null) {
2795 LogUtils.w(LOG_TAG, "null views in isBlank check");
2796 return true;
2797 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002798 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002799 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2800 mBodyView.getText().toString()) == 0)
2801 && mTo.length() == 0
2802 && mCc.length() == 0 && mBcc.length() == 0
2803 && mAttachmentsView.getAttachments().size() == 0;
2804 }
2805
2806 @VisibleForTesting
2807 protected int getSignatureStartPosition(String signature, String bodyText) {
2808 int startPos = -1;
2809
2810 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2811 return startPos;
2812 }
2813
2814 int bodyLength = bodyText.length();
2815 int signatureLength = signature.length();
2816 String printableVersion = convertToPrintableSignature(signature);
2817 int printableLength = printableVersion.length();
2818
2819 if (bodyLength >= printableLength
2820 && bodyText.substring(bodyLength - printableLength)
2821 .equals(printableVersion)) {
2822 startPos = bodyLength - printableLength;
2823 } else if (bodyLength >= signatureLength
2824 && bodyText.substring(bodyLength - signatureLength)
2825 .equals(signature)) {
2826 startPos = bodyLength - signatureLength;
2827 }
2828 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002829 }
2830
2831 /**
2832 * Allows any changes made by the user to be ignored. Called when the user
2833 * decides to discard a draft.
2834 */
2835 private void discardChanges() {
2836 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002837 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002838 mReplyFromChanged = false;
2839 }
2840
2841 /**
Tony Mantler581edd42014-02-18 15:41:22 -08002842 * @param save True to save, false to send
2843 * @param showToast True to show a toast once the message is sent/saved
Mindy Pereira181df782012-03-01 13:32:44 -08002844 */
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002845 protected void sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002846 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002847 if (mAccounts == null || mAccount == null) {
2848 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002849 if (autoSend) {
2850 finish();
2851 }
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002852 return;
Mark Wei009b3712012-10-18 18:07:50 -07002853 }
2854
Scott Kennedyff8553f2013-04-05 20:57:44 -07002855 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002856 if (orientationChanged) {
2857 to = cc = bcc = new String[0];
2858 } else {
2859 to = getToAddresses();
2860 cc = getCcAddresses();
2861 bcc = getBccAddresses();
2862 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002863
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002864 final ArrayList<String> recipients = buildEmailAddressList(to);
2865 recipients.addAll(buildEmailAddressList(cc));
2866 recipients.addAll(buildEmailAddressList(bcc));
2867
Mindy Pereira181df782012-03-01 13:32:44 -08002868 // Don't let the user send to nobody (but it's okay to save a message
2869 // with no recipients)
2870 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2871 showRecipientErrorDialog(getString(R.string.recipient_needed));
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002872 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002873 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002874
Mindy Pereira181df782012-03-01 13:32:44 -08002875 List<String> wrongEmails = new ArrayList<String>();
2876 if (!save) {
2877 checkInvalidEmails(to, wrongEmails);
2878 checkInvalidEmails(cc, wrongEmails);
2879 checkInvalidEmails(bcc, wrongEmails);
2880 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002881
Mindy Pereira181df782012-03-01 13:32:44 -08002882 // Don't let the user send an email with invalid recipients
2883 if (wrongEmails.size() > 0) {
2884 String errorText = String.format(getString(R.string.invalid_recipient),
2885 wrongEmails.get(0));
2886 showRecipientErrorDialog(errorText);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002887 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002888 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002889
Mindy Pereira181df782012-03-01 13:32:44 -08002890 if (!save) {
Alan Lau3d519042014-06-05 11:13:06 -07002891 if (autoSend) {
2892 // Skip all further checks during autosend. This flow is used by Android Wear
2893 // and Google Now.
2894 sendOrSave(save, showToast);
2895 return;
2896 }
2897
2898 // Show a warning before sending only if there are no attachments, body, or subject.
Mindy Pereira181df782012-03-01 13:32:44 -08002899 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2900 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002901 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002902
Mindy Pereira181df782012-03-01 13:32:44 -08002903 // A warning about an empty body may not be warranted when
2904 // forwarding mails, since a common use case is to forward
2905 // quoted text and not append any more text.
2906 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002907
Mindy Pereira181df782012-03-01 13:32:44 -08002908 // When we bring up a dialog warning the user about a send,
2909 // assume that they accept sending the message. If they do not,
2910 // the dialog listener is required to enable sending again.
2911 if (warnAboutEmptySubject) {
Tony Mantler581edd42014-02-18 15:41:22 -08002912 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002913 showToast, recipients);
2914 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002915 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002916
Mindy Pereira181df782012-03-01 13:32:44 -08002917 if (warnAboutEmptyBody) {
Tony Mantler581edd42014-02-18 15:41:22 -08002918 showSendConfirmDialog(R.string.confirm_send_message_with_no_body,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002919 showToast, recipients);
2920 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002921 }
2922 }
Alan Lau3d519042014-06-05 11:13:06 -07002923 // Ask for confirmation to send.
2924 if (showSendConfirmation()) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002925 showSendConfirmDialog(R.string.confirm_send_message, showToast, recipients);
2926 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002927 }
2928 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002929
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002930 performAdditionalSendOrSaveSanityChecks(save, showToast, recipients);
Mindy Pereira181df782012-03-01 13:32:44 -08002931 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002932
Mindy Pereira181df782012-03-01 13:32:44 -08002933 /**
2934 * Returns a boolean indicating whether warnings should be shown for empty
2935 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002936 *
Mindy Pereira181df782012-03-01 13:32:44 -08002937 * @return True if a warning should be shown for empty text fields
2938 */
2939 protected boolean showEmptyTextWarnings() {
2940 return mAttachmentsView.getAttachments().size() == 0;
2941 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002942
Mindy Pereira181df782012-03-01 13:32:44 -08002943 /**
2944 * Returns a boolean indicating whether the user should confirm each send
2945 *
2946 * @return True if a warning should be on each send
2947 */
2948 protected boolean showSendConfirmation() {
Tony Mantler581edd42014-02-18 15:41:22 -08002949 return mCachedSettings != null && mCachedSettings.confirmSend;
Mindy Pereira181df782012-03-01 13:32:44 -08002950 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002951
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002952 public static class SendConfirmDialogFragment extends DialogFragment
2953 implements DialogInterface.OnClickListener {
2954
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002955 private static final String MESSAGE_ID = "messageId";
2956 private static final String SHOW_TOAST = "showToast";
2957 private static final String RECIPIENTS = "recipients";
2958
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002959 private boolean mShowToast;
2960
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002961 private ArrayList<String> mRecipients;
2962
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002963 // Public no-args constructor needed for fragment re-instantiation
2964 public SendConfirmDialogFragment() {}
2965
Tony Mantler2558b502013-07-09 10:53:34 -07002966 public static SendConfirmDialogFragment newInstance(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002967 final boolean showToast, final ArrayList<String> recipients) {
Tony Mantler2558b502013-07-09 10:53:34 -07002968 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2969 final Bundle args = new Bundle(3);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002970 args.putInt(MESSAGE_ID, messageId);
2971 args.putBoolean(SHOW_TOAST, showToast);
2972 args.putStringArrayList(RECIPIENTS, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07002973 frag.setArguments(args);
2974 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002975 }
Tony Mantler2558b502013-07-09 10:53:34 -07002976
2977 @Override
2978 public Dialog onCreateDialog(Bundle savedInstanceState) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002979 final int messageId = getArguments().getInt(MESSAGE_ID);
2980 mShowToast = getArguments().getBoolean(SHOW_TOAST);
2981 mRecipients = getArguments().getStringArrayList(RECIPIENTS);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002982
2983 final int confirmTextId = (messageId == R.string.confirm_send_message) ?
2984 R.string.ok : R.string.send;
Tony Mantler2558b502013-07-09 10:53:34 -07002985
2986 return new AlertDialog.Builder(getActivity())
2987 .setMessage(messageId)
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002988 .setPositiveButton(confirmTextId, this)
Paul Westbrook7d1c5c42013-10-01 23:40:04 -07002989 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07002990 .create();
2991 }
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002992
2993 @Override
2994 public void onClick(DialogInterface dialog, int which) {
2995 if (which == DialogInterface.BUTTON_POSITIVE) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002996 ((ComposeActivity) getActivity()).finishSendConfirmDialog(mShowToast, mRecipients);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002997 }
2998 }
Tony Mantler2558b502013-07-09 10:53:34 -07002999 }
3000
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003001 private void finishSendConfirmDialog(
3002 final boolean showToast, final ArrayList<String> recipients) {
3003 performAdditionalSendOrSaveSanityChecks(false /* save */, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003004 }
3005
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003006 // The list of recipients are used by the additional sendOrSave checks.
3007 // However, the send confirm dialog may be shown before performing
3008 // the additional checks. As a result, we need to plumb the recipient
3009 // list through the send confirm dialog so that
3010 // performAdditionalSendOrSaveChecks can be performed properly.
Tony Mantler581edd42014-02-18 15:41:22 -08003011 private void showSendConfirmDialog(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003012 final boolean showToast, final ArrayList<String> recipients) {
3013 final DialogFragment frag = SendConfirmDialogFragment.newInstance(
3014 messageId, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003015 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08003016 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003017
Mindy Pereira181df782012-03-01 13:32:44 -08003018 /**
3019 * Returns whether the ComposeArea believes there is any text in the body of
3020 * the composition. TODO: When ComposeArea controls the Body as well, add
3021 * that here.
3022 */
3023 public boolean isBodyEmpty() {
3024 return !mQuotedTextView.isTextIncluded();
3025 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003026
Mindy Pereira181df782012-03-01 13:32:44 -08003027 /**
3028 * Test to see if the subject is empty.
3029 *
3030 * @return boolean.
3031 */
3032 // TODO: this will likely go away when composeArea.focus() is implemented
3033 // after all the widget control is moved over.
3034 public boolean isSubjectEmpty() {
3035 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
3036 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003037
Andy Huang0a2a3462013-12-20 15:56:13 -08003038 @VisibleForTesting
3039 public String getSubject() {
3040 return mSubject.getText().toString();
3041 }
3042
Andy Huang91ede362014-01-21 19:16:00 -08003043 private int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Jin Cao77b4c2c2014-05-20 13:55:53 -07003044 Message message, final Message refMessage, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08003045 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07003046 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08003047 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003048
Paul Westbrookb4931c62013-01-14 17:51:18 -08003049 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07003050
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003051 MessageModification.putToAddresses(values, message.getToAddresses());
3052 MessageModification.putCcAddresses(values, message.getCcAddresses());
3053 MessageModification.putBccAddresses(values, message.getBccAddresses());
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003054 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07003055
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003056 MessageModification.putSubject(values, message.subject);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003057
Jin Cao77b4c2c2014-05-20 13:55:53 -07003058 // bodyHtml already have the composing spans removed.
3059 final String htmlBody = message.bodyHtml;
Anthony Lee2a3cc132014-04-22 14:15:25 -07003060 final String textBody = Utils.convertHtmlToPlainText(htmlBody);
3061 // fullbody will contain the actual body plus the quoted text.
3062 final String fullBody;
3063 final String quotedString;
3064 final boolean hasQuotedText = !TextUtils.isEmpty(quotedText);
3065 if (hasQuotedText) {
3066 // The quoted text is HTML at this point.
3067 quotedString = quotedText.toString();
3068 fullBody = htmlBody + quotedString;
3069 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
3070 MessageModification.putAppendRefMessageContent(values, true /* include quoted */);
3071 } else {
3072 fullBody = htmlBody;
3073 quotedString = null;
Mindy Pereira29ef1b82012-01-13 11:26:21 -08003074 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003075 if (refMessage != null) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07003076 // The code below might need to be revisited. The quoted text position is different
3077 // between text/html and text/plain parts and they should be stored seperately and
3078 // the right version should be used in the UI. text/html should have preference
3079 // if both exist. Issues like this made me file b/14256940 to make sure that we
3080 // properly handle the existing of both text/html and text/plain parts and to verify
3081 // that we are not making some assumptions that break if there is no text/html part.
3082 int quotedTextPos = -1;
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003083 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
3084 MessageModification.putBodyHtml(values, fullBody.toString());
Anthony Lee2a3cc132014-04-22 14:15:25 -07003085 if (hasQuotedText) {
3086 quotedTextPos = htmlBody.length() +
3087 QuotedTextView.getQuotedTextOffset(quotedString);
3088 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003089 }
3090 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08003091 MessageModification.putBody(values,
Tony Mantler581edd42014-02-18 15:41:22 -08003092 Utils.convertHtmlToPlainText(fullBody.toString()));
Anthony Lee2a3cc132014-04-22 14:15:25 -07003093 if (hasQuotedText && (quotedTextPos == -1)) {
3094 quotedTextPos = textBody.length();
3095 }
3096 }
3097 if (quotedTextPos != -1) {
3098 // The quoted text pos is the text/html version first and the text/plan version
3099 // if there is no text/html part. The reason for this is because preference
3100 // is given to text/html in the compose window if it exists. In the future, we
3101 // should calculate the index for both since the user could choose to compose
3102 // explicitly in text/plain.
3103 MessageModification.putQuoteStartPos(values, quotedTextPos);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003104 }
3105 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07003106 MessageModification.putBodyHtml(values, fullBody.toString());
Tony Mantler581edd42014-02-18 15:41:22 -08003107 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString()));
Mindy Pereirac2031972012-04-03 09:38:35 -07003108 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07003109 int draftType = getDraftType(composeMode);
3110 MessageModification.putDraftType(values, draftType);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003111 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07003112 if (!TextUtils.isEmpty(refMessageId)) {
3113 MessageModification.putRefMessageId(values, refMessageId);
3114 }
Scott Kennedy60847252013-08-15 15:55:42 -07003115 if (extraValues != null) {
3116 values.putAll(extraValues);
3117 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07003118 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
3119 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08003120 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
3121 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003122
Mindy Pereira181df782012-03-01 13:32:44 -08003123 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08003124 // Do the send/save action on the specified handler to avoid possible
3125 // ANRs
3126 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003127
Mindy Pereira181df782012-03-01 13:32:44 -08003128 return sendOrSaveMessage.requestId();
3129 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003130
Paul Westbrookb4931c62013-01-14 17:51:18 -08003131 /**
3132 * Removes any composing spans from the specified string. This will create a new
3133 * SpannableString instance, as to not modify the behavior of the EditText view.
3134 */
3135 private static SpannableString removeComposingSpans(Spanned body) {
3136 final SpannableString messageBody = new SpannableString(body);
3137 BaseInputConnection.removeComposingSpans(messageBody);
3138 return messageBody;
3139 }
3140
Mindy Pereira002ff522012-05-30 10:31:26 -07003141 private static int getDraftType(int mode) {
3142 int draftType = -1;
3143 switch (mode) {
3144 case ComposeActivity.COMPOSE:
3145 draftType = DraftType.COMPOSE;
3146 break;
3147 case ComposeActivity.REPLY:
3148 draftType = DraftType.REPLY;
3149 break;
3150 case ComposeActivity.REPLY_ALL:
3151 draftType = DraftType.REPLY_ALL;
3152 break;
3153 case ComposeActivity.FORWARD:
3154 draftType = DraftType.FORWARD;
3155 break;
3156 }
3157 return draftType;
3158 }
3159
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003160 /**
3161 * Derived classes should override this step to perform additional checks before
3162 * send or save. The default implementation simply calls {@link #sendOrSave(boolean, boolean)}.
3163 */
3164 protected void performAdditionalSendOrSaveSanityChecks(
3165 final boolean save, final boolean showToast, ArrayList<String> recipients) {
3166 sendOrSave(save, showToast);
3167 }
3168
3169 protected void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08003170 // Check if user is a monkey. Monkeys can compose and hit send
3171 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07003172 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08003173 return;
3174 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003175
Jin Cao77b4c2c2014-05-20 13:55:53 -07003176 final SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003177 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08003178 private int mRestoredRequestId;
3179
Marc Blank0bbc8582012-04-23 15:07:57 -07003180 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08003181 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08003182 synchronized (mActiveTasks) {
3183 int numTasks = mActiveTasks.size();
3184 if (numTasks == 0) {
3185 // Start service so we won't be killed if this app is
3186 // put in the background.
3187 startService(new Intent(ComposeActivity.this, EmptyService.class));
3188 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003189
Mindy Pereira181df782012-03-01 13:32:44 -08003190 mActiveTasks.add(sendOrSaveTask);
3191 }
3192 if (sTestSendOrSaveCallback != null) {
3193 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
3194 }
3195 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003196
Marc Blank0bbc8582012-04-23 15:07:57 -07003197 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003198 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
3199 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08003200 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08003201 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08003202 mDraftId = message.id;
3203 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003204 if (sRequestMessageIdMap != null) {
3205 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
3206 }
Mindy Pereira181df782012-03-01 13:32:44 -08003207 // Cache request message map, in case the process is killed
3208 saveRequestMap();
3209 }
3210 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003211 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08003212 }
3213 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003214
Marc Blank0bbc8582012-04-23 15:07:57 -07003215 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003216 public Message getMessage() {
3217 synchronized (mDraftLock) {
3218 return mDraft;
3219 }
3220 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003221
Marc Blank0bbc8582012-04-23 15:07:57 -07003222 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003223 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07003224 // Update the last sent from account.
3225 if (mAccount != null) {
3226 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
3227 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003228 if (success) {
3229 // Successfully sent or saved so reset change markers
3230 discardChanges();
3231 } else {
3232 // A failure happened with saving/sending the draft
3233 // TODO(pwestbro): add a better string that should be used
3234 // when failing to send or save
3235 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
3236 .show();
3237 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003238
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003239 int numTasks;
3240 synchronized (mActiveTasks) {
3241 // Remove the task from the list of active tasks
3242 mActiveTasks.remove(task);
3243 numTasks = mActiveTasks.size();
3244 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003245
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003246 if (numTasks == 0) {
3247 // Stop service so we can be killed.
3248 stopService(new Intent(ComposeActivity.this, EmptyService.class));
3249 }
3250 if (sTestSendOrSaveCallback != null) {
3251 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
3252 }
3253 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07003254
3255 @Override
3256 public void incrementRecipientsTimesContacted(final List<String> recipients) {
3257 ComposeActivity.this.incrementRecipientsTimesContacted(recipients);
3258 }
Mindy Pereira181df782012-03-01 13:32:44 -08003259 };
Tony Mantler1e05a1e2013-08-12 16:44:26 -07003260 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003261
Jin Cao77b4c2c2014-05-20 13:55:53 -07003262 final Spanned body = removeComposingSpans(mBodyView.getText());
3263 SEND_SAVE_TASK_HANDLER.post(new Runnable() {
3264 @Override
3265 public void run() {
3266 final Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode(), body);
3267 mRequestId = sendOrSaveInternal(ComposeActivity.this, mReplyFromAccount, msg,
3268 mRefMessage, mQuotedTextView.getQuotedTextIfIncluded(), callback,
3269 SEND_SAVE_TASK_HANDLER, save, mComposeMode, mDraftAccount, mExtraValues);
3270 }
3271 });
Mindy Pereira82cc5662012-01-09 17:29:30 -08003272
Mindy Pereira181df782012-03-01 13:32:44 -08003273 // Don't display the toast if the user is just changing the orientation,
3274 // but we still need to save the draft to the cursor because this is how we restore
3275 // the attachments when the configuration change completes.
3276 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
3277 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
3278 Toast.LENGTH_LONG).show();
3279 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003280
Mindy Pereira181df782012-03-01 13:32:44 -08003281 // Need to update variables here because the send or save completes
3282 // asynchronously even though the toast shows right away.
3283 discardChanges();
3284 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003285
Mindy Pereira181df782012-03-01 13:32:44 -08003286 // If we are sending, finish the activity
3287 if (!save) {
3288 finish();
3289 }
3290 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003291
Mindy Pereira181df782012-03-01 13:32:44 -08003292 /**
3293 * Save the state of the request messageid map. This allows for the Gmail
3294 * process to be killed, but and still allow for ComposeActivity instances
3295 * to be recreated correctly.
3296 */
3297 private void saveRequestMap() {
3298 // TODO: store the request map in user preferences.
3299 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003300
Tony Mantler581edd42014-02-18 15:41:22 -08003301 @SuppressLint("NewApi")
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003302 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08003303 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
3304 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07003305 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07003306 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003307 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08003308 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08003309 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
3310 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08003311 }
3312
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003313 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003314 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003315 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07003316 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003317 }
3318 }
3319
Andy Huang4fe0af82013-08-20 17:24:51 -07003320 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003321 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07003322 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003323 case COMPOSE:
3324 msgType = "new_message";
3325 break;
3326 case REPLY:
3327 msgType = "reply";
3328 break;
3329 case REPLY_ALL:
3330 msgType = "reply_all";
3331 break;
3332 case FORWARD:
3333 msgType = "forward";
3334 break;
3335 default:
3336 msgType = "unknown";
3337 break;
3338 }
Andy Huang4fe0af82013-08-20 17:24:51 -07003339 return msgType;
3340 }
3341
3342 private void logSendOrSave(boolean save) {
3343 if (!Analytics.isLoggable() || mAttachmentsView == null) {
3344 return;
3345 }
3346
3347 final String category = (save) ? "message_save" : "message_send";
3348 final int attachmentCount = getAttachments().size();
3349 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07003350 final String label;
3351 final long value;
3352 if (mComposeMode == COMPOSE) {
3353 label = Integer.toString(attachmentCount);
3354 value = attachmentCount;
3355 } else {
3356 label = null;
3357 value = 0;
3358 }
3359 Analytics.getInstance().sendEvent(category, msgType, label, value);
3360 }
3361
Mindy Pereira326c6602012-01-04 15:32:42 -08003362 @Override
3363 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003364 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003365 if (position == ComposeActivity.REPLY) {
3366 mComposeMode = ComposeActivity.REPLY;
3367 } else if (position == ComposeActivity.REPLY_ALL) {
3368 mComposeMode = ComposeActivity.REPLY_ALL;
3369 } else if (position == ComposeActivity.FORWARD) {
3370 mComposeMode = ComposeActivity.FORWARD;
3371 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003372 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003373 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003374 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003375 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003376 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003377 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003378 boolean showCc = false;
3379 boolean showBcc = false;
3380 if (mDraft != null) {
3381 // Following desktop behavior, if the user has added a BCC
3382 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003383 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003384 // Use the draft to determine what to populate.
3385 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003386 showCc = showBcc
3387 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003388 }
3389 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003390 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003391 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003392 }
3393 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003394 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003395 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003396 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003397 return true;
3398 }
3399
Mindy Pereirab3112a22012-06-20 12:10:03 -07003400 @VisibleForTesting
3401 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003402 // When switching between reply, reply all, forward,
3403 // follow the behavior of webview.
3404 // The contents of the following fields are cleared
3405 // so that they can be populated directly from the
3406 // ref message:
3407 // 1) Any recipient fields
3408 // 2) The subject
3409 mTo.setText("");
3410 mCc.setText("");
3411 mBcc.setText("");
3412 // Any edits to the subject are replaced with the original subject.
3413 mSubject.setText("");
3414
3415 // Any changes to the contents of the following fields are kept:
3416 // 1) Body
3417 // 2) Attachments
3418 // If the user made changes to attachments, keep their changes.
3419 if (!mAttachmentsChanged) {
3420 mAttachmentsView.deleteAllAttachments();
3421 }
3422 }
3423
Mindy Pereira326c6602012-01-04 15:32:42 -08003424 private class ComposeModeAdapter extends ArrayAdapter<String> {
3425
3426 private LayoutInflater mInflater;
3427
3428 public ComposeModeAdapter(Context context) {
3429 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3430 .getStringArray(R.array.compose_modes));
3431 }
3432
3433 private LayoutInflater getInflater() {
3434 if (mInflater == null) {
3435 mInflater = LayoutInflater.from(getContext());
3436 }
3437 return mInflater;
3438 }
3439
3440 @Override
3441 public View getView(int position, View convertView, ViewGroup parent) {
3442 if (convertView == null) {
3443 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3444 }
3445 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3446 return super.getView(position, convertView, parent);
3447 }
3448 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003449
3450 @Override
3451 public void onRespondInline(String text) {
3452 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003453 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003454 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003455 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003456 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003457 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003458 }
3459
3460 /**
3461 * Append text to the body of the message. If there is no existing body
3462 * text, just sets the body to text.
3463 *
Tony Mantler581edd42014-02-18 15:41:22 -08003464 * @param text Text to append
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003465 * @param withSignature True to append a signature.
3466 */
3467 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003468 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003469 if (bodyText != null && bodyText.length() > 0) {
3470 bodyText.append(text);
3471 } else {
3472 setBody(text, withSignature);
3473 }
3474 }
3475
3476 /**
3477 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003478 *
Tony Mantler581edd42014-02-18 15:41:22 -08003479 * @param text text to set
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003480 * @param withSignature True to append a signature.
3481 */
3482 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003483 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003484 if (withSignature) {
3485 appendSignature();
3486 }
3487 }
3488
3489 private void appendSignature() {
Tony Mantler6a7ac782014-02-19 15:22:02 -08003490 final String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3491 final int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
mindyp27083062012-11-15 09:02:01 -08003492 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003493 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003494 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003495 // Appending a signature does not count as changing text.
3496 mBodyView.removeTextChangedListener(this);
3497 mBodyView.append(convertToPrintableSignature(mSignature));
3498 mBodyView.addTextChangedListener(this);
3499 }
Tony Mantler6a7ac782014-02-19 15:22:02 -08003500 resetBodySelection();
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003501 }
3502 }
3503
3504 private String convertToPrintableSignature(String signature) {
3505 String signatureResource = getResources().getString(R.string.signature);
3506 if (signature == null) {
3507 signature = "";
3508 }
3509 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003510 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003511
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003512 @Override
3513 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003514 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3515 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003516 // Clear a signature, if there was one.
3517 mBodyView.removeTextChangedListener(this);
3518 String oldSignature = mSignature;
3519 String bodyText = getBody().getText().toString();
3520 if (!TextUtils.isEmpty(oldSignature)) {
3521 int pos = getSignatureStartPosition(oldSignature, bodyText);
3522 if (pos > -1) {
3523 mBodyView.setText(bodyText.substring(0, pos));
3524 }
3525 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003526 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003527 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003528 // TODO: handle discarding attachments when switching accounts.
3529 // Only enable save for this draft if there is any other content
3530 // in the message.
3531 if (!isBlank()) {
3532 enableSave(true);
3533 }
3534 mReplyFromChanged = true;
3535 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003536 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003537 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003538
3539 public void enableSave(boolean enabled) {
3540 if (mSave != null) {
3541 mSave.setEnabled(enabled);
3542 }
3543 }
3544
Tony Mantler2558b502013-07-09 10:53:34 -07003545 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003546 // Public no-args constructor needed for fragment re-instantiation
3547 public DiscardConfirmDialogFragment() {}
3548
Tony Mantler2558b502013-07-09 10:53:34 -07003549 @Override
3550 public Dialog onCreateDialog(Bundle savedInstanceState) {
3551 return new AlertDialog.Builder(getActivity())
3552 .setMessage(R.string.confirm_discard_text)
3553 .setPositiveButton(R.string.discard,
3554 new DialogInterface.OnClickListener() {
3555 @Override
3556 public void onClick(DialogInterface dialog, int which) {
3557 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3558 }
3559 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003560 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003561 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003562 }
3563 }
3564
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003565 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003566 final DialogFragment frag = new DiscardConfirmDialogFragment();
3567 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003568 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003569 /**
3570 * Effectively discard the current message.
3571 *
3572 * This method is either invoked from the menu or from the dialog
3573 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003574 */
Tony Mantler2558b502013-07-09 10:53:34 -07003575 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003576 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003577 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3578 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003579 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003580 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003581 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3582 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003583 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003584 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003585 // This is not strictly necessary (since we should not try to
3586 // save the draft after calling this) but it ensures that if we
3587 // do save again for some reason we make a new draft rather than
3588 // trying to resave an expunged draft.
3589 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3590 }
3591 }
3592
Tony Mantler2558b502013-07-09 10:53:34 -07003593 // Display a toast to let the user know
3594 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003595
3596 // This prevents the draft from being saved in onPause().
3597 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003598 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003599 finish();
3600 }
3601
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003602 private void saveIfNeeded() {
3603 if (mAccount == null) {
3604 // We have not chosen an account yet so there's no way that we can save. This is ok,
3605 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3606 // user has not interacted with us yet and there is no real state to save.
3607 return;
3608 }
3609
3610 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003611 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003612 }
3613 }
3614
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003615 @Override
3616 public void onAttachmentDeleted() {
3617 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003618 // If we are showing any attachments, make sure we have an upper
3619 // divider.
3620 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003621 updateSaveUi();
3622 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003623
mindyp40882432012-09-06 11:07:40 -07003624 @Override
3625 public void onAttachmentAdded() {
3626 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3627 mAttachmentsView.focusLastAttachment();
3628 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003629
3630 /**
3631 * This is called any time one of our text fields changes.
3632 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003633 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003634 public void afterTextChanged(Editable s) {
3635 mTextChanged = true;
3636 updateSaveUi();
3637 }
3638
3639 @Override
3640 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3641 // Do nothing.
3642 }
3643
Marc Blank0bbc8582012-04-23 15:07:57 -07003644 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003645 public void onTextChanged(CharSequence s, int start, int before, int count) {
3646 // Do nothing.
3647 }
3648
3649
3650 // There is a big difference between the text associated with an address changing
3651 // to add the display name or to format properly and a recipient being added or deleted.
3652 // Make sure we only notify of changes when a recipient has been added or deleted.
3653 private class RecipientTextWatcher implements TextWatcher {
3654 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3655
3656 private RecipientEditTextView mView;
3657
3658 private TextWatcher mListener;
3659
3660 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3661 mView = view;
3662 mListener = listener;
3663 }
3664
3665 @Override
3666 public void afterTextChanged(Editable s) {
3667 if (hasChanged()) {
3668 mListener.afterTextChanged(s);
3669 }
3670 }
3671
3672 private boolean hasChanged() {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003673 final ArrayList<String> currRecips = buildEmailAddressList(getAddressesFromList(mView));
3674 int totalCount = currRecips.size();
Mindy Pereira75f66632012-01-11 11:42:02 -08003675 int totalPrevCount = 0;
3676 for (Entry<String, Integer> entry : mContent.entrySet()) {
3677 totalPrevCount += entry.getValue();
3678 }
3679 if (totalCount != totalPrevCount) {
3680 return true;
3681 }
3682
3683 for (String recip : currRecips) {
3684 if (!mContent.containsKey(recip)) {
3685 return true;
3686 } else {
3687 int count = mContent.get(recip) - 1;
3688 if (count < 0) {
3689 return true;
3690 } else {
3691 mContent.put(recip, count);
3692 }
3693 }
3694 }
3695 return false;
3696 }
3697
Mindy Pereira75f66632012-01-11 11:42:02 -08003698 @Override
3699 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003700 final ArrayList<String> recips = buildEmailAddressList(getAddressesFromList(mView));
Mindy Pereira75f66632012-01-11 11:42:02 -08003701 for (String recip : recips) {
3702 if (!mContent.containsKey(recip)) {
3703 mContent.put(recip, 1);
3704 } else {
3705 mContent.put(recip, (mContent.get(recip)) + 1);
3706 }
3707 }
3708 }
3709
3710 @Override
3711 public void onTextChanged(CharSequence s, int start, int before, int count) {
3712 // Do nothing.
3713 }
3714 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003715
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003716 /**
3717 * Returns a list of email addresses from the recipients. List only contains
3718 * email addresses strips additional info like the recipient's name.
3719 */
3720 private static ArrayList<String> buildEmailAddressList(String[] recips) {
3721 // Tokenize them all and put them in the list.
3722 final ArrayList<String> recipAddresses = Lists.newArrayListWithCapacity(recips.length);
3723 for (int i = 0; i < recips.length; i++) {
3724 recipAddresses.add(Rfc822Tokenizer.tokenize(recips[i])[0].getAddress());
3725 }
3726 return recipAddresses;
3727 }
3728
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003729 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3730 if (sTestSendOrSaveCallback != null && testCallback != null) {
3731 throw new IllegalStateException("Attempting to register more than one test callback");
3732 }
3733 sTestSendOrSaveCallback = testCallback;
3734 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003735
3736 @VisibleForTesting
3737 protected ArrayList<Attachment> getAttachments() {
3738 return mAttachmentsView.getAttachments();
3739 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003740
3741 @Override
3742 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3743 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003744 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3745 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3746 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003747 case REFERENCE_MESSAGE_LOADER:
3748 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3749 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003750 case LOADER_ACCOUNT_CURSOR:
3751 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3752 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003753 }
3754 return null;
3755 }
3756
3757 @Override
3758 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003759 int id = loader.getId();
3760 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003761 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003762 if (data != null && data.moveToFirst()) {
3763 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003764 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003765 initFromRefMessage(mComposeMode);
3766 finishSetup(mComposeMode, intent, null);
3767 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003768 String to = intent.getStringExtra(EXTRA_TO);
3769 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003770 mRefMessage.setTo(null);
3771 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003772 clearChangeListeners();
3773 mTo.append(to);
3774 initChangeListeners();
3775 }
3776 }
3777 } else {
3778 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003779 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003780 break;
Alice Yanga990a712013-03-13 18:37:00 -07003781 case REFERENCE_MESSAGE_LOADER:
3782 // Only populate mRefMessage and leave other fields untouched.
3783 if (data != null && data.moveToFirst()) {
3784 mRefMessage = new Message(data);
3785 }
Andy Huang9f855d62013-05-30 17:15:03 -07003786 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003787 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003788 case LOADER_ACCOUNT_CURSOR:
3789 if (data != null && data.moveToFirst()) {
3790 // there are accounts now!
3791 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003792 final ArrayList<Account> accounts = new ArrayList<Account>();
3793 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003794 do {
3795 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003796 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003797 initializedAccounts.add(account);
3798 }
3799 accounts.add(account);
3800 } while (data.moveToNext());
3801 if (initializedAccounts.size() > 0) {
3802 findViewById(R.id.wait).setVisibility(View.GONE);
3803 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3804 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003805 mAccounts = initializedAccounts.toArray(
3806 new Account[initializedAccounts.size()]);
3807
Mindy Pereirab199d172012-08-13 11:04:03 -07003808 finishCreate();
3809 invalidateOptionsMenu();
3810 } else {
3811 // Show "waiting"
3812 account = accounts.size() > 0 ? accounts.get(0) : null;
3813 showWaitFragment(account);
3814 }
3815 }
3816 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003817 }
3818 }
3819
Mindy Pereirab199d172012-08-13 11:04:03 -07003820 private void showWaitFragment(Account account) {
3821 WaitFragment fragment = getWaitFragment();
3822 if (fragment != null) {
3823 fragment.updateAccount(account);
3824 } else {
3825 findViewById(R.id.wait).setVisibility(View.VISIBLE);
Andy Huangc96efcc2014-04-09 15:30:42 -07003826 replaceFragment(WaitFragment.newInstance(account, false /* expectingMessages */),
Mindy Pereirab199d172012-08-13 11:04:03 -07003827 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3828 }
3829 }
3830
3831 private WaitFragment getWaitFragment() {
3832 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3833 }
3834
3835 private int replaceFragment(Fragment fragment, int transition, String tag) {
3836 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003837 fragmentTransaction.setTransition(transition);
3838 fragmentTransaction.replace(R.id.wait, fragment, tag);
3839 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3840 return transactionId;
3841 }
3842
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003843 @Override
3844 public void onLoaderReset(Loader<Cursor> arg0) {
3845 // Do nothing.
3846 }
Jin Cao77b4c2c2014-05-20 13:55:53 -07003847
3848 /**
3849 * Background task to convert the message's html to Spanned.
3850 */
3851 private class HtmlToSpannedTask extends AsyncTask<String, Void, Spanned> {
3852
3853 @Override
3854 protected Spanned doInBackground(String... input) {
3855 return HtmlUtils.htmlToSpan(input[0]);
3856 }
3857
3858 @Override
3859 protected void onPostExecute(Spanned spanned) {
3860 mBodyView.removeTextChangedListener(ComposeActivity.this);
3861 mBodyView.setText(spanned);
3862 mTextChanged = false;
3863 mBodyView.addTextChangedListener(ComposeActivity.this);
3864 }
3865 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003866}