blob: e3de32f6fd3485ab2c0f33264e0626dce5a25814 [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;
Andy Huangff017272014-06-18 00:27:35 -070053import android.text.SpanWatcher;
mindyped9c2f02012-10-12 10:02:08 -070054import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080055import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080056import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080057import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080058import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080059import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070060import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070061import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080062import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080063import android.view.Menu;
64import android.view.MenuInflater;
65import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080066import android.view.View;
67import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070068import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080069import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070070import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080071import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080072import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070073import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080074import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080075import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080076
Mindy Pereirac17d0732011-12-29 10:46:19 -080077import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070078import com.android.common.contacts.DataUsageStatUpdater;
Tony Mantler821e5782014-01-06 15:33:43 -080079import com.android.emailcommon.mail.Address;
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -070080import com.android.ex.chips.BaseRecipientAdapter;
81import com.android.ex.chips.DropdownChipLayouter;
Andy Huang5c5fd572012-04-08 18:19:29 -070082import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080083import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070084import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070085import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070086import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070087import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080088import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080089import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080090import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080091import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080092import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080093import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070094import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080095import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080096import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070097import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080098import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080099import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -0700100import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -0700101import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -0700102import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700103import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -0700104import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -0700105import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -0700106import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800107import com.android.mail.utils.ContentProviderTask;
Jin Cao77b4c2c2014-05-20 13:55:53 -0700108import com.android.mail.utils.HtmlUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -0700109import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800110import com.android.mail.utils.LogUtils;
Alan Lau15490232014-03-06 14:53:14 -0800111import com.android.mail.utils.NotificationActionUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800112import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800113import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800114import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800115import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800116
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700117import java.io.FileNotFoundException;
118import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700119import java.io.UnsupportedEncodingException;
120import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800121import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700122import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800123import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800124import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800125import java.util.HashSet;
126import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700127import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700128import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800129import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800130
131public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700132 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700133 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
Andrew Sappersteinffd61552014-05-14 15:04:23 -0700134 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
135 RecipientEditTextView.RecipientEntryItemClickedListener {
Scott Kennedya0287a82014-04-07 14:30:13 -0700136 /**
137 * An {@link Intent} action that launches {@link ComposeActivity}, but is handled as if the
138 * {@link Activity} were launched with no special action.
139 */
140 private static final String ACTION_LAUNCH_COMPOSE =
141 "com.android.mail.intent.action.LAUNCH_COMPOSE";
142
Mindy Pereira6349a042012-01-04 11:25:01 -0800143 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700144 public static final int COMPOSE = -1;
145 public static final int REPLY = 0;
146 public static final int REPLY_ALL = 1;
147 public static final int FORWARD = 2;
148 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800149
150 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700151 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800152
Mindy Pereira326689d2012-05-17 10:14:14 -0700153 private static final String EXTRA_SHOW_CC = "showCc";
154 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800155 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800156 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700157
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700158 private static final String UTF8_ENCODING_NAME = "UTF-8";
159
160 private static final String MAIL_TO = "mailto";
161
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700162 private static final String EXTRA_SUBJECT = "subject";
163
164 private static final String EXTRA_BODY = "body";
165
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700166 /**
167 * Expected to be html formatted text.
168 */
169 private static final String EXTRA_QUOTED_TEXT = "quotedText";
170
mindypd27b6ea2012-10-05 09:43:49 -0700171 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700172
Mark Wei62066e42012-09-13 12:07:02 -0700173 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
174
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700175 // Extra that we can get passed from other activities
Tony Mantler184ec732013-10-24 13:13:49 -0700176 @VisibleForTesting
177 protected static final String EXTRA_TO = "to";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700178 private static final String EXTRA_CC = "cc";
179 private static final String EXTRA_BCC = "bcc";
180
Scott Kennedy60847252013-08-15 15:55:42 -0700181 /**
182 * An optional extra containing a {@link ContentValues} of values to be added to
183 * {@link SendOrSaveMessage#mValues}.
184 */
185 public static final String EXTRA_VALUES = "extra-values";
186
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700187 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700188 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
189 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700190
Alan Lau439aa5d2014-05-27 17:57:13 -0700191 private static final String LEGACY_WEAR_EXTRA = "com.google.android.wearable.extras";
192
Andrew Sapperstein09da9422014-05-30 09:48:08 -0700193 /**
194 * Constant value for the threshold to use for auto-complete suggestions
195 * for the to/cc/bcc fields.
196 */
197 private static final int COMPLETION_THRESHOLD = 1;
198
Mindy Pereira82cc5662012-01-09 17:29:30 -0800199 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
200 // Map containing information about requests to create new messages, and the id of the
201 // messages that were the result of those requests.
202 //
203 // This map is used when the activity that initiated the save a of a new message, is killed
204 // before the save has completed (and when we know the id of the newly created message). When
205 // a save is completed, the service that is running in the background, will update the map
206 //
207 // When a new ComposeActivity instance is created, it will attempt to use the information in
208 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
209 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
210 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700211 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800212 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800213 /**
214 * Notifies the {@code Activity} that the caller is an Email
215 * {@code Activity}, so that the back behavior may be modified accordingly.
216 *
217 * @see #onAppUpPressed
218 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700219 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800220
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700221 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700222
Scott Kennedy5680ec22013-01-07 13:15:20 -0800223 /** If set, we will clear notifications for this folder. */
224 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
Alan Laue806c942014-06-06 16:19:15 -0700225 public static final String EXTRA_NOTIFICATION_CONVERSATION = "extra-notification-conversation";
Scott Kennedy5680ec22013-01-07 13:15:20 -0800226
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800227 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700228 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700229 // If this is a reply/forward then this extra will hold a uri we must query
230 // to get the original message.
231 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700232 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700233 // draft message
234 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800235 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700236 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800237 // Request numbers for activities we start
238 private static final int RESULT_PICK_ATTACHMENT = 1;
239 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700240 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700241 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700242
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700243 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
244 private static final String EXTRA_REQUEST_ID = "requestId";
245 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700246 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700247 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700248 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700249 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700250 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700251 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700252 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700253 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700254 private static final String MIME_TYPE_PHOTO = "image/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800255
Andy Huang9f855d62013-05-30 17:15:03 -0700256 private static final String KEY_INNER_SAVED_STATE = "compose_state";
257
Mindy Pereira82cc5662012-01-09 17:29:30 -0800258 /**
259 * A single thread for running tasks in the background.
260 */
Jin Cao5134be52014-05-06 19:18:38 -0700261 private final static Handler SEND_SAVE_TASK_HANDLER;
262 static {
263 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
264 handlerThread.start();
265
266 SEND_SAVE_TASK_HANDLER = new Handler(handlerThread.getLooper());
267 }
268
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700269 private boolean mUseNewChips = false;
270
Mindy Pereirac17d0732011-12-29 10:46:19 -0800271 private RecipientEditTextView mTo;
272 private RecipientEditTextView mCc;
273 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800274 private Button mCcBccButton;
275 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800276 private AttachmentsView mAttachmentsView;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700277 protected Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700278 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800279 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800280 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800281 private TextView mSubject;
282
Mindy Pereira326c6602012-01-04 15:32:42 -0800283 private ComposeModeAdapter mComposeModeAdapter;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700284 protected int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800285 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800286 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700287 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800288 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800289 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800290 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700291 @VisibleForTesting
292 protected FromAddressSpinner mFromSpinner;
Andy Huang5f082212014-06-11 22:19:21 -0700293 protected boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800294 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800295 private boolean mTextChanged;
296 private boolean mReplyFromChanged;
297 private MenuItem mSave;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700298 @VisibleForTesting
299 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800300 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
301 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800302 private ReplyFromAccount mDraftAccount;
Tony Mantler581edd42014-02-18 15:41:22 -0800303 private final Object mDraftLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800304
Mindy Pereira326c6602012-01-04 15:32:42 -0800305 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700306 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
307 */
308 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700309 private RecipientTextWatcher mToListener;
310 private RecipientTextWatcher mCcListener;
311 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700312 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700313 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700314 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700315 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700316
mindyp1623f9b2012-11-21 12:41:16 -0800317 // Array of the outstanding send or save tasks. Access is synchronized
318 // with the object itself
319 /* package for testing */
320 @VisibleForTesting
Tony Mantler581edd42014-02-18 15:41:22 -0800321 public final ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
mindyp1623f9b2012-11-21 12:41:16 -0800322 // FIXME: this variable is never read. related to sRequestMessageIdMap.
323 private int mRequestId;
324 private String mSignature;
325 private Account[] mAccounts;
326 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700327 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800328
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700329 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800330 * Can be called from a non-UI thread.
331 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800332 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700333 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
334 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800335 }
336
Mindy Pereira6349a042012-01-04 11:25:01 -0800337 /**
338 * Can be called from a non-UI thread.
339 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800340 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700341 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800342 }
343
344 /**
345 * Can be called from a non-UI thread.
346 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700347 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700348 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
349 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700350 }
351
352 /**
353 * Can be called from a non-UI thread.
354 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700355 public static void composeWithExtraValues(Context launcher, Account account,
356 String subject, final ContentValues extraValues) {
357 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
358 }
359
360 /**
361 * Can be called from a non-UI thread.
362 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800363 public static Intent createReplyIntent(final Context launcher, final Account account,
364 final Uri messageUri, final boolean isReplyAll) {
365 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
366 }
367
368 /**
369 * Can be called from a non-UI thread.
370 */
371 public static Intent createForwardIntent(final Context launcher, final Account account,
372 final Uri messageUri) {
373 return createActionIntent(launcher, account, messageUri, FORWARD);
374 }
375
Scott Kennedya0287a82014-04-07 14:30:13 -0700376 private static Intent createActionIntent(final Context context, final Account account,
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800377 final Uri messageUri, final int action) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700378 final Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
379 intent.setPackage(context.getPackageName());
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800380
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700381 updateActionIntent(account, messageUri, action, intent);
382
383 return intent;
384 }
385
386 @VisibleForTesting
387 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800388 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
389 intent.putExtra(EXTRA_ACTION, action);
390 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
391 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
392
393 return intent;
394 }
395
396 /**
397 * Can be called from a non-UI thread.
398 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800399 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700400 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800401 }
402
403 /**
404 * Can be called from a non-UI thread.
405 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800406 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700407 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
408 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800409 }
410
411 /**
412 * Can be called from a non-UI thread.
413 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800414 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700415 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800416 }
417
Alice Yang1ebc2db2013-03-14 21:21:44 -0700418 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
419 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700420 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700421 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700422 }
423
Scott Kennedya0287a82014-04-07 14:30:13 -0700424 private static void launch(Context context, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700425 String toAddress, String body, String quotedText, String subject,
426 final ContentValues extraValues) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700427 Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
428 intent.setPackage(context.getPackageName());
Mindy Pereira6349a042012-01-04 11:25:01 -0800429 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
430 intent.putExtra(EXTRA_ACTION, action);
431 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700432 if (action == EDIT_DRAFT) {
433 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
434 } else {
435 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
436 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700437 if (toAddress != null) {
438 intent.putExtra(EXTRA_TO, toAddress);
439 }
440 if (body != null) {
441 intent.putExtra(EXTRA_BODY, body);
442 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700443 if (quotedText != null) {
444 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
445 }
446 if (subject != null) {
447 intent.putExtra(EXTRA_SUBJECT, subject);
448 }
Scott Kennedy60847252013-08-15 15:55:42 -0700449 if (extraValues != null) {
450 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
451 intent.putExtra(EXTRA_VALUES, extraValues);
452 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700453 context.startActivity(intent);
Mindy Pereira6349a042012-01-04 11:25:01 -0800454 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800455
Scott Kennedya0287a82014-04-07 14:30:13 -0700456 public static void composeMailto(Context context, Account account, Uri mailto) {
457 final Intent intent = new Intent(Intent.ACTION_VIEW, mailto);
458 intent.setPackage(context.getPackageName());
Andy Huang0a2a3462013-12-20 15:56:13 -0800459 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
460 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Scott Kennedya0287a82014-04-07 14:30:13 -0700461 context.startActivity(intent);
Andy Huang0a2a3462013-12-20 15:56:13 -0800462 }
463
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800464 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700465 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800466 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800467 setContentView(R.layout.compose);
Andy Huang9f855d62013-05-30 17:15:03 -0700468 mInnerSavedState = (savedInstanceState != null) ?
469 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700470 checkValidAccounts();
471 }
472
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700473 private boolean shouldUseNewChips() {
474 // Get the Android ID from this device
475 String androidId = android.provider.Settings.Secure
476 .getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
477
478 // If we don't have a valid android id, just use account name hash code
479 if (TextUtils.isEmpty(androidId)) {
480 LogUtils.d(LOG_TAG, "Fallback to email address");
481 androidId = mAccount.getEmailAddress();
482 }
483
484 // randomly cut our userbase in half
485 return (androidId.hashCode() % 2) == 1;
486 }
487
Mindy Pereirab199d172012-08-13 11:04:03 -0700488 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700489 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800490 findViews();
Tony Mantler581edd42014-02-18 15:41:22 -0800491 final Intent intent = getIntent();
492 final Message message;
493 final ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700494 mShowQuotedText = false;
Tony Mantler581edd42014-02-18 15:41:22 -0800495 final CharSequence quotedText;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700496 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700497 // Check for any of the possibly supplied accounts.;
Tony Mantler581edd42014-02-18 15:41:22 -0800498 final Account account;
Andy Huang9f855d62013-05-30 17:15:03 -0700499 if (hadSavedInstanceStateMessage(savedState)) {
500 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
501 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
Tony Mantler581edd42014-02-18 15:41:22 -0800502 message = savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700503
Andy Huang9f855d62013-05-30 17:15:03 -0700504 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800505 mRefMessage = savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700506 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700507
508 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700509 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700510 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700511 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
512 // Initialize the message from the message in the intent
Tony Mantler581edd42014-02-18 15:41:22 -0800513 message = intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700514 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800515 mRefMessage = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
516 mRefMessageUri = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
517 quotedText = null;
Andy Huang4fe0af82013-08-20 17:24:51 -0700518
519 if (Analytics.isLoggable()) {
520 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
521 Analytics.getInstance().sendEvent(
522 "notification_action", "compose", getActionString(action), 0);
523 }
524 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700525 }
Mark Wei62066e42012-09-13 12:07:02 -0700526 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700527
528 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800529 if (mAccount == null) {
530 return;
531 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700532
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700533 mUseNewChips = shouldUseNewChips();
Scott Kennedyfe853d32013-06-19 11:47:35 -0700534 initRecipients();
535
Scott Kennedy5680ec22013-01-07 13:15:20 -0800536 // Clear the notification and mark the conversation as seen, if necessary
537 final Folder notificationFolder =
538 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800539
Alan Laue806c942014-06-06 16:19:15 -0700540 if (notificationFolder != null) {
541 final Uri conversationUri = intent.getParcelableExtra(EXTRA_NOTIFICATION_CONVERSATION);
542 Intent actionIntent;
543 if (conversationUri != null) {
544 actionIntent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS_WEAR);
545 actionIntent.putExtra(Utils.EXTRA_CONVERSATION, conversationUri);
546 } else {
547 actionIntent = new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
548 actionIntent.setData(Utils.appendVersionQueryParameter(this,
549 notificationFolder.folderUri.fullUri));
550 }
551 actionIntent.setPackage(getPackageName());
552 actionIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
553 actionIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
554
555 startService(actionIntent);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800556 }
557
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700558 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
559 mLaunchedFromEmail = true;
560 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
561 final Uri dataUri = intent.getData();
562 if (dataUri != null) {
563 final String dataScheme = intent.getData().getScheme();
564 final String accountScheme = mAccount.composeIntentUri.getScheme();
565 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
566 }
567 }
568
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700569 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700570 mShowQuotedText = true;
571 mComposeMode = action;
Alan Lau15490232014-03-06 14:53:14 -0800572
573 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Alan Lauc4288f22014-05-16 11:44:27 -0700574 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
Alan Lau439aa5d2014-05-27 17:57:13 -0700575 String wearReply = null;
Alan Lauc4288f22014-05-16 11:44:27 -0700576 if (remoteInput != null) {
Alan Lau439aa5d2014-05-27 17:57:13 -0700577 LogUtils.d(LOG_TAG, "Got remote input from new api");
578 CharSequence input = remoteInput.getCharSequence(
Alan Lauc4288f22014-05-16 11:44:27 -0700579 NotificationActionUtils.WEAR_REPLY_INPUT);
Alan Lau439aa5d2014-05-27 17:57:13 -0700580 if (input != null) {
581 wearReply = input.toString();
Alan Lau15490232014-03-06 14:53:14 -0800582 }
Alan Lauc4288f22014-05-16 11:44:27 -0700583 } else {
Alan Lau439aa5d2014-05-27 17:57:13 -0700584 // TODO: remove after legacy code has been removed.
585 LogUtils.d(LOG_TAG,
586 "No remote input from new api, falling back to compatibility mode");
587 ClipData clipData = intent.getClipData();
588 if (clipData != null
589 && LEGACY_WEAR_EXTRA.equals(clipData.getDescription().getLabel())) {
590 Bundle extras = clipData.getItemAt(0).getIntent().getExtras();
591 if (extras != null) {
592 wearReply = extras.getString(NotificationActionUtils.WEAR_REPLY_INPUT);
593 }
594 }
595 }
596
597 if (!TextUtils.isEmpty(wearReply)) {
598 createWearReplyTask(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION,
599 mComposeMode, wearReply).execute();
600 finish();
601 return;
602 } else {
603 LogUtils.w(LOG_TAG, "remote input string is null");
Alan Lau15490232014-03-06 14:53:14 -0800604 }
605 }
606
Alice Yanga990a712013-03-13 18:37:00 -0700607 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700608 return;
609 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700610 initFromDraftMessage(message);
611 initQuotedTextFromRefMessage(mRefMessage, action);
Alice Yanga990a712013-03-13 18:37:00 -0700612 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700613 // if we should be showing quoted text but mRefMessage is null
614 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700615 if (mShowQuotedText && mRefMessage == null) {
616 if (quotedText != null) {
617 initQuotedText(quotedText, false /* shouldQuoteText */);
618 } else if (mExtraValues != null) {
619 initExtraValues(mExtraValues);
620 return;
621 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700622 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700623 } else if (action == EDIT_DRAFT) {
Tony Mantler581edd42014-02-18 15:41:22 -0800624 if (message == null) {
625 throw new IllegalStateException("Message must not be null to edit draft");
626 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700627 initFromDraftMessage(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700628 // Update the action to the draft type of the previous draft
629 switch (message.draftType) {
630 case UIProvider.DraftType.REPLY:
631 action = REPLY;
632 break;
633 case UIProvider.DraftType.REPLY_ALL:
634 action = REPLY_ALL;
635 break;
636 case UIProvider.DraftType.FORWARD:
637 action = FORWARD;
638 break;
639 case UIProvider.DraftType.COMPOSE:
640 default:
641 action = COMPOSE;
642 break;
643 }
Alice Yanga990a712013-03-13 18:37:00 -0700644 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
645
646 mShowQuotedText = message.appendRefMessageContent;
647 if (message.refMessageUri != null) {
648 // If we're editing an existing draft that was in reference to an existing message,
649 // still need to load that original message since we might need to refer to the
650 // original sender and recipients if user switches "reply <-> reply-all".
651 mRefMessageUri = message.refMessageUri;
652 mComposeMode = action;
653 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
654 return;
655 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700656 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
657 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800658 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700659 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700660 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700661 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700662 if (initFromExtras(intent)) {
663 return;
664 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700665 }
Alice Yanga990a712013-03-13 18:37:00 -0700666
667 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700668 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700669 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700670
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -0700671 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
Alan Lau15490232014-03-06 14:53:14 -0800672 private static AsyncTask<Void, Void, Message> createWearReplyTask(
673 final ComposeActivity composeActivity,
674 final Uri refMessageUri, final String[] projection, final int action,
675 final String wearReply) {
676 return new AsyncTask<Void, Void, Message>() {
677 private Intent mEmptyServiceIntent = new Intent(composeActivity, EmptyService.class);
678
679 @Override
680 protected void onPreExecute() {
681 // Start service so we won't be killed if this app is put in the background.
682 composeActivity.startService(mEmptyServiceIntent);
683 }
684
685 @Override
686 protected Message doInBackground(Void... params) {
687 Cursor cursor = composeActivity.getContentResolver()
688 .query(refMessageUri, projection, null, null, null, null);
689 if (cursor != null) {
690 try {
691 cursor.moveToFirst();
692 return new Message(cursor);
693 } finally {
694 cursor.close();
695 }
696 }
697 return null;
698 }
699
700 @Override
701 protected void onPostExecute(Message message) {
702 composeActivity.stopService(mEmptyServiceIntent);
703
704 composeActivity.mRefMessage = message;
705 composeActivity.initFromRefMessage(action);
706 composeActivity.setBody(wearReply, false);
707 composeActivity.finishSetup(action, composeActivity.getIntent(), null);
708 composeActivity.sendOrSaveWithSanityChecks(false /* save */, true /* show toast */,
709 false /* orientationChanged */, true /* autoSend */);
710 }
711 };
712 }
713
Mindy Pereirab199d172012-08-13 11:04:03 -0700714 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700715 final Account[] allAccounts = AccountUtils.getAccounts(this);
716 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700717 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
718 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700719 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700720 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
721 }
722 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700723 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700724 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700725 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700726 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700727 anySyncing = true;
728 break;
729 }
730 }
731 if (!anySyncing) {
732 // There are accounts, but none are sync'd, which is just like having no accounts.
733 mAccounts = null;
734 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
735 return;
736 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700737 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700738 finishCreate();
739 }
740 }
741
Mindy Pereira47d0e652012-07-23 09:45:07 -0700742 private Account obtainAccount(Intent intent) {
743 Account account = null;
744 Object accountExtra = null;
745 if (intent != null && intent.getExtras() != null) {
746 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
747 if (accountExtra instanceof Account) {
748 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700749 } else if (accountExtra instanceof String) {
750 // This is the Account attached to the widget compose intent.
Tony Mantler26a20752014-02-28 16:44:24 -0800751 account = Account.newInstance((String) accountExtra);
mindyp7ae042e2012-08-27 13:27:37 -0700752 if (account != null) {
753 return account;
754 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700755 }
mindyp5ee9dc42013-01-08 09:54:54 -0800756 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
757 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
758 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700759 }
Tony Mantler581edd42014-02-18 15:41:22 -0800760
761 MailAppProvider provider = MailAppProvider.getInstance();
762 String lastAccountUri = provider.getLastSentFromAccount();
763 if (TextUtils.isEmpty(lastAccountUri)) {
764 lastAccountUri = provider.getLastViewedAccount();
Mindy Pereira47d0e652012-07-23 09:45:07 -0700765 }
Tony Mantler581edd42014-02-18 15:41:22 -0800766 if (!TextUtils.isEmpty(lastAccountUri)) {
767 accountExtra = Uri.parse(lastAccountUri);
768 }
769
Mindy Pereirab199d172012-08-13 11:04:03 -0700770 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700771 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
772 // For backwards compatibility, we need to check account
773 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700774 for (Account a : mAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -0700775 if (a.getEmailAddress().equals(accountExtra)) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700776 account = a;
777 }
778 }
779 } else if (accountExtra instanceof Uri) {
780 // The uri of the last viewed account is what is stored in
781 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700782 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700783 if (a.uri.equals(accountExtra)) {
784 account = a;
785 }
786 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700787 }
788 if (account == null) {
789 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700790 }
791 }
792 return account;
793 }
794
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700795 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800796 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700797 // Don't bother with the intent if we have procured a message from the
798 // intent already.
799 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
800 initAttachmentsFromIntent(intent);
801 }
Alice Yanga990a712013-03-13 18:37:00 -0700802 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700803 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
804 action);
mindypd4a48662012-11-08 17:13:49 -0800805
806 // If this is a draft message, the draft account is whatever account was
807 // used to open the draft message in Compose.
808 if (mDraft != null) {
809 mDraftAccount = mReplyFromAccount;
810 }
811
Mindy Pereira75f66632012-01-11 11:42:02 -0800812 initChangeListeners();
Jin Cao32973b42014-05-06 16:12:11 -0700813
814 // These two should be identical since we check CC and BCC the same way
815 boolean showCc = !TextUtils.isEmpty(mCc.getText()) || (savedInstanceState != null &&
816 savedInstanceState.getBoolean(EXTRA_SHOW_CC));
817 boolean showBcc = !TextUtils.isEmpty(mBcc.getText()) || (savedInstanceState != null &&
818 savedInstanceState.getBoolean(EXTRA_SHOW_BCC));
819 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereira326689d2012-05-17 10:14:14 -0700820 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700821 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800822
Tony Mantler581edd42014-02-18 15:41:22 -0800823 mRespondedInline = mInnerSavedState != null &&
824 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE);
mindyp1623f9b2012-11-21 12:41:16 -0800825 if (mRespondedInline) {
826 mQuotedTextView.setVisibility(View.GONE);
827 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700828 }
829
Scott Kennedyff8553f2013-04-05 20:57:44 -0700830 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700831 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
832 }
833
Mindy Pereira71c9e562012-05-17 11:01:02 -0700834 private void updateHideOrShowQuotedText(boolean showQuotedText) {
835 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700836 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700837 }
838
839 private void setFocus(int action) {
840 if (action == EDIT_DRAFT) {
841 int type = mDraft.draftType;
842 switch (type) {
843 case UIProvider.DraftType.COMPOSE:
844 case UIProvider.DraftType.FORWARD:
845 action = COMPOSE;
846 break;
847 case UIProvider.DraftType.REPLY:
848 case UIProvider.DraftType.REPLY_ALL:
849 default:
850 action = REPLY;
851 break;
852 }
853 }
854 switch (action) {
855 case FORWARD:
856 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800857 if (TextUtils.isEmpty(mTo.getText())) {
858 mTo.requestFocus();
859 break;
860 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700861 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700862 case REPLY:
863 case REPLY_ALL:
864 default:
865 focusBody();
866 break;
867 }
868 }
869
870 /**
871 * Focus the body of the message.
872 */
Tony Mantler6a7ac782014-02-19 15:22:02 -0800873 private void focusBody() {
Mindy Pereira433b1982012-04-03 11:53:07 -0700874 mBodyView.requestFocus();
Tony Mantler6a7ac782014-02-19 15:22:02 -0800875 resetBodySelection();
876 }
Mindy Pereira433b1982012-04-03 11:53:07 -0700877
Tony Mantler6a7ac782014-02-19 15:22:02 -0800878 private void resetBodySelection() {
879 int length = mBodyView.getText().length();
Mindy Pereira433b1982012-04-03 11:53:07 -0700880 int signatureStartPos = getSignatureStartPosition(
881 mSignature, mBodyView.getText().toString());
882 if (signatureStartPos > -1) {
883 // In case the user deleted the newlines...
884 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700885 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700886 // Move cursor to the end.
887 mBodyView.setSelection(length);
888 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800889 }
890
891 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700892 protected void onStart() {
893 super.onStart();
894
895 Analytics.getInstance().activityStart(this);
896 }
897
898 @Override
899 protected void onStop() {
900 super.onStop();
901
902 Analytics.getInstance().activityStop(this);
903 }
904
905 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800906 protected void onResume() {
907 super.onResume();
908 // Update the from spinner as other accounts
909 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800910 if (mFromSpinner != null && mAccount != null) {
Andrew Sappersteina01ddca2014-03-04 10:59:56 -0800911 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800912 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800913 }
914
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800915 @Override
916 protected void onPause() {
917 super.onPause();
918
Mindy Pereiraa2148332012-07-02 13:54:14 -0700919 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700920 // Don't save unnecessary drafts if we are only changing the orientation.
921 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700922 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700923
Andy Huange003b4c2013-08-16 10:32:05 -0700924 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700925 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
926 // because that method can be invoked many times in a single compose session.)
927 logSendOrSave(true /* save */);
928 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700929 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800930 }
931
932 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700933 protected void onActivityResult(int request, int result, Intent data) {
Andy Huang5f082212014-06-11 22:19:21 -0700934 if (request == RESULT_PICK_ATTACHMENT) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700935 mAddingAttachment = false;
Andy Huang5f082212014-06-11 22:19:21 -0700936 if (result == RESULT_OK) {
937 addAttachmentAndUpdateView(data);
938 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700939 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700940 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700941 if (result != RESULT_OK) {
942 finish();
943 } else {
944 // Watch for accounts to show up!
945 // restart the loader to get the updated list of accounts
946 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
947 showWaitFragment(null);
948 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800949 }
950 }
951
952 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700953 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700954 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
955 if (hasAccounts) {
956 clearChangeListeners();
957 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700958 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700959 if (mInnerSavedState != null) {
960 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
961 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
962 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700963 // There should be a focus and it should be an EditText since we
964 // only save these extras if these conditions are true.
965 EditText focusEditText = (EditText) getCurrentFocus();
966 final int length = focusEditText.getText().length();
967 if (selectionStart < length && selectionEnd < length) {
968 focusEditText.setSelection(selectionStart, selectionEnd);
969 }
970 }
971 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700972 if (hasAccounts) {
973 initChangeListeners();
974 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700975 }
976
977 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700978 protected void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800979 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700980 final Bundle inner = new Bundle();
981 saveState(inner);
982 state.putBundle(KEY_INNER_SAVED_STATE, inner);
983 }
984
985 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700986 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
987 if (mAccounts == null || mAccounts.length == 0) {
988 return;
989 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700990 // The framework is happy to save and restore the selection but only if it also saves and
991 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
992 // this manually.
993 View focus = getCurrentFocus();
994 if (focus != null && focus instanceof EditText) {
995 EditText focusEditText = (EditText) focus;
996 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
997 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
998 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700999
1000 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -07001001 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -07001002 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
1003 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
1004 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001005 if (selectedReplyFromAccount != null) {
1006 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
1007 .toString());
1008 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
1009 } else {
1010 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
1011 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001012
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001013 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
1014 // We don't have a draft id, and we have a request id,
1015 // save the request id.
1016 state.putInt(EXTRA_REQUEST_ID, mRequestId);
1017 }
1018
1019 // We want to restore the current mode after a pause
1020 // or rotation.
1021 int mode = getMode();
1022 state.putInt(EXTRA_ACTION, mode);
1023
Jin Cao77b4c2c2014-05-20 13:55:53 -07001024 final Message message = createMessage(selectedReplyFromAccount, mRefMessage, mode,
1025 removeComposingSpans(mBodyView.getText()));
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001026 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -08001027 message.id = mDraft.id;
1028 message.serverId = mDraft.serverId;
1029 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001030 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001031 state.putParcelable(EXTRA_MESSAGE, message);
1032
1033 if (mRefMessage != null) {
1034 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001035 } else if (message.appendRefMessageContent) {
1036 // If we have no ref message but should be appending
1037 // ref message content, we have orphaned quoted text. Save it.
1038 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001039 }
Mindy Pereira326689d2012-05-17 10:14:14 -07001040 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
1041 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -08001042 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -08001043 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -07001044 state.putParcelableArrayList(
1045 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -07001046
1047 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001048 }
1049
1050 private int getMode() {
1051 int mode = ComposeActivity.COMPOSE;
1052 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001053 if (actionBar != null
1054 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001055 mode = actionBar.getSelectedNavigationIndex();
1056 }
1057 return mode;
1058 }
1059
Jin Cao77b4c2c2014-05-20 13:55:53 -07001060 /**
1061 * This function might be called from a background thread, so be sure to move everything that
1062 * can potentially modify the UI to the main thread (e.g. removeComposingSpans for body).
1063 */
Anthony Lee2a3cc132014-04-22 14:15:25 -07001064 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, Message refMessage,
Jin Cao77b4c2c2014-05-20 13:55:53 -07001065 int mode, Spanned body) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001066 Message message = new Message();
1067 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -07001068 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001069 message.uri = null;
1070 message.conversationUri = null;
1071 message.subject = mSubject.getText().toString();
1072 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001073 message.setTo(formatSenders(mTo.getText().toString()));
1074 message.setCc(formatSenders(mCc.getText().toString()));
1075 message.setBcc(formatSenders(mBcc.getText().toString()));
1076 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001077 message.dateReceivedMs = 0;
Jin Cao77b4c2c2014-05-20 13:55:53 -07001078 message.bodyHtml = spannedBodyToHtml(body, true);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001079 message.bodyText = mBodyView.getText().toString();
1080 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -07001081 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001082 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
1083 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
1084 message.hasAttachments = attachments != null && attachments.size() > 0;
1085 message.attachmentListUri = null;
1086 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001087 message.alwaysShowImages = false;
1088 message.attachmentsJson = Attachment.toJSONArray(attachments);
1089 CharSequence quotedText = mQuotedTextView.getQuotedText();
Anthony Lee2a3cc132014-04-22 14:15:25 -07001090 message.quotedTextOffset = -1; // Just a default value.
1091 if (refMessage != null && !TextUtils.isEmpty(quotedText)) {
1092 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1093 // We want the index to point to just the quoted text and not the
1094 // "On December 25, 2014..." part of it.
1095 message.quotedTextOffset =
1096 QuotedTextView.getQuotedTextOffset(quotedText.toString());
1097 } else if (!TextUtils.isEmpty(refMessage.bodyText)) {
1098 // We want to point to the entire quoted text.
1099 message.quotedTextOffset = QuotedTextView.findQuotedTextIndex(quotedText);
1100 }
1101 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001102 message.accountUri = null;
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001103 final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
1104 : mAccount != null ? mAccount.getEmailAddress() : null;
Tony Mantlerf441d142013-10-22 11:46:00 -07001105 final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
1106 : mAccount != null ? mAccount.getSenderName() : null;
Tony Mantler821e5782014-01-06 15:33:43 -08001107 final Address address = new Address(email, senderName);
Tony Mantlerf441d142013-10-22 11:46:00 -07001108 message.setFrom(address.toHeader());
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001109 message.draftType = getDraftType(mode);
mindype7b76aa2012-11-14 16:19:13 -08001110 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001111 }
1112
Scott Kennedyff8553f2013-04-05 20:57:44 -07001113 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -07001114 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
1115 return string.substring(0, string.length() - 1);
1116 }
1117 return string;
1118 }
1119
Mindy Pereira818143e2012-01-11 13:59:49 -08001120 @VisibleForTesting
Andy Huang91ede362014-01-21 19:16:00 -08001121 protected void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -07001122 if (account == null) {
1123 return;
1124 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001125 if (!account.equals(mAccount)) {
1126 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001127 mCachedSettings = mAccount.settings;
1128 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001129 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001130 if (mAccount != null) {
Tony Mantler79b11562013-10-09 15:31:50 -07001131 MailActivity.setNfcMessage(mAccount.getEmailAddress());
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001132 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001133 }
1134
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001135 private void initFromSpinner(Bundle bundle, int action) {
1136 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001137 action = COMPOSE;
1138 }
Andrew Sappersteina01ddca2014-03-04 10:59:56 -08001139 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001140
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001141 if (bundle != null) {
1142 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
1143 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
1144 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
1145 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001146 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001147 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
1148 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001149 }
1150 if (mReplyFromAccount == null) {
1151 if (mDraft != null) {
1152 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
1153 } else if (mRefMessage != null) {
1154 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
1155 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001156 }
1157 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -07001158 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001159 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001160
Mindy Pereira62de1b12012-04-06 12:17:56 -07001161 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001162
Mindy Pereira62de1b12012-04-06 12:17:56 -07001163 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001164 // If there is only 1 account, just show that account.
1165 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001166 // send mail from / save drafts to.
1167 mFromStatic.setVisibility(View.GONE);
Andy Huangca4676f2014-01-16 13:22:20 -08001168 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001169 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001170 } else {
1171 mFromStatic.setVisibility(View.VISIBLE);
Andy Huangca4676f2014-01-16 13:22:20 -08001172 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001173 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001174 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001175 }
1176
Mindy Pereira62de1b12012-04-06 12:17:56 -07001177 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1178 if (refMessage.accountUri != null) {
1179 // This must be from combined inbox.
1180 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1181 for (ReplyFromAccount from : replyFromAccounts) {
1182 if (from.account.uri.equals(refMessage.accountUri)) {
1183 return from;
1184 }
1185 }
1186 return null;
1187 } else {
1188 return getReplyFromAccount(account, refMessage);
1189 }
1190 }
1191
1192 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001193 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001194 * return who the message should be sent from.
1195 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001196 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001197 * @return the address from which to reply.
1198 */
1199 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1200 // First see if we are supposed to use the default address or
1201 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001202 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001203 return getDefaultReplyFromAccount(account);
1204 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001205 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001206 // all the message recipients and find one that matches
1207 // a custom from or account.
1208 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001209 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1210 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001211 return getMatchingRecipient(account, allRecipients);
1212 }
1213 }
1214
1215 /**
1216 * Compare all the recipients of an email to the current account and all
1217 * custom addresses associated with that account. Return the match if there
1218 * is one, or the default account if there isn't.
1219 */
1220 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1221 // Tokenize the list and place in a hashmap.
1222 ReplyFromAccount matchingReplyFrom = null;
1223 Rfc822Token[] tokens;
1224 HashSet<String> recipientsMap = new HashSet<String>();
1225 for (String address : sentTo) {
1226 tokens = Rfc822Tokenizer.tokenize(address);
Tony Mantler581edd42014-02-18 15:41:22 -08001227 for (final Rfc822Token token : tokens) {
1228 recipientsMap.add(token.getAddress());
Mindy Pereira62de1b12012-04-06 12:17:56 -07001229 }
1230 }
1231
1232 int matchingAddressCount = 0;
1233 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001234 customFroms = account.getReplyFroms();
1235 if (customFroms != null) {
1236 for (ReplyFromAccount entry : customFroms) {
1237 if (recipientsMap.contains(entry.address)) {
1238 matchingReplyFrom = entry;
1239 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001240 }
1241 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001242 }
1243 if (matchingAddressCount > 1) {
1244 matchingReplyFrom = getDefaultReplyFromAccount(account);
1245 }
1246 return matchingReplyFrom;
1247 }
1248
Scott Kennedyff8553f2013-04-05 20:57:44 -07001249 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1250 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001251 if (from.isDefault) {
1252 return from;
1253 }
1254 }
Tony Mantlerf441d142013-10-22 11:46:00 -07001255 return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1256 account.getSenderName(), account.getEmailAddress(), true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001257 }
1258
Tony Mantlerf441d142013-10-22 11:46:00 -07001259 private ReplyFromAccount getReplyFromAccountFromDraft(final Account account,
1260 final Message msg) {
1261 final Address[] draftFroms = Address.parse(msg.getFrom());
1262 final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
Mindy Pereira62de1b12012-04-06 12:17:56 -07001263 ReplyFromAccount replyFromAccount = null;
1264 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Tony Mantler79b11562013-10-09 15:31:50 -07001265 if (TextUtils.equals(account.getEmailAddress(), sender)) {
Tony Mantlerf441d142013-10-22 11:46:00 -07001266 replyFromAccount = getDefaultReplyFromAccount(account);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001267 } else {
1268 for (ReplyFromAccount fromAccount : replyFromAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -07001269 if (TextUtils.equals(fromAccount.address, sender)) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001270 replyFromAccount = fromAccount;
1271 break;
1272 }
1273 }
1274 }
1275 return replyFromAccount;
1276 }
1277
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001278 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001279 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001280 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001281 if (mCcBccButton != null) {
1282 mCcBccButton.setOnClickListener(this);
1283 }
1284 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001285 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Tony Mantler581edd42014-02-18 15:41:22 -08001286 final View addAttachmentsButton = findViewById(R.id.add_attachment);
1287 if (addAttachmentsButton != null) {
1288 addAttachmentsButton.setOnClickListener(this);
mindypcd0b0b92012-08-23 14:33:17 -07001289 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001290 mTo = (RecipientEditTextView) findViewById(R.id.to);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001291 initializeRecipientEditTextView(mTo);
Mindy Pereira818143e2012-01-11 13:59:49 -08001292 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001293 initializeRecipientEditTextView(mCc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001294 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001295 initializeRecipientEditTextView(mBcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001296 // TODO: add special chips text change watchers before adding
1297 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001298 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001299 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001300 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1301 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001302 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001303 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001304 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001305 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001306 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001307 }
1308
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001309 private void initializeRecipientEditTextView(RecipientEditTextView view) {
1310 view.setTokenizer(new Rfc822Tokenizer());
1311 view.setThreshold(COMPLETION_THRESHOLD);
1312 }
1313
mindyp62d3ec72012-08-24 13:04:09 -07001314 @Override
1315 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1316 if (action == EditorInfo.IME_ACTION_DONE) {
1317 focusBody();
1318 return true;
1319 }
1320 return false;
1321 }
1322
Andy Huang91ede362014-01-21 19:16:00 -08001323 /**
1324 * Convert the body text (in {@link Spanned} form) to ready-to-send HTML format as a plain
1325 * String.
1326 *
1327 * @param body the body text including fancy style spans
Jin Cao77b4c2c2014-05-20 13:55:53 -07001328 * @param removedComposing whether the function already removed composingSpans. Necessary
1329 * because we cannot call removeComposingSpans from a background thread.
Andy Huang91ede362014-01-21 19:16:00 -08001330 * @return HTML formatted body that's suitable for sending or saving
1331 */
Jin Cao77b4c2c2014-05-20 13:55:53 -07001332 private String spannedBodyToHtml(Spanned body, boolean removedComposing) {
1333 if (!removedComposing) {
1334 body = removeComposingSpans(body);
1335 }
1336 final HtmlifyBeginResult r = onHtmlifyBegin(body);
Andy Huang91ede362014-01-21 19:16:00 -08001337 return onHtmlifyEnd(Html.toHtml(r.result), r.extras);
1338 }
1339
1340 /**
1341 * A hook for subclasses to convert custom spans in the body text prior to system HTML
1342 * conversion. That HTML conversion is lossy, so anything above and beyond its capability
1343 * has to be handled here.
1344 *
1345 * @param body
1346 * @return a copy of the body text with custom spans replaced with HTML
1347 */
1348 protected HtmlifyBeginResult onHtmlifyBegin(Spanned body) {
1349 return new HtmlifyBeginResult(body, null /* extras */);
1350 }
1351
1352 protected String onHtmlifyEnd(String html, Object extras) {
1353 return html;
1354 }
1355
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001356 protected TextView getBody() {
1357 return mBodyView;
1358 }
1359
1360 @VisibleForTesting
Andy Huang0a2a3462013-12-20 15:56:13 -08001361 public String getBodyHtml() {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001362 return spannedBodyToHtml(mBodyView.getText(), false);
Andy Huang0a2a3462013-12-20 15:56:13 -08001363 }
1364
1365 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001366 public Account getFromAccount() {
1367 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1368 mReplyFromAccount.account : mAccount;
1369 }
1370
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001371 private void clearChangeListeners() {
1372 mSubject.removeTextChangedListener(this);
1373 mBodyView.removeTextChangedListener(this);
1374 mTo.removeTextChangedListener(mToListener);
1375 mCc.removeTextChangedListener(mCcListener);
1376 mBcc.removeTextChangedListener(mBccListener);
1377 mFromSpinner.setOnAccountChangedListener(null);
1378 mAttachmentsView.setAttachmentChangesListener(null);
1379 }
1380
Mindy Pereira75f66632012-01-11 11:42:02 -08001381 // Now that the message has been initialized from any existing draft or
1382 // ref message data, set up listeners for any changes that occur to the
1383 // message.
1384 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001385 // Make sure we only add text changed listeners once!
1386 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001387 mSubject.addTextChangedListener(this);
1388 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001389 if (mToListener == null) {
1390 mToListener = new RecipientTextWatcher(mTo, this);
1391 }
1392 mTo.addTextChangedListener(mToListener);
1393 if (mCcListener == null) {
1394 mCcListener = new RecipientTextWatcher(mCc, this);
1395 }
1396 mCc.addTextChangedListener(mCcListener);
1397 if (mBccListener == null) {
1398 mBccListener = new RecipientTextWatcher(mBcc, this);
1399 }
1400 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001401 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001402 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001403 }
1404
Alice Yanga990a712013-03-13 18:37:00 -07001405 private void initActionBar() {
1406 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001407 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001408 if (actionBar == null) {
1409 return;
1410 }
Alice Yanga990a712013-03-13 18:37:00 -07001411 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001412 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1413 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001414 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001415 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001416 if (mComposeModeAdapter == null) {
1417 mComposeModeAdapter = new ComposeModeAdapter(this);
1418 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001419 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1420 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001421 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001422 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001423 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001424 break;
1425 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001426 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001427 break;
1428 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001429 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001430 break;
1431 }
1432 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001433 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1434 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1435 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001436 }
1437
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001438 private void initFromRefMessage(int action) {
1439 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001440
1441 // Check if To: address and email body needs to be prefilled based on extras.
1442 // This is used for reporting rendering feedback.
1443 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1444 Intent intent = getIntent();
1445 if (intent.getExtras() != null) {
1446 String toAddresses = intent.getStringExtra(EXTRA_TO);
1447 if (toAddresses != null) {
1448 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1449 }
1450 String body = intent.getStringExtra(EXTRA_BODY);
1451 if (body != null) {
1452 setBody(body, false /* withSignature */);
1453 }
1454 }
1455 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001456 }
1457
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001458 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001459 setSubject(mRefMessage, action);
1460 // Setup recipients
1461 if (action == FORWARD) {
1462 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001463 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001464 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001465 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001466 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1467 initAttachments(mRefMessage);
1468 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001469 }
1470
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001471 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001472 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001473
1474 mDraft = message;
1475 mDraftId = message.id;
1476 mSubject.setText(message.subject);
1477 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001478 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001479 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001480 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1481 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001482 if (message.hasAttachments) {
1483 List<Attachment> attachments = message.getAttachments();
1484 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001485 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001486 }
1487 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07001488 int quotedTextIndex = message.appendRefMessageContent ? message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001489 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001490 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001491 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira002ff522012-05-30 10:31:26 -07001492 if (quotedTextIndex > -1) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07001493 // Find the offset in the html text of the actual quoted text and strip it out.
1494 // Note that the actual quotedTextOffset in the message has not changed as
1495 // this different offset is used only for display purposes. They point to different
1496 // parts of the original message. Please see the comments in QuoteTextView
1497 // to see the differences.
Mindy Pereira752222d2012-07-19 09:58:53 -07001498 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1499 if (quotedTextIndex > -1) {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001500 new HtmlToSpannedTask().execute(message.bodyHtml.substring(0, quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001501 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1502 message.bodyHtml.length());
1503 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001504 } else {
Jin Cao77b4c2c2014-05-20 13:55:53 -07001505 new HtmlToSpannedTask().execute(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001506 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001507 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001508 final String body = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07001509 final CharSequence bodyText;
1510 if (TextUtils.isEmpty(body)) {
1511 bodyText = "";
1512 quotedText = null;
1513 } else {
1514 if (quotedTextIndex > body.length()) {
1515 // Sanity check to guarantee that we will not over index the String.
1516 // If this happens there is a bigger problem. This should never happen hence
1517 // the wtf logging.
1518 quotedTextIndex = -1;
1519 LogUtils.wtf(LOG_TAG, "quotedTextIndex (%d) > body.length() (%d)",
1520 quotedTextIndex, body.length());
1521 }
1522 bodyText = quotedTextIndex > -1 ? body.substring(0, quotedTextIndex) : body;
1523 if (quotedTextIndex > -1) {
1524 quotedText = body.substring(quotedTextIndex);
1525 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001526 }
1527 mBodyView.setText(bodyText);
1528 }
1529 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001530 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001531 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001532 }
1533
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001534 /**
1535 * Fill all the widgets with the content found in the Intent Extra, if any.
1536 * Also apply the same style to all widgets. Note: if initFromExtras is
1537 * called as a result of switching between reply, reply all, and forward per
1538 * the latest revision of Gmail, and the user has already made changes to
1539 * attachments on a previous incarnation of the message (as a reply, reply
1540 * all, or forward), the original attachments from the message will not be
1541 * re-instantiated. The user's changes will be respected. This follows the
1542 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001543 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001544 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001545 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001546 // If we were invoked with a SENDTO intent, the value
1547 // should take precedence
1548 final Uri dataUri = intent.getData();
1549 if (dataUri != null) {
1550 if (MAIL_TO.equals(dataUri.getScheme())) {
1551 initFromMailTo(dataUri.toString());
1552 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001553 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001554 String toText = dataUri.getSchemeSpecificPart();
1555 if (toText != null) {
1556 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001557 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001558 }
1559 }
1560 }
1561 }
1562
1563 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1564 if (extraStrings != null) {
1565 addToAddresses(Arrays.asList(extraStrings));
1566 }
1567 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1568 if (extraStrings != null) {
1569 addCcAddresses(Arrays.asList(extraStrings), null);
1570 }
1571 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1572 if (extraStrings != null) {
1573 addBccAddresses(Arrays.asList(extraStrings));
1574 }
1575
1576 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1577 if (extraString != null) {
1578 mSubject.setText(extraString);
1579 }
1580
1581 for (String extra : ALL_EXTRAS) {
1582 if (intent.hasExtra(extra)) {
1583 String value = intent.getStringExtra(extra);
1584 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001585 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001586 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001587 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001588 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001589 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001590 } else if (EXTRA_SUBJECT.equals(extra)) {
1591 mSubject.setText(value);
1592 } else if (EXTRA_BODY.equals(extra)) {
1593 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001594 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1595 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001596 }
1597 }
1598 }
1599
1600 Bundle extras = intent.getExtras();
1601 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001602 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1603 if (text != null) {
1604 setBody(text, true /* with signature */);
1605 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001606
1607 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001608 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001609
1610 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1611 if (mExtraValues != null) {
1612 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1613 initExtraValues(mExtraValues);
1614 return true;
1615 }
1616
1617 return false;
1618 }
1619
1620 protected void initExtraValues(ContentValues extraValues) {
1621 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001622 }
1623
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001624
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001625 @VisibleForTesting
1626 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001627 // TODO: handle the case where there are spaces in the display name as
1628 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1629 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001630 // Since URLDecode.decode changes + into ' ', and + is a valid
1631 // email character, we need to find/ replace these ourselves before
1632 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001633 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001634 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001635 } catch (IllegalArgumentException e) {
1636 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1637 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1638 } else {
1639 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1640 }
1641 return null;
1642 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001643 }
1644
1645 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001646 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1647 * changing '+' into ' '
1648 *
1649 * @param toReplace Input string
1650 * @return The string with all "+" characters replaced with "%2B"
1651 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001652 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001653 return toReplace.replace("+", "%2B");
1654 }
1655
1656 /**
Jin Caod67d7e32014-03-26 16:49:48 -07001657 * Replaces all occurrences of '%' with "%25", to prevent URLDecode.decode from
1658 * crashing on decoded '%' symbols
1659 *
1660 * @param toReplace Input string
1661 * @return The string with all "%" characters replaced with "%25"
1662 */
1663 private static String replacePercent(String toReplace) {
1664 return toReplace.replace("%", "%25");
1665 }
1666
1667 /**
1668 * Helper function to encapsulate encoding/decoding string from Uri.getQueryParameters
1669 * @param content Input string
1670 * @return The string that's properly escaped to be shown in mail subject/content
1671 */
1672 private static String decodeContentFromQueryParam(String content) {
1673 try {
1674 return URLDecoder.decode(replacePlus(replacePercent(content)), UTF8_ENCODING_NAME);
1675 } catch (UnsupportedEncodingException e) {
1676 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), content);
1677 return ""; // Default to empty string so setText/setBody has same behavior as before.
1678 }
1679 }
1680
1681 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001682 * Initialize the compose view from a String representing a mailTo uri.
1683 * @param mailToString The uri as a string.
1684 */
1685 public void initFromMailTo(String mailToString) {
1686 // We need to disguise this string as a URI in order to parse it
1687 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1688 Uri uri = Uri.parse("foo://" + mailToString);
1689 int index = mailToString.indexOf("?");
1690 int length = "mailto".length() + 1;
1691 String to;
1692 try {
1693 // Extract the recipient after mailto:
1694 if (index == -1) {
1695 to = decodeEmailInUri(mailToString.substring(length));
1696 } else {
1697 to = decodeEmailInUri(mailToString.substring(length, index));
1698 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001699 if (!TextUtils.isEmpty(to)) {
1700 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1701 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001702 } catch (UnsupportedEncodingException e) {
1703 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1704 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1705 } else {
1706 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1707 }
1708 }
1709
1710 List<String> cc = uri.getQueryParameters("cc");
1711 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1712
1713 List<String> otherTo = uri.getQueryParameters("to");
1714 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1715
1716 List<String> bcc = uri.getQueryParameters("bcc");
1717 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1718
Jin Caod67d7e32014-03-26 16:49:48 -07001719 // NOTE: Uri.getQueryParameters already decodes % encoded characters
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001720 List<String> subject = uri.getQueryParameters("subject");
1721 if (subject.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001722 mSubject.setText(decodeContentFromQueryParam(subject.get(0)));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001723 }
1724
1725 List<String> body = uri.getQueryParameters("body");
1726 if (body.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001727 setBody(decodeContentFromQueryParam(body.get(0)), true /* with signature */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001728 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001729 }
1730
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001731 @VisibleForTesting
1732 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001733 addAttachments(refMessage.getAttachments());
1734 }
1735
1736 public long addAttachments(List<Attachment> attachments) {
1737 long size = 0;
1738 AttachmentFailureException error = null;
1739 for (Attachment a : attachments) {
1740 try {
1741 size += mAttachmentsView.addAttachment(mAccount, a);
1742 } catch (AttachmentFailureException e) {
1743 error = e;
1744 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001745 }
Mark Wei434f2942012-08-24 11:54:02 -07001746 if (error != null) {
1747 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1748 if (attachments.size() > 1) {
1749 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1750 } else {
1751 showAttachmentTooBigToast(error.getErrorRes());
1752 }
1753 }
1754 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001755 }
1756
1757 /**
1758 * When an attachment is too large to be added to a message, show a toast.
1759 * This method also updates the position of the toast so that it is shown
1760 * clearly above they keyboard if it happens to be open.
1761 */
Mark Wei434f2942012-08-24 11:54:02 -07001762 private void showAttachmentTooBigToast(int errorRes) {
1763 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1764 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1765 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001766 }
1767
Mark Wei434f2942012-08-24 11:54:02 -07001768 private void showErrorToast(String message) {
1769 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1770 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001771 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1772 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1773 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001774 }
1775
Paul Westbrookf97588b2012-03-20 11:11:37 -07001776 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001777 Bundle extras = intent.getExtras();
1778 if (extras == null) {
1779 extras = Bundle.EMPTY;
1780 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001781 final String action = intent.getAction();
1782 if (!mAttachmentsChanged) {
1783 long totalSize = 0;
1784 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1785 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1786 for (String uriString : uris) {
1787 final Uri uri = Uri.parse(uriString);
1788 long size = 0;
1789 try {
Andy Huang91ede362014-01-21 19:16:00 -08001790 if (handleSpecialAttachmentUri(uri)) {
1791 continue;
1792 }
1793
Andy Huange003b4c2013-08-16 10:32:05 -07001794 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1795 size = mAttachmentsView.addAttachment(mAccount, a);
1796
1797 Analytics.getInstance().sendEvent("send_intent_attachment",
1798 Utils.normalizeMimeType(a.getContentType()), null, size);
1799
Paul Westbrookf97588b2012-03-20 11:11:37 -07001800 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001801 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001802 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001803 }
1804 totalSize += size;
1805 }
1806 }
mindyp9a9e8d62012-10-03 12:24:07 -07001807 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1808 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
Andy Huang91ede362014-01-21 19:16:00 -08001809 final ArrayList<Uri> uris = extras
mindyp9a9e8d62012-10-03 12:24:07 -07001810 .getParcelableArrayList(Intent.EXTRA_STREAM);
1811 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
Andy Huang91ede362014-01-21 19:16:00 -08001812 for (Uri uri : uris) {
mindyp9a9e8d62012-10-03 12:24:07 -07001813 try {
Andy Huang91ede362014-01-21 19:16:00 -08001814 if (handleSpecialAttachmentUri(uri)) {
1815 continue;
1816 }
1817
1818 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
Andy Huange003b4c2013-08-16 10:32:05 -07001819 attachments.add(a);
1820
1821 Analytics.getInstance().sendEvent("send_intent_attachment",
1822 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1823
mindyp9a9e8d62012-10-03 12:24:07 -07001824 } catch (AttachmentFailureException e) {
1825 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1826 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1827 getApplicationContext(),
1828 mAccount.settings.getMaxAttachmentSize());
1829 showErrorToast(getString
1830 (R.string.generic_attachment_problem, maxSize));
1831 }
1832 }
1833 totalSize += addAttachments(attachments);
1834 } else {
Tony Mantler581edd42014-02-18 15:41:22 -08001835 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
mindyp9a9e8d62012-10-03 12:24:07 -07001836 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001837 try {
Andy Huang91ede362014-01-21 19:16:00 -08001838 if (!handleSpecialAttachmentUri(uri)) {
1839 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1840 size = mAttachmentsView.addAttachment(mAccount, a);
Andy Huange003b4c2013-08-16 10:32:05 -07001841
Andy Huang91ede362014-01-21 19:16:00 -08001842 Analytics.getInstance().sendEvent("send_intent_attachment",
1843 Utils.normalizeMimeType(a.getContentType()), null, size);
1844 }
Andy Huange003b4c2013-08-16 10:32:05 -07001845
Paul Westbrookf97588b2012-03-20 11:11:37 -07001846 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001847 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001848 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001849 }
mindyp9a9e8d62012-10-03 12:24:07 -07001850 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001851 }
1852 }
1853
1854 if (totalSize > 0) {
1855 mAttachmentsChanged = true;
1856 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001857
1858 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1859 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001860 }
1861 }
1862 }
1863
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001864 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001865 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1866 mShowQuotedText = true;
1867 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001868
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001869 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1870 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001871 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1872 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001873 }
1874
1875 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001876 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001877 boolean ccVisible = mCcBccView.isCcVisible();
1878 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001879 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001880 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001881 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001882 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001883 : R.string.add_bcc_label));
1884 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001885 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001886 }
1887 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001888 }
1889
Mindy Pereira013194c2012-01-06 15:09:33 -08001890 /**
1891 * Add attachment and update the compose area appropriately.
Mindy Pereira013194c2012-01-06 15:09:33 -08001892 */
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001893 private void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07001894 if (data == null) {
1895 return;
1896 }
1897
1898 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1899 final ClipData clipData = data.getClipData();
1900 if (clipData != null) {
1901 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1902 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1903 }
1904 return;
1905 }
1906 }
1907
1908 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07001909 }
1910
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001911 private void addAttachmentAndUpdateView(Uri contentUri) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001912 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001913 return;
1914 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001915 try {
Andy Huang91ede362014-01-21 19:16:00 -08001916
1917 if (handleSpecialAttachmentUri(contentUri)) {
1918 return;
1919 }
1920
Andy Huang5c5fd572012-04-08 18:19:29 -07001921 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1922 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001923 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001924 showErrorToast(getResources().getString(
1925 e.getErrorRes(),
1926 AttachmentUtils.convertToHumanReadableSize(
1927 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001928 }
1929 }
1930
Andy Huang91ede362014-01-21 19:16:00 -08001931 /**
1932 * Allow subclasses to implement custom handling of attachments.
1933 *
1934 * @param contentUri a passed-in URI from a pick intent
1935 * @return true iff handled
1936 */
1937 protected boolean handleSpecialAttachmentUri(final Uri contentUri) {
1938 return false;
1939 }
1940
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001941 private void addAttachmentAndUpdateView(Attachment attachment) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001942 try {
Mark Wei434f2942012-08-24 11:54:02 -07001943 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001944 if (size > 0) {
1945 mAttachmentsChanged = true;
1946 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001947 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001948 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001949 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001950 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001951 }
1952 }
1953
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001954 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001955 // Don't populate the address if this is a forward.
1956 if (action == ComposeActivity.FORWARD) {
1957 return;
1958 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001959 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001960 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001961
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001962 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1963 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001964 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001965 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001966 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001967 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001968 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1969 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
Andy Huange2af8872014-01-16 12:36:27 -08001970 final String[] replyToAddresses = getReplyToAddresses(
1971 refMessage.getReplyToAddressesUnescaped(), fromAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001972
1973 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1974 // Cc list is the union of the To and Cc recipients of the original
1975 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001976 // already on the To list.
1977 if (action == ComposeActivity.REPLY) {
Tony Mantler24f116f2014-01-16 10:20:50 -08001978 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001979 addToAddresses(toAddresses);
1980 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001981 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08001982 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001983 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001984 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001985 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001986 addCcAddresses(ccAddresses, toAddresses);
1987 }
1988 }
1989
Andy Huange2af8872014-01-16 12:36:27 -08001990 // If there is no reply to address, the reply to address is the sender.
1991 private static String[] getReplyToAddresses(String[] replyTo, String from) {
1992 boolean hasReplyTo = false;
1993 for (final String replyToAddress : replyTo) {
1994 if (!TextUtils.isEmpty(replyToAddress)) {
1995 hasReplyTo = true;
1996 }
1997 }
1998 return hasReplyTo ? replyTo : new String[] {from};
1999 }
2000
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002001 private void addToAddresses(Collection<String> addresses) {
2002 addAddressesToList(addresses, mTo);
2003 }
2004
2005 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002006 addCcAddressesToList(tokenizeAddressList(addresses),
2007 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002008 }
2009
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07002010 private void addBccAddresses(Collection<String> addresses) {
2011 addAddressesToList(addresses, mBcc);
2012 }
2013
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002014 @VisibleForTesting
2015 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
2016 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
2017 String address;
2018
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002019 if (compareToList == null) {
Tony Mantler581edd42014-02-18 15:41:22 -08002020 for (final Rfc822Token[] tokens : addresses) {
2021 for (final Rfc822Token token : tokens) {
2022 address = token.toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002023 list.append(address + END_TOKEN);
2024 }
2025 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002026 } else {
2027 HashSet<String> compareTo = convertToHashSet(compareToList);
Tony Mantler581edd42014-02-18 15:41:22 -08002028 for (final Rfc822Token[] tokens : addresses) {
2029 for (final Rfc822Token token : tokens) {
2030 address = token.toString();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002031 // Check if this is a duplicate:
Tony Mantler581edd42014-02-18 15:41:22 -08002032 if (!compareTo.contains(token.getAddress())) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002033 // Get the address here
2034 list.append(address + END_TOKEN);
2035 }
2036 }
2037 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002038 }
2039 }
2040
Scott Kennedyff8553f2013-04-05 20:57:44 -07002041 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
2042 final HashSet<String> hash = new HashSet<String>();
2043 for (final Rfc822Token[] tokens : list) {
Tony Mantler581edd42014-02-18 15:41:22 -08002044 for (final Rfc822Token token : tokens) {
2045 hash.add(token.getAddress());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002046 }
2047 }
2048 return hash;
2049 }
2050
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002051 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
2052 @VisibleForTesting
2053 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
2054
2055 for (String address: addresses) {
2056 tokenized.add(Rfc822Tokenizer.tokenize(address));
2057 }
2058 return tokenized;
2059 }
2060
2061 @VisibleForTesting
2062 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
2063 for (String address : addresses) {
2064 addAddressToList(address, list);
2065 }
2066 }
2067
Scott Kennedyff8553f2013-04-05 20:57:44 -07002068 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002069 if (address == null || list == null)
2070 return;
2071
Scott Kennedyff8553f2013-04-05 20:57:44 -07002072 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002073
Tony Mantler581edd42014-02-18 15:41:22 -08002074 for (final Rfc822Token token : tokens) {
2075 list.append(token + END_TOKEN);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002076 }
2077 }
2078
2079 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07002080 protected Collection<String> initToRecipients(final String fullSenderAddress,
Tony Mantler24f116f2014-01-16 10:20:50 -08002081 final String[] replyToAddresses, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002082 // The To recipient is the reply-to address specified in the original
2083 // message, unless it is:
2084 // the current user OR a custom from of the current user, in which case
2085 // it's the To recipient list of the original message.
2086 // OR missing, in which case use the sender of the original message
2087 Set<String> toAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002088 for (final String replyToAddress : replyToAddresses) {
2089 if (!TextUtils.isEmpty(replyToAddress)
2090 && !recipientMatchesThisAccount(replyToAddress)) {
2091 toAddresses.add(replyToAddress);
2092 }
2093 }
2094 if (toAddresses.size() == 0) {
mindyp65b06f52012-11-21 10:35:08 -08002095 // In this case, the user is replying to a message in which their
Tony Mantler24f116f2014-01-16 10:20:50 -08002096 // current account or some of their custom from addresses are the only
2097 // recipients and they sent the original message.
mindyp65b06f52012-11-21 10:35:08 -08002098 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
2099 && recipientMatchesThisAccount(inToAddresses[0])) {
2100 toAddresses.add(inToAddresses[0]);
2101 return toAddresses;
2102 }
2103 // This happens if the user replies to a message they originally
2104 // wrote. In this case, "reply" really means "re-send," so we
2105 // target the original recipients. This works as expected even
2106 // if the user sent the original message to themselves.
2107 for (String address : inToAddresses) {
2108 if (!recipientMatchesThisAccount(address)) {
2109 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08002110 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07002111 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002112 }
2113 return toAddresses;
2114 }
2115
Scott Kennedyff8553f2013-04-05 20:57:44 -07002116 private void addRecipients(final Set<String> recipients, final String[] addresses) {
2117 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002118 // Do not add this account, or any of its custom from addresses, to
2119 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08002120 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08002121 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002122 recipients.add(email.replace("\"\"", ""));
2123 }
2124 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002125 }
2126
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002127 /**
2128 * A recipient matches this account if it has the same address as the
2129 * currently selected account OR one of the custom from addresses associated
2130 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002131 * @param recipientAddress address we are comparing with the currently selected account
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002132 */
mindyp5ee5d692012-11-19 16:02:16 -08002133 protected boolean recipientMatchesThisAccount(String recipientAddress) {
2134 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08002135 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002136 }
2137
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002138 /**
2139 * Returns a formatted subject string with the appropriate prefix for the action type.
2140 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
2141 */
Andrew Sapperstein7e04f142014-06-11 13:43:07 -07002142 public static String buildFormattedSubject(Resources res, String subject, int action) {
2143 String prefix;
2144 String correctedSubject = null;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002145 if (action == ComposeActivity.COMPOSE) {
2146 prefix = "";
2147 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002148 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002149 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002150 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002151 }
2152
2153 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07002154 if (!TextUtils.isEmpty(subject)
2155 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002156 correctedSubject = subject;
2157 } else {
Andrew Sapperstein7e04f142014-06-11 13:43:07 -07002158 correctedSubject = String.format(
2159 res.getString(R.string.formatted_subject), prefix, subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002160 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002161
2162 return correctedSubject;
2163 }
2164
2165 private void setSubject(Message refMessage, int action) {
2166 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002167 }
2168
Mindy Pereira818143e2012-01-11 13:59:49 -08002169 private void initRecipients() {
2170 setupRecipients(mTo);
2171 setupRecipients(mCc);
2172 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002173 }
2174
Mindy Pereira818143e2012-01-11 13:59:49 -08002175 private void setupRecipients(RecipientEditTextView view) {
Andrew Sapperstein47c30c22014-06-04 10:26:22 -07002176 // todo - remove this experiment
2177 if (LogUtils.isLoggable("NewChips", LogUtils.DEBUG) || mUseNewChips) {
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002178 final DropdownChipLayouter layouter = getDropdownChipLayouter();
2179 if (layouter != null) {
2180 view.setDropdownChipLayouter(layouter);
2181 }
2182 view.setAdapter(getRecipientAdapter());
2183 } else {
2184 view.setAdapter(new RecipientAdapter(this, mAccount));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002185 }
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002186 view.setRecipientEntryItemClickedListener(this);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002187 if (mValidator == null) {
Tony Mantler79b11562013-10-09 15:31:50 -07002188 final String accountName = mAccount.getEmailAddress();
Mindy Pereira33fe9082012-01-09 16:24:30 -08002189 int offset = accountName.indexOf("@") + 1;
2190 String account = accountName;
Tony Mantler79b11562013-10-09 15:31:50 -07002191 if (offset > 0) {
2192 account = account.substring(offset);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002193 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002194 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002195 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002196 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002197 }
2198
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002199 /**
2200 * Derived classes should override if they wish to provide their own autocomplete behavior.
2201 */
2202 public BaseRecipientAdapter getRecipientAdapter() {
2203 return new RecipientAdapter(this, mAccount);
2204 }
2205
2206 /**
2207 * Derived classes should override this to provide their own dropdown behavior.
2208 * If the result is null, the default {@link com.android.ex.chips.DropdownChipLayouter}
2209 * is used.
2210 */
2211 public DropdownChipLayouter getDropdownChipLayouter() {
2212 return null;
2213 }
2214
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002215 @Override
2216 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002217 final int id = v.getId();
2218 if (id == R.id.add_cc_bcc) {
2219 // Verify that cc/ bcc aren't showing.
2220 // Animate in cc/bcc.
2221 showCcBccViews();
Andrew Sapperstein6aea7862013-10-24 19:59:51 -07002222 } else if (id == R.id.add_attachment) {
2223 doAttach(Utils.isRunningKitkatOrLater() ? MIME_TYPE_ALL : MIME_TYPE_PHOTO);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002224 }
2225 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002226
2227 @Override
2228 public boolean onCreateOptionsMenu(Menu menu) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002229 final boolean superCreated = super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07002230 // Don't render any menu items when there are no accounts.
2231 if (mAccounts == null || mAccounts.length == 0) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002232 return superCreated;
Mindy Pereirab199d172012-08-13 11:04:03 -07002233 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002234 MenuInflater inflater = getMenuInflater();
2235 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08002236
2237 /*
2238 * Start save in the correct enabled state.
2239 * 1) If a user launches compose from within gmail, save is disabled
2240 * until they add something, at which point, save is enabled, auto save
2241 * on exit; if the user empties everything, save is disabled, exiting does not
2242 * auto-save
2243 * 2) if a user replies/ reply all/ forwards from within gmail, save is
2244 * disabled until they change something, at which point, save is
2245 * enabled, auto save on exit; if the user empties everything, save is
2246 * disabled, exiting does not auto-save.
2247 * 3) If a user launches compose from another application and something
2248 * gets populated (attachments, recipients, body, subject, etc), save is
2249 * enabled, auto save on exit; if the user empties everything, save is
2250 * disabled, exiting does not auto-save
2251 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08002252 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08002253 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07002254 enableSave(mInnerSavedState != null ?
2255 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08002256 : (Intent.ACTION_SEND.equals(action)
2257 || Intent.ACTION_SEND_MULTIPLE.equals(action)
2258 || Intent.ACTION_SENDTO.equals(action)
2259 || shouldSave()));
2260
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07002261 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2262 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2263 if (helpItem != null) {
2264 helpItem.setVisible(mAccount != null
2265 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2266 }
2267 if (sendFeedbackItem != null) {
2268 sendFeedbackItem.setVisible(mAccount != null
2269 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2270 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002271
Andrew Sapperstein8809f9f2013-10-11 16:13:35 -07002272 // Show attach picture on pre-K devices.
2273 menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002274
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002275 return true;
2276 }
2277
2278 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002279 public boolean onPrepareOptionsMenu(Menu menu) {
2280 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08002281 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002282 // Its possible there is a menu item OR a button.
2283 boolean ccFieldVisible = mCc.isShown();
2284 boolean bccFieldVisible = mBcc.isShown();
2285 if (!ccFieldVisible || !bccFieldVisible) {
2286 ccBcc.setVisible(true);
2287 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
2288 : R.string.add_bcc_label));
2289 } else {
2290 ccBcc.setVisible(false);
2291 }
2292 }
2293 return true;
2294 }
2295
2296 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002297 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002298 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002299
Andy Huangf8c59b02014-03-19 20:00:53 -07002300 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id,
2301 "compose", 0);
Andy Huangdc97bf42013-08-15 16:52:45 -07002302
Mindy Pereira75f66632012-01-11 11:42:02 -08002303 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002304 if (id == R.id.add_file_attachment) {
2305 doAttach(MIME_TYPE_ALL);
2306 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002307 doAttach(MIME_TYPE_PHOTO);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002308 } else if (id == R.id.add_cc_bcc) {
2309 showCcBccViews();
2310 } else if (id == R.id.save) {
2311 doSave(true);
2312 } else if (id == R.id.send) {
2313 doSend();
2314 } else if (id == R.id.discard) {
2315 doDiscard();
2316 } else if (id == R.id.settings) {
2317 Utils.showSettings(this, mAccount);
2318 } else if (id == android.R.id.home) {
2319 onAppUpPressed();
2320 } else if (id == R.id.help_info_menu_item) {
2321 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2322 } else if (id == R.id.feedback_menu_item) {
2323 Utils.sendFeedback(this, mAccount, false);
2324 } else {
2325 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002326 }
Tony Mantler581edd42014-02-18 15:41:22 -08002327 return handled || super.onOptionsItemSelected(item);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002328 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002329
Mindy Pereirab199d172012-08-13 11:04:03 -07002330 @Override
2331 public void onBackPressed() {
2332 // If we are showing the wait fragment, just exit.
2333 if (getWaitFragment() != null) {
2334 finish();
2335 } else {
2336 super.onBackPressed();
2337 }
2338 }
2339
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002340 /**
2341 * Carries out the "up" action in the action bar.
2342 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002343 private void onAppUpPressed() {
2344 if (mLaunchedFromEmail) {
2345 // If this was started from Gmail, simply treat app up as the system back button, so
2346 // that the last view is restored.
2347 onBackPressed();
2348 return;
2349 }
2350
2351 // Fire the main activity to ensure it launches the "top" screen of mail.
2352 // Since the main Activity is singleTask, it should revive that task if it was already
2353 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002354 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002355 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2356 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2357 startActivity(mailIntent);
2358 finish();
2359 }
2360
Mindy Pereira33fe9082012-01-09 16:24:30 -08002361 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002362 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002363 logSendOrSave(false /* save */);
2364 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002365 }
2366
Mindy Pereira48e31b02012-05-30 13:12:24 -07002367 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002368 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002369 }
2370
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002371 @Override
2372 public void onRecipientEntryItemClicked(int charactersTyped, int position) {
2373 // Send analytics of characters typed and position in dropdown selected.
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002374 final String category = mUseNewChips ? "suggest_click_new" : "suggest_click_old";
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002375 Analytics.getInstance().sendEvent(
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002376 category, Integer.toString(charactersTyped), Integer.toString(position), 0);
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002377 }
2378
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002379 @VisibleForTesting
2380 public interface SendOrSaveCallback {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002381 void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
2382 void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2383 Message getMessage();
2384 void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2385 void incrementRecipientsTimesContacted(List<String> recipients);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002386 }
2387
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002388 @VisibleForTesting
2389 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002390 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002391 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002392 public final SendOrSaveCallback mSendOrSaveCallback;
2393 @VisibleForTesting
2394 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002395 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002396
2397 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002398 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002399 mContext = context;
2400 mSendOrSaveCallback = callback;
2401 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002402 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002403 }
2404
2405 @Override
2406 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002407 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002408
Mindy Pereira92551d02012-04-05 11:31:12 -07002409 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002410 Message message = mSendOrSaveCallback.getMessage();
2411 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002412 // If a previous draft has been saved, in an account that is different
2413 // than what the user wants to send from, remove the old draft, and treat this
2414 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002415 if (mExistingDraftAccount != null
2416 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002417 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2418 ContentResolver resolver = mContext.getContentResolver();
2419 ContentValues values = new ContentValues();
2420 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002421 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2422 new ContentProviderTask.UpdateTask()
2423 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2424 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002425 } else {
2426 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002427 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002428 // reset messageId to 0, so a new message will be created
2429 messageId = UIProvider.INVALID_MESSAGE_ID;
2430 }
2431 }
2432
2433 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002434 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002435
2436 if (!sendOrSaveMessage.mSave) {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002437 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002438 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002439 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002440 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002441 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002442 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2443 }
2444 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2445 }
2446
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002447 private void incrementRecipientsTimesContacted(final String addressString) {
Tony Mantler9f324232013-08-08 14:24:30 -07002448 if (TextUtils.isEmpty(addressString)) {
2449 return;
2450 }
2451 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2452 final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
Tony Mantler581edd42014-02-18 15:41:22 -08002453 for (final Rfc822Token token : tokens) {
2454 recipients.add(token.getAddress());
Tony Mantler9f324232013-08-08 14:24:30 -07002455 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002456 mSendOrSaveCallback.incrementRecipientsTimesContacted(recipients);
Tony Mantler9f324232013-08-08 14:24:30 -07002457 }
2458
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002459 /**
2460 * Send or Save a message.
2461 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002462 private void sendOrSaveMessage(final long messageIdToSave,
2463 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002464 final ContentResolver resolver = mContext.getContentResolver();
2465 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2466
2467 final String accountMethod = sendOrSaveMessage.mSave ?
2468 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2469 UIProvider.AccountCallMethods.SEND_MESSAGE;
2470
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002471 try {
2472 if (updateExistingMessage) {
2473 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002474
Paul Westbrook013a23c2013-02-22 10:37:41 -08002475 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002476 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002477 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002478 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002479 final Bundle result = callAccountSendSaveMethod(resolver,
2480 selectedAccount.account, accountMethod, sendOrSaveMessage);
2481 if (result != null) {
2482 // If a non-null value was returned, then the provider handled the call
2483 // method
2484 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002485 }
2486 if (sendOrSaveMessage.mSave && messageUri != null) {
2487 final Cursor messageCursor = resolver.query(messageUri,
2488 UIProvider.MESSAGE_PROJECTION, null, null, null);
2489 if (messageCursor != null) {
2490 try {
2491 if (messageCursor.moveToFirst()) {
2492 // Broadcast notification that a new message has
2493 // been allocated
2494 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2495 new Message(messageCursor));
2496 }
2497 } finally {
2498 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002499 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002500 }
2501 }
2502 }
2503 } finally {
2504 // Close any opened file descriptors
2505 closeOpenedAttachmentFds(sendOrSaveMessage);
2506 }
2507 }
2508
Scott Kennedyff8553f2013-04-05 20:57:44 -07002509 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002510 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2511 if (openedFds != null) {
2512 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002513 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002514 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2515 if (fd != null) {
2516 try {
2517 fd.close();
2518 } catch (IOException e) {
2519 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002520 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002521 }
2522 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002523 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002524 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002525
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002526 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002527 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002528 *
2529 * If this was successful, this method will return an non-null Bundle instance
2530 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002531 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2532 final Account account, final String method,
2533 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002534 // Copy all of the values from the content values to the bundle
2535 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2536 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2537
2538 for (Entry<String, Object> entry : valueSet) {
2539 final Object entryValue = entry.getValue();
2540 final String key = entry.getKey();
2541 if (entryValue instanceof String) {
2542 methodExtras.putString(key, (String)entryValue);
2543 } else if (entryValue instanceof Boolean) {
2544 methodExtras.putBoolean(key, (Boolean)entryValue);
2545 } else if (entryValue instanceof Integer) {
2546 methodExtras.putInt(key, (Integer)entryValue);
2547 } else if (entryValue instanceof Long) {
2548 methodExtras.putLong(key, (Long)entryValue);
2549 } else {
2550 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2551 entryValue.getClass().getName());
2552 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002553 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002554
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002555 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2556 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2557 if (fdMap != null) {
2558 methodExtras.putParcelable(
2559 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2560 }
2561
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002562 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002563 }
2564 }
2565
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002566 /**
2567 * Reports recipients that have been contacted in order to improve auto-complete
2568 * suggestions. Default behavior updates usage statistics in ContactsProvider.
2569 * @param recipients addresses
2570 */
2571 protected void incrementRecipientsTimesContacted(List<String> recipients) {
2572 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(this);
2573 statsUpdater.updateWithAddress(recipients);
2574 }
2575
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002576 @VisibleForTesting
2577 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002578 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002579 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002580 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002581 @VisibleForTesting
2582 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002583 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002584 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002585
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002586 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2587 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002588 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002589 mValues = values;
2590 mRefMessageId = refMessageId;
2591 mSave = save;
2592 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002593
2594 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002595 }
2596
2597 int requestId() {
2598 return mRequestId;
2599 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002600
2601 Bundle attachmentFds() {
2602 return mAttachmentFds;
2603 }
2604
2605 /**
2606 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2607 * called before the ComposeActivity finishes.
2608 * Note: The caller is responsible for closing these file descriptors.
2609 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002610 private static Bundle initializeAttachmentFds(final Context context,
2611 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002612 if (attachments == null || attachments.size() == 0) {
2613 return null;
2614 }
2615
2616 final Bundle result = new Bundle(attachments.size());
2617 final ContentResolver resolver = context.getContentResolver();
2618
2619 for (Attachment attachment : attachments) {
2620 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2621 continue;
2622 }
2623
2624 ParcelFileDescriptor fileDescriptor;
2625 try {
2626 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2627 } catch (FileNotFoundException e) {
2628 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2629 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002630 } catch (SecurityException e) {
2631 // We have encountered a security exception when attempting to open the file
2632 // specified by the content uri. If the attachment has been cached, this
2633 // isn't a problem, as even through the original permission may have been
2634 // revoked, we have cached the file. This will happen when saving/sending
2635 // a previously saved draft.
2636 // TODO(markwei): Expose whether the attachment has been cached through the
2637 // attachment object. This would allow us to limit when the log is made, as
2638 // if the attachment has been cached, this really isn't an error
2639 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2640 // Just set the file descriptor to null, as the underlying provider needs
2641 // to handle the file descriptor not being set.
2642 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002643 }
2644
2645 if (fileDescriptor != null) {
2646 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2647 }
2648 }
2649
2650 return result;
2651 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002652 }
2653
2654 /**
2655 * Get the to recipients.
2656 */
2657 public String[] getToAddresses() {
2658 return getAddressesFromList(mTo);
2659 }
2660
2661 /**
2662 * Get the cc recipients.
2663 */
2664 public String[] getCcAddresses() {
2665 return getAddressesFromList(mCc);
2666 }
2667
2668 /**
2669 * Get the bcc recipients.
2670 */
2671 public String[] getBccAddresses() {
2672 return getAddressesFromList(mBcc);
2673 }
2674
2675 public String[] getAddressesFromList(RecipientEditTextView list) {
2676 if (list == null) {
2677 return new String[0];
2678 }
2679 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2680 int count = tokens.length;
2681 String[] result = new String[count];
2682 for (int i = 0; i < count; i++) {
2683 result[i] = tokens[i].toString();
2684 }
2685 return result;
2686 }
2687
2688 /**
2689 * Check for invalid email addresses.
2690 * @param to String array of email addresses to check.
2691 * @param wrongEmailsOut Emails addresses that were invalid.
2692 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002693 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002694 if (mValidator == null) {
2695 return;
2696 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002697 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002698 if (!mValidator.isValid(email)) {
2699 wrongEmailsOut.add(email);
2700 }
2701 }
2702 }
2703
Tony Mantler2558b502013-07-09 10:53:34 -07002704 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002705 // Public no-args constructor needed for fragment re-instantiation
2706 public RecipientErrorDialogFragment() {}
2707
Tony Mantler2558b502013-07-09 10:53:34 -07002708 public static RecipientErrorDialogFragment newInstance(final String message) {
2709 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2710 final Bundle args = new Bundle(1);
2711 args.putString("message", message);
2712 frag.setArguments(args);
2713 return frag;
2714 }
2715
2716 @Override
2717 public Dialog onCreateDialog(Bundle savedInstanceState) {
2718 final String message = getArguments().getString("message");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002719 return new AlertDialog.Builder(getActivity())
2720 .setMessage(message)
Tony Mantler2558b502013-07-09 10:53:34 -07002721 .setPositiveButton(
2722 R.string.ok, new Dialog.OnClickListener() {
2723 @Override
2724 public void onClick(DialogInterface dialog, int which) {
2725 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2726 }
2727 }).create();
2728 }
2729 }
2730
2731 private void finishRecipientErrorDialog() {
2732 // after the user dismisses the recipient error
2733 // dialog we want to make sure to refocus the
2734 // recipient to field so they can fix the issue
2735 // easily
2736 if (mTo != null) {
2737 mTo.requestFocus();
2738 }
2739 }
2740
Mindy Pereira82cc5662012-01-09 17:29:30 -08002741 /**
2742 * Show an error because the user has entered an invalid recipient.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002743 */
Tony Mantler2558b502013-07-09 10:53:34 -07002744 private void showRecipientErrorDialog(final String message) {
2745 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2746 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002747 }
2748
2749 /**
2750 * Update the state of the UI based on whether or not the current draft
2751 * needs to be saved and the message is not empty.
2752 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002753 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002754 if (mSave != null) {
2755 mSave.setEnabled((shouldSave() && !isBlank()));
2756 }
2757 }
2758
2759 /**
2760 * Returns true if we need to save the current draft.
2761 */
2762 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002763 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002764 // The message should only be saved if:
2765 // It hasn't been sent AND
2766 // Some text has been added to the message OR
2767 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002768 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002769 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002770 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002771 }
2772 }
2773
2774 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002775 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002776 * @return boolean
2777 */
2778 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002779 // Need to check for null since isBlank() can be called from onPause()
2780 // before findViews() is called
2781 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2782 mAttachmentsView == null) {
2783 LogUtils.w(LOG_TAG, "null views in isBlank check");
2784 return true;
2785 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002786 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002787 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2788 mBodyView.getText().toString()) == 0)
2789 && mTo.length() == 0
2790 && mCc.length() == 0 && mBcc.length() == 0
2791 && mAttachmentsView.getAttachments().size() == 0;
2792 }
2793
2794 @VisibleForTesting
2795 protected int getSignatureStartPosition(String signature, String bodyText) {
2796 int startPos = -1;
2797
2798 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2799 return startPos;
2800 }
2801
2802 int bodyLength = bodyText.length();
2803 int signatureLength = signature.length();
2804 String printableVersion = convertToPrintableSignature(signature);
2805 int printableLength = printableVersion.length();
2806
2807 if (bodyLength >= printableLength
2808 && bodyText.substring(bodyLength - printableLength)
2809 .equals(printableVersion)) {
2810 startPos = bodyLength - printableLength;
2811 } else if (bodyLength >= signatureLength
2812 && bodyText.substring(bodyLength - signatureLength)
2813 .equals(signature)) {
2814 startPos = bodyLength - signatureLength;
2815 }
2816 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002817 }
2818
2819 /**
2820 * Allows any changes made by the user to be ignored. Called when the user
2821 * decides to discard a draft.
2822 */
2823 private void discardChanges() {
2824 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002825 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002826 mReplyFromChanged = false;
2827 }
2828
2829 /**
Tony Mantler581edd42014-02-18 15:41:22 -08002830 * @param save True to save, false to send
2831 * @param showToast True to show a toast once the message is sent/saved
Mindy Pereira181df782012-03-01 13:32:44 -08002832 */
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002833 protected void sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002834 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002835 if (mAccounts == null || mAccount == null) {
2836 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002837 if (autoSend) {
2838 finish();
2839 }
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002840 return;
Mark Wei009b3712012-10-18 18:07:50 -07002841 }
2842
Scott Kennedyff8553f2013-04-05 20:57:44 -07002843 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002844 if (orientationChanged) {
2845 to = cc = bcc = new String[0];
2846 } else {
2847 to = getToAddresses();
2848 cc = getCcAddresses();
2849 bcc = getBccAddresses();
2850 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002851
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002852 final ArrayList<String> recipients = buildEmailAddressList(to);
2853 recipients.addAll(buildEmailAddressList(cc));
2854 recipients.addAll(buildEmailAddressList(bcc));
2855
Mindy Pereira181df782012-03-01 13:32:44 -08002856 // Don't let the user send to nobody (but it's okay to save a message
2857 // with no recipients)
2858 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2859 showRecipientErrorDialog(getString(R.string.recipient_needed));
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002860 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002861 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002862
Mindy Pereira181df782012-03-01 13:32:44 -08002863 List<String> wrongEmails = new ArrayList<String>();
2864 if (!save) {
2865 checkInvalidEmails(to, wrongEmails);
2866 checkInvalidEmails(cc, wrongEmails);
2867 checkInvalidEmails(bcc, wrongEmails);
2868 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002869
Mindy Pereira181df782012-03-01 13:32:44 -08002870 // Don't let the user send an email with invalid recipients
2871 if (wrongEmails.size() > 0) {
2872 String errorText = String.format(getString(R.string.invalid_recipient),
2873 wrongEmails.get(0));
2874 showRecipientErrorDialog(errorText);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002875 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002876 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002877
Mindy Pereira181df782012-03-01 13:32:44 -08002878 if (!save) {
Alan Lau3d519042014-06-05 11:13:06 -07002879 if (autoSend) {
2880 // Skip all further checks during autosend. This flow is used by Android Wear
2881 // and Google Now.
2882 sendOrSave(save, showToast);
2883 return;
2884 }
2885
2886 // Show a warning before sending only if there are no attachments, body, or subject.
Mindy Pereira181df782012-03-01 13:32:44 -08002887 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2888 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002889 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002890
Mindy Pereira181df782012-03-01 13:32:44 -08002891 // A warning about an empty body may not be warranted when
2892 // forwarding mails, since a common use case is to forward
2893 // quoted text and not append any more text.
2894 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002895
Mindy Pereira181df782012-03-01 13:32:44 -08002896 // When we bring up a dialog warning the user about a send,
2897 // assume that they accept sending the message. If they do not,
2898 // the dialog listener is required to enable sending again.
2899 if (warnAboutEmptySubject) {
Tony Mantler581edd42014-02-18 15:41:22 -08002900 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002901 showToast, recipients);
2902 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002903 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002904
Mindy Pereira181df782012-03-01 13:32:44 -08002905 if (warnAboutEmptyBody) {
Tony Mantler581edd42014-02-18 15:41:22 -08002906 showSendConfirmDialog(R.string.confirm_send_message_with_no_body,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002907 showToast, recipients);
2908 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002909 }
2910 }
Alan Lau3d519042014-06-05 11:13:06 -07002911 // Ask for confirmation to send.
2912 if (showSendConfirmation()) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002913 showSendConfirmDialog(R.string.confirm_send_message, showToast, recipients);
2914 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002915 }
2916 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002917
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002918 performAdditionalSendOrSaveSanityChecks(save, showToast, recipients);
Mindy Pereira181df782012-03-01 13:32:44 -08002919 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002920
Mindy Pereira181df782012-03-01 13:32:44 -08002921 /**
2922 * Returns a boolean indicating whether warnings should be shown for empty
2923 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002924 *
Mindy Pereira181df782012-03-01 13:32:44 -08002925 * @return True if a warning should be shown for empty text fields
2926 */
2927 protected boolean showEmptyTextWarnings() {
2928 return mAttachmentsView.getAttachments().size() == 0;
2929 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002930
Mindy Pereira181df782012-03-01 13:32:44 -08002931 /**
2932 * Returns a boolean indicating whether the user should confirm each send
2933 *
2934 * @return True if a warning should be on each send
2935 */
2936 protected boolean showSendConfirmation() {
Tony Mantler581edd42014-02-18 15:41:22 -08002937 return mCachedSettings != null && mCachedSettings.confirmSend;
Mindy Pereira181df782012-03-01 13:32:44 -08002938 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002939
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002940 public static class SendConfirmDialogFragment extends DialogFragment
2941 implements DialogInterface.OnClickListener {
2942
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002943 private static final String MESSAGE_ID = "messageId";
2944 private static final String SHOW_TOAST = "showToast";
2945 private static final String RECIPIENTS = "recipients";
2946
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002947 private boolean mShowToast;
2948
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002949 private ArrayList<String> mRecipients;
2950
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002951 // Public no-args constructor needed for fragment re-instantiation
2952 public SendConfirmDialogFragment() {}
2953
Tony Mantler2558b502013-07-09 10:53:34 -07002954 public static SendConfirmDialogFragment newInstance(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002955 final boolean showToast, final ArrayList<String> recipients) {
Tony Mantler2558b502013-07-09 10:53:34 -07002956 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2957 final Bundle args = new Bundle(3);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002958 args.putInt(MESSAGE_ID, messageId);
2959 args.putBoolean(SHOW_TOAST, showToast);
2960 args.putStringArrayList(RECIPIENTS, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07002961 frag.setArguments(args);
2962 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002963 }
Tony Mantler2558b502013-07-09 10:53:34 -07002964
2965 @Override
2966 public Dialog onCreateDialog(Bundle savedInstanceState) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002967 final int messageId = getArguments().getInt(MESSAGE_ID);
2968 mShowToast = getArguments().getBoolean(SHOW_TOAST);
2969 mRecipients = getArguments().getStringArrayList(RECIPIENTS);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002970
2971 final int confirmTextId = (messageId == R.string.confirm_send_message) ?
2972 R.string.ok : R.string.send;
Tony Mantler2558b502013-07-09 10:53:34 -07002973
2974 return new AlertDialog.Builder(getActivity())
2975 .setMessage(messageId)
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002976 .setPositiveButton(confirmTextId, this)
Paul Westbrook7d1c5c42013-10-01 23:40:04 -07002977 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07002978 .create();
2979 }
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002980
2981 @Override
2982 public void onClick(DialogInterface dialog, int which) {
2983 if (which == DialogInterface.BUTTON_POSITIVE) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002984 ((ComposeActivity) getActivity()).finishSendConfirmDialog(mShowToast, mRecipients);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002985 }
2986 }
Tony Mantler2558b502013-07-09 10:53:34 -07002987 }
2988
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002989 private void finishSendConfirmDialog(
2990 final boolean showToast, final ArrayList<String> recipients) {
2991 performAdditionalSendOrSaveSanityChecks(false /* save */, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07002992 }
2993
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002994 // The list of recipients are used by the additional sendOrSave checks.
2995 // However, the send confirm dialog may be shown before performing
2996 // the additional checks. As a result, we need to plumb the recipient
2997 // list through the send confirm dialog so that
2998 // performAdditionalSendOrSaveChecks can be performed properly.
Tony Mantler581edd42014-02-18 15:41:22 -08002999 private void showSendConfirmDialog(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003000 final boolean showToast, final ArrayList<String> recipients) {
3001 final DialogFragment frag = SendConfirmDialogFragment.newInstance(
3002 messageId, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07003003 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08003004 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003005
Mindy Pereira181df782012-03-01 13:32:44 -08003006 /**
3007 * Returns whether the ComposeArea believes there is any text in the body of
3008 * the composition. TODO: When ComposeArea controls the Body as well, add
3009 * that here.
3010 */
3011 public boolean isBodyEmpty() {
3012 return !mQuotedTextView.isTextIncluded();
3013 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003014
Mindy Pereira181df782012-03-01 13:32:44 -08003015 /**
3016 * Test to see if the subject is empty.
3017 *
3018 * @return boolean.
3019 */
3020 // TODO: this will likely go away when composeArea.focus() is implemented
3021 // after all the widget control is moved over.
3022 public boolean isSubjectEmpty() {
3023 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
3024 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003025
Andy Huang0a2a3462013-12-20 15:56:13 -08003026 @VisibleForTesting
3027 public String getSubject() {
3028 return mSubject.getText().toString();
3029 }
3030
Andy Huang91ede362014-01-21 19:16:00 -08003031 private int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Jin Cao77b4c2c2014-05-20 13:55:53 -07003032 Message message, final Message refMessage, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08003033 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07003034 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08003035 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003036
Paul Westbrookb4931c62013-01-14 17:51:18 -08003037 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07003038
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003039 MessageModification.putToAddresses(values, message.getToAddresses());
3040 MessageModification.putCcAddresses(values, message.getCcAddresses());
3041 MessageModification.putBccAddresses(values, message.getBccAddresses());
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003042 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07003043
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003044 MessageModification.putSubject(values, message.subject);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003045
Jin Cao77b4c2c2014-05-20 13:55:53 -07003046 // bodyHtml already have the composing spans removed.
3047 final String htmlBody = message.bodyHtml;
Anthony Lee2a3cc132014-04-22 14:15:25 -07003048 final String textBody = Utils.convertHtmlToPlainText(htmlBody);
3049 // fullbody will contain the actual body plus the quoted text.
3050 final String fullBody;
3051 final String quotedString;
3052 final boolean hasQuotedText = !TextUtils.isEmpty(quotedText);
3053 if (hasQuotedText) {
3054 // The quoted text is HTML at this point.
3055 quotedString = quotedText.toString();
3056 fullBody = htmlBody + quotedString;
3057 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
3058 MessageModification.putAppendRefMessageContent(values, true /* include quoted */);
3059 } else {
3060 fullBody = htmlBody;
3061 quotedString = null;
Mindy Pereira29ef1b82012-01-13 11:26:21 -08003062 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003063 if (refMessage != null) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07003064 // The code below might need to be revisited. The quoted text position is different
3065 // between text/html and text/plain parts and they should be stored seperately and
3066 // the right version should be used in the UI. text/html should have preference
3067 // if both exist. Issues like this made me file b/14256940 to make sure that we
3068 // properly handle the existing of both text/html and text/plain parts and to verify
3069 // that we are not making some assumptions that break if there is no text/html part.
3070 int quotedTextPos = -1;
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003071 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
3072 MessageModification.putBodyHtml(values, fullBody.toString());
Anthony Lee2a3cc132014-04-22 14:15:25 -07003073 if (hasQuotedText) {
3074 quotedTextPos = htmlBody.length() +
3075 QuotedTextView.getQuotedTextOffset(quotedString);
3076 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003077 }
3078 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08003079 MessageModification.putBody(values,
Tony Mantler581edd42014-02-18 15:41:22 -08003080 Utils.convertHtmlToPlainText(fullBody.toString()));
Anthony Lee2a3cc132014-04-22 14:15:25 -07003081 if (hasQuotedText && (quotedTextPos == -1)) {
3082 quotedTextPos = textBody.length();
3083 }
3084 }
3085 if (quotedTextPos != -1) {
3086 // The quoted text pos is the text/html version first and the text/plan version
3087 // if there is no text/html part. The reason for this is because preference
3088 // is given to text/html in the compose window if it exists. In the future, we
3089 // should calculate the index for both since the user could choose to compose
3090 // explicitly in text/plain.
3091 MessageModification.putQuoteStartPos(values, quotedTextPos);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003092 }
3093 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07003094 MessageModification.putBodyHtml(values, fullBody.toString());
Tony Mantler581edd42014-02-18 15:41:22 -08003095 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString()));
Mindy Pereirac2031972012-04-03 09:38:35 -07003096 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07003097 int draftType = getDraftType(composeMode);
3098 MessageModification.putDraftType(values, draftType);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003099 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07003100 if (!TextUtils.isEmpty(refMessageId)) {
3101 MessageModification.putRefMessageId(values, refMessageId);
3102 }
Scott Kennedy60847252013-08-15 15:55:42 -07003103 if (extraValues != null) {
3104 values.putAll(extraValues);
3105 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07003106 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
3107 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08003108 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
3109 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003110
Mindy Pereira181df782012-03-01 13:32:44 -08003111 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08003112 // Do the send/save action on the specified handler to avoid possible
3113 // ANRs
3114 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003115
Mindy Pereira181df782012-03-01 13:32:44 -08003116 return sendOrSaveMessage.requestId();
3117 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003118
Paul Westbrookb4931c62013-01-14 17:51:18 -08003119 /**
3120 * Removes any composing spans from the specified string. This will create a new
3121 * SpannableString instance, as to not modify the behavior of the EditText view.
3122 */
3123 private static SpannableString removeComposingSpans(Spanned body) {
3124 final SpannableString messageBody = new SpannableString(body);
3125 BaseInputConnection.removeComposingSpans(messageBody);
Andy Huangff017272014-06-18 00:27:35 -07003126
3127 // Remove watcher spans while we're at it, so any off-UI thread manipulation of these
3128 // spans doesn't trigger unexpected side-effects. This copy is essentially 100% detached
3129 // from the EditText.
3130 //
3131 // (must remove SpanWatchers first to avoid triggering them as we remove other spans)
3132 removeSpansOfType(messageBody, SpanWatcher.class);
3133 removeSpansOfType(messageBody, TextWatcher.class);
3134
Paul Westbrookb4931c62013-01-14 17:51:18 -08003135 return messageBody;
3136 }
3137
Andy Huangff017272014-06-18 00:27:35 -07003138 private static void removeSpansOfType(SpannableString str, Class<?> cls) {
3139 for (Object span : str.getSpans(0, str.length(), cls)) {
3140 str.removeSpan(span);
3141 }
3142 }
3143
Mindy Pereira002ff522012-05-30 10:31:26 -07003144 private static int getDraftType(int mode) {
3145 int draftType = -1;
3146 switch (mode) {
3147 case ComposeActivity.COMPOSE:
3148 draftType = DraftType.COMPOSE;
3149 break;
3150 case ComposeActivity.REPLY:
3151 draftType = DraftType.REPLY;
3152 break;
3153 case ComposeActivity.REPLY_ALL:
3154 draftType = DraftType.REPLY_ALL;
3155 break;
3156 case ComposeActivity.FORWARD:
3157 draftType = DraftType.FORWARD;
3158 break;
3159 }
3160 return draftType;
3161 }
3162
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003163 /**
3164 * Derived classes should override this step to perform additional checks before
3165 * send or save. The default implementation simply calls {@link #sendOrSave(boolean, boolean)}.
3166 */
3167 protected void performAdditionalSendOrSaveSanityChecks(
3168 final boolean save, final boolean showToast, ArrayList<String> recipients) {
3169 sendOrSave(save, showToast);
3170 }
3171
3172 protected void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08003173 // Check if user is a monkey. Monkeys can compose and hit send
3174 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07003175 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08003176 return;
3177 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003178
Jin Cao77b4c2c2014-05-20 13:55:53 -07003179 final SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003180 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08003181 private int mRestoredRequestId;
3182
Marc Blank0bbc8582012-04-23 15:07:57 -07003183 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08003184 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08003185 synchronized (mActiveTasks) {
3186 int numTasks = mActiveTasks.size();
3187 if (numTasks == 0) {
3188 // Start service so we won't be killed if this app is
3189 // put in the background.
3190 startService(new Intent(ComposeActivity.this, EmptyService.class));
3191 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003192
Mindy Pereira181df782012-03-01 13:32:44 -08003193 mActiveTasks.add(sendOrSaveTask);
3194 }
3195 if (sTestSendOrSaveCallback != null) {
3196 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
3197 }
3198 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003199
Marc Blank0bbc8582012-04-23 15:07:57 -07003200 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003201 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
3202 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08003203 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08003204 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08003205 mDraftId = message.id;
3206 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003207 if (sRequestMessageIdMap != null) {
3208 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
3209 }
Mindy Pereira181df782012-03-01 13:32:44 -08003210 // Cache request message map, in case the process is killed
3211 saveRequestMap();
3212 }
3213 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003214 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08003215 }
3216 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003217
Marc Blank0bbc8582012-04-23 15:07:57 -07003218 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003219 public Message getMessage() {
3220 synchronized (mDraftLock) {
3221 return mDraft;
3222 }
3223 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003224
Marc Blank0bbc8582012-04-23 15:07:57 -07003225 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003226 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07003227 // Update the last sent from account.
3228 if (mAccount != null) {
3229 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
3230 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003231 if (success) {
3232 // Successfully sent or saved so reset change markers
3233 discardChanges();
3234 } else {
3235 // A failure happened with saving/sending the draft
3236 // TODO(pwestbro): add a better string that should be used
3237 // when failing to send or save
3238 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
3239 .show();
3240 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003241
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003242 int numTasks;
3243 synchronized (mActiveTasks) {
3244 // Remove the task from the list of active tasks
3245 mActiveTasks.remove(task);
3246 numTasks = mActiveTasks.size();
3247 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003248
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003249 if (numTasks == 0) {
3250 // Stop service so we can be killed.
3251 stopService(new Intent(ComposeActivity.this, EmptyService.class));
3252 }
3253 if (sTestSendOrSaveCallback != null) {
3254 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
3255 }
3256 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07003257
3258 @Override
3259 public void incrementRecipientsTimesContacted(final List<String> recipients) {
3260 ComposeActivity.this.incrementRecipientsTimesContacted(recipients);
3261 }
Mindy Pereira181df782012-03-01 13:32:44 -08003262 };
Tony Mantler1e05a1e2013-08-12 16:44:26 -07003263 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003264
Jin Cao77b4c2c2014-05-20 13:55:53 -07003265 final Spanned body = removeComposingSpans(mBodyView.getText());
3266 SEND_SAVE_TASK_HANDLER.post(new Runnable() {
3267 @Override
3268 public void run() {
3269 final Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode(), body);
3270 mRequestId = sendOrSaveInternal(ComposeActivity.this, mReplyFromAccount, msg,
3271 mRefMessage, mQuotedTextView.getQuotedTextIfIncluded(), callback,
3272 SEND_SAVE_TASK_HANDLER, save, mComposeMode, mDraftAccount, mExtraValues);
3273 }
3274 });
Mindy Pereira82cc5662012-01-09 17:29:30 -08003275
Mindy Pereira181df782012-03-01 13:32:44 -08003276 // Don't display the toast if the user is just changing the orientation,
3277 // but we still need to save the draft to the cursor because this is how we restore
3278 // the attachments when the configuration change completes.
3279 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
3280 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
3281 Toast.LENGTH_LONG).show();
3282 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003283
Mindy Pereira181df782012-03-01 13:32:44 -08003284 // Need to update variables here because the send or save completes
3285 // asynchronously even though the toast shows right away.
3286 discardChanges();
3287 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003288
Mindy Pereira181df782012-03-01 13:32:44 -08003289 // If we are sending, finish the activity
3290 if (!save) {
3291 finish();
3292 }
3293 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003294
Mindy Pereira181df782012-03-01 13:32:44 -08003295 /**
3296 * Save the state of the request messageid map. This allows for the Gmail
3297 * process to be killed, but and still allow for ComposeActivity instances
3298 * to be recreated correctly.
3299 */
3300 private void saveRequestMap() {
3301 // TODO: store the request map in user preferences.
3302 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003303
Tony Mantler581edd42014-02-18 15:41:22 -08003304 @SuppressLint("NewApi")
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003305 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08003306 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
3307 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07003308 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07003309 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003310 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08003311 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08003312 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
3313 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08003314 }
3315
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003316 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003317 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003318 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07003319 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003320 }
3321 }
3322
Andy Huang4fe0af82013-08-20 17:24:51 -07003323 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003324 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07003325 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003326 case COMPOSE:
3327 msgType = "new_message";
3328 break;
3329 case REPLY:
3330 msgType = "reply";
3331 break;
3332 case REPLY_ALL:
3333 msgType = "reply_all";
3334 break;
3335 case FORWARD:
3336 msgType = "forward";
3337 break;
3338 default:
3339 msgType = "unknown";
3340 break;
3341 }
Andy Huang4fe0af82013-08-20 17:24:51 -07003342 return msgType;
3343 }
3344
3345 private void logSendOrSave(boolean save) {
3346 if (!Analytics.isLoggable() || mAttachmentsView == null) {
3347 return;
3348 }
3349
3350 final String category = (save) ? "message_save" : "message_send";
3351 final int attachmentCount = getAttachments().size();
3352 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07003353 final String label;
3354 final long value;
3355 if (mComposeMode == COMPOSE) {
3356 label = Integer.toString(attachmentCount);
3357 value = attachmentCount;
3358 } else {
3359 label = null;
3360 value = 0;
3361 }
3362 Analytics.getInstance().sendEvent(category, msgType, label, value);
3363 }
3364
Mindy Pereira326c6602012-01-04 15:32:42 -08003365 @Override
3366 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003367 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003368 if (position == ComposeActivity.REPLY) {
3369 mComposeMode = ComposeActivity.REPLY;
3370 } else if (position == ComposeActivity.REPLY_ALL) {
3371 mComposeMode = ComposeActivity.REPLY_ALL;
3372 } else if (position == ComposeActivity.FORWARD) {
3373 mComposeMode = ComposeActivity.FORWARD;
3374 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003375 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003376 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003377 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003378 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003379 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003380 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003381 boolean showCc = false;
3382 boolean showBcc = false;
3383 if (mDraft != null) {
3384 // Following desktop behavior, if the user has added a BCC
3385 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003386 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003387 // Use the draft to determine what to populate.
3388 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003389 showCc = showBcc
3390 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003391 }
3392 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003393 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003394 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003395 }
3396 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003397 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003398 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003399 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003400 return true;
3401 }
3402
Mindy Pereirab3112a22012-06-20 12:10:03 -07003403 @VisibleForTesting
3404 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003405 // When switching between reply, reply all, forward,
3406 // follow the behavior of webview.
3407 // The contents of the following fields are cleared
3408 // so that they can be populated directly from the
3409 // ref message:
3410 // 1) Any recipient fields
3411 // 2) The subject
3412 mTo.setText("");
3413 mCc.setText("");
3414 mBcc.setText("");
3415 // Any edits to the subject are replaced with the original subject.
3416 mSubject.setText("");
3417
3418 // Any changes to the contents of the following fields are kept:
3419 // 1) Body
3420 // 2) Attachments
3421 // If the user made changes to attachments, keep their changes.
3422 if (!mAttachmentsChanged) {
3423 mAttachmentsView.deleteAllAttachments();
3424 }
3425 }
3426
Mindy Pereira326c6602012-01-04 15:32:42 -08003427 private class ComposeModeAdapter extends ArrayAdapter<String> {
3428
3429 private LayoutInflater mInflater;
3430
3431 public ComposeModeAdapter(Context context) {
3432 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3433 .getStringArray(R.array.compose_modes));
3434 }
3435
3436 private LayoutInflater getInflater() {
3437 if (mInflater == null) {
3438 mInflater = LayoutInflater.from(getContext());
3439 }
3440 return mInflater;
3441 }
3442
3443 @Override
3444 public View getView(int position, View convertView, ViewGroup parent) {
3445 if (convertView == null) {
3446 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3447 }
3448 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3449 return super.getView(position, convertView, parent);
3450 }
3451 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003452
3453 @Override
3454 public void onRespondInline(String text) {
3455 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003456 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003457 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003458 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003459 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003460 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003461 }
3462
3463 /**
3464 * Append text to the body of the message. If there is no existing body
3465 * text, just sets the body to text.
3466 *
Tony Mantler581edd42014-02-18 15:41:22 -08003467 * @param text Text to append
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003468 * @param withSignature True to append a signature.
3469 */
3470 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003471 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003472 if (bodyText != null && bodyText.length() > 0) {
3473 bodyText.append(text);
3474 } else {
3475 setBody(text, withSignature);
3476 }
3477 }
3478
3479 /**
3480 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003481 *
Tony Mantler581edd42014-02-18 15:41:22 -08003482 * @param text text to set
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003483 * @param withSignature True to append a signature.
3484 */
3485 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003486 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003487 if (withSignature) {
3488 appendSignature();
3489 }
3490 }
3491
3492 private void appendSignature() {
Tony Mantler6a7ac782014-02-19 15:22:02 -08003493 final String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3494 final int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
mindyp27083062012-11-15 09:02:01 -08003495 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003496 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003497 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003498 // Appending a signature does not count as changing text.
3499 mBodyView.removeTextChangedListener(this);
3500 mBodyView.append(convertToPrintableSignature(mSignature));
3501 mBodyView.addTextChangedListener(this);
3502 }
Tony Mantler6a7ac782014-02-19 15:22:02 -08003503 resetBodySelection();
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003504 }
3505 }
3506
3507 private String convertToPrintableSignature(String signature) {
3508 String signatureResource = getResources().getString(R.string.signature);
3509 if (signature == null) {
3510 signature = "";
3511 }
3512 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003513 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003514
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003515 @Override
3516 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003517 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3518 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003519 // Clear a signature, if there was one.
3520 mBodyView.removeTextChangedListener(this);
3521 String oldSignature = mSignature;
3522 String bodyText = getBody().getText().toString();
3523 if (!TextUtils.isEmpty(oldSignature)) {
3524 int pos = getSignatureStartPosition(oldSignature, bodyText);
3525 if (pos > -1) {
3526 mBodyView.setText(bodyText.substring(0, pos));
3527 }
3528 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003529 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003530 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003531 // TODO: handle discarding attachments when switching accounts.
3532 // Only enable save for this draft if there is any other content
3533 // in the message.
3534 if (!isBlank()) {
3535 enableSave(true);
3536 }
3537 mReplyFromChanged = true;
3538 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003539 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003540 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003541
3542 public void enableSave(boolean enabled) {
3543 if (mSave != null) {
3544 mSave.setEnabled(enabled);
3545 }
3546 }
3547
Tony Mantler2558b502013-07-09 10:53:34 -07003548 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003549 // Public no-args constructor needed for fragment re-instantiation
3550 public DiscardConfirmDialogFragment() {}
3551
Tony Mantler2558b502013-07-09 10:53:34 -07003552 @Override
3553 public Dialog onCreateDialog(Bundle savedInstanceState) {
3554 return new AlertDialog.Builder(getActivity())
3555 .setMessage(R.string.confirm_discard_text)
3556 .setPositiveButton(R.string.discard,
3557 new DialogInterface.OnClickListener() {
3558 @Override
3559 public void onClick(DialogInterface dialog, int which) {
3560 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3561 }
3562 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003563 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003564 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003565 }
3566 }
3567
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003568 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003569 final DialogFragment frag = new DiscardConfirmDialogFragment();
3570 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003571 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003572 /**
3573 * Effectively discard the current message.
3574 *
3575 * This method is either invoked from the menu or from the dialog
3576 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003577 */
Tony Mantler2558b502013-07-09 10:53:34 -07003578 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003579 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003580 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3581 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003582 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003583 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003584 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3585 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003586 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003587 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003588 // This is not strictly necessary (since we should not try to
3589 // save the draft after calling this) but it ensures that if we
3590 // do save again for some reason we make a new draft rather than
3591 // trying to resave an expunged draft.
3592 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3593 }
3594 }
3595
Tony Mantler2558b502013-07-09 10:53:34 -07003596 // Display a toast to let the user know
3597 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003598
3599 // This prevents the draft from being saved in onPause().
3600 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003601 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003602 finish();
3603 }
3604
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003605 private void saveIfNeeded() {
3606 if (mAccount == null) {
3607 // We have not chosen an account yet so there's no way that we can save. This is ok,
3608 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3609 // user has not interacted with us yet and there is no real state to save.
3610 return;
3611 }
3612
3613 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003614 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003615 }
3616 }
3617
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003618 @Override
3619 public void onAttachmentDeleted() {
3620 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003621 // If we are showing any attachments, make sure we have an upper
3622 // divider.
3623 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003624 updateSaveUi();
3625 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003626
mindyp40882432012-09-06 11:07:40 -07003627 @Override
3628 public void onAttachmentAdded() {
3629 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3630 mAttachmentsView.focusLastAttachment();
3631 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003632
3633 /**
3634 * This is called any time one of our text fields changes.
3635 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003636 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003637 public void afterTextChanged(Editable s) {
3638 mTextChanged = true;
3639 updateSaveUi();
3640 }
3641
3642 @Override
3643 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3644 // Do nothing.
3645 }
3646
Marc Blank0bbc8582012-04-23 15:07:57 -07003647 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003648 public void onTextChanged(CharSequence s, int start, int before, int count) {
3649 // Do nothing.
3650 }
3651
3652
3653 // There is a big difference between the text associated with an address changing
3654 // to add the display name or to format properly and a recipient being added or deleted.
3655 // Make sure we only notify of changes when a recipient has been added or deleted.
3656 private class RecipientTextWatcher implements TextWatcher {
3657 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3658
3659 private RecipientEditTextView mView;
3660
3661 private TextWatcher mListener;
3662
3663 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3664 mView = view;
3665 mListener = listener;
3666 }
3667
3668 @Override
3669 public void afterTextChanged(Editable s) {
3670 if (hasChanged()) {
3671 mListener.afterTextChanged(s);
3672 }
3673 }
3674
3675 private boolean hasChanged() {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003676 final ArrayList<String> currRecips = buildEmailAddressList(getAddressesFromList(mView));
3677 int totalCount = currRecips.size();
Mindy Pereira75f66632012-01-11 11:42:02 -08003678 int totalPrevCount = 0;
3679 for (Entry<String, Integer> entry : mContent.entrySet()) {
3680 totalPrevCount += entry.getValue();
3681 }
3682 if (totalCount != totalPrevCount) {
3683 return true;
3684 }
3685
3686 for (String recip : currRecips) {
3687 if (!mContent.containsKey(recip)) {
3688 return true;
3689 } else {
3690 int count = mContent.get(recip) - 1;
3691 if (count < 0) {
3692 return true;
3693 } else {
3694 mContent.put(recip, count);
3695 }
3696 }
3697 }
3698 return false;
3699 }
3700
Mindy Pereira75f66632012-01-11 11:42:02 -08003701 @Override
3702 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003703 final ArrayList<String> recips = buildEmailAddressList(getAddressesFromList(mView));
Mindy Pereira75f66632012-01-11 11:42:02 -08003704 for (String recip : recips) {
3705 if (!mContent.containsKey(recip)) {
3706 mContent.put(recip, 1);
3707 } else {
3708 mContent.put(recip, (mContent.get(recip)) + 1);
3709 }
3710 }
3711 }
3712
3713 @Override
3714 public void onTextChanged(CharSequence s, int start, int before, int count) {
3715 // Do nothing.
3716 }
3717 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003718
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003719 /**
3720 * Returns a list of email addresses from the recipients. List only contains
3721 * email addresses strips additional info like the recipient's name.
3722 */
3723 private static ArrayList<String> buildEmailAddressList(String[] recips) {
3724 // Tokenize them all and put them in the list.
3725 final ArrayList<String> recipAddresses = Lists.newArrayListWithCapacity(recips.length);
3726 for (int i = 0; i < recips.length; i++) {
3727 recipAddresses.add(Rfc822Tokenizer.tokenize(recips[i])[0].getAddress());
3728 }
3729 return recipAddresses;
3730 }
3731
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003732 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3733 if (sTestSendOrSaveCallback != null && testCallback != null) {
3734 throw new IllegalStateException("Attempting to register more than one test callback");
3735 }
3736 sTestSendOrSaveCallback = testCallback;
3737 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003738
3739 @VisibleForTesting
3740 protected ArrayList<Attachment> getAttachments() {
3741 return mAttachmentsView.getAttachments();
3742 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003743
3744 @Override
3745 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3746 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003747 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3748 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3749 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003750 case REFERENCE_MESSAGE_LOADER:
3751 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3752 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003753 case LOADER_ACCOUNT_CURSOR:
3754 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3755 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003756 }
3757 return null;
3758 }
3759
3760 @Override
3761 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003762 int id = loader.getId();
3763 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003764 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003765 if (data != null && data.moveToFirst()) {
3766 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003767 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003768 initFromRefMessage(mComposeMode);
3769 finishSetup(mComposeMode, intent, null);
3770 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003771 String to = intent.getStringExtra(EXTRA_TO);
3772 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003773 mRefMessage.setTo(null);
3774 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003775 clearChangeListeners();
3776 mTo.append(to);
3777 initChangeListeners();
3778 }
3779 }
3780 } else {
3781 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003782 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003783 break;
Alice Yanga990a712013-03-13 18:37:00 -07003784 case REFERENCE_MESSAGE_LOADER:
3785 // Only populate mRefMessage and leave other fields untouched.
3786 if (data != null && data.moveToFirst()) {
3787 mRefMessage = new Message(data);
3788 }
Andy Huang9f855d62013-05-30 17:15:03 -07003789 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003790 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003791 case LOADER_ACCOUNT_CURSOR:
3792 if (data != null && data.moveToFirst()) {
3793 // there are accounts now!
3794 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003795 final ArrayList<Account> accounts = new ArrayList<Account>();
3796 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003797 do {
3798 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003799 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003800 initializedAccounts.add(account);
3801 }
3802 accounts.add(account);
3803 } while (data.moveToNext());
3804 if (initializedAccounts.size() > 0) {
3805 findViewById(R.id.wait).setVisibility(View.GONE);
3806 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3807 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003808 mAccounts = initializedAccounts.toArray(
3809 new Account[initializedAccounts.size()]);
3810
Mindy Pereirab199d172012-08-13 11:04:03 -07003811 finishCreate();
3812 invalidateOptionsMenu();
3813 } else {
3814 // Show "waiting"
3815 account = accounts.size() > 0 ? accounts.get(0) : null;
3816 showWaitFragment(account);
3817 }
3818 }
3819 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003820 }
3821 }
3822
Mindy Pereirab199d172012-08-13 11:04:03 -07003823 private void showWaitFragment(Account account) {
3824 WaitFragment fragment = getWaitFragment();
3825 if (fragment != null) {
3826 fragment.updateAccount(account);
3827 } else {
3828 findViewById(R.id.wait).setVisibility(View.VISIBLE);
Andy Huangc96efcc2014-04-09 15:30:42 -07003829 replaceFragment(WaitFragment.newInstance(account, false /* expectingMessages */),
Mindy Pereirab199d172012-08-13 11:04:03 -07003830 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3831 }
3832 }
3833
3834 private WaitFragment getWaitFragment() {
3835 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3836 }
3837
3838 private int replaceFragment(Fragment fragment, int transition, String tag) {
3839 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003840 fragmentTransaction.setTransition(transition);
3841 fragmentTransaction.replace(R.id.wait, fragment, tag);
3842 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3843 return transactionId;
3844 }
3845
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003846 @Override
3847 public void onLoaderReset(Loader<Cursor> arg0) {
3848 // Do nothing.
3849 }
Jin Cao77b4c2c2014-05-20 13:55:53 -07003850
3851 /**
3852 * Background task to convert the message's html to Spanned.
3853 */
3854 private class HtmlToSpannedTask extends AsyncTask<String, Void, Spanned> {
3855
3856 @Override
3857 protected Spanned doInBackground(String... input) {
3858 return HtmlUtils.htmlToSpan(input[0]);
3859 }
3860
3861 @Override
3862 protected void onPostExecute(Spanned spanned) {
3863 mBodyView.removeTextChangedListener(ComposeActivity.this);
3864 mBodyView.setText(spanned);
3865 mTextChanged = false;
3866 mBodyView.addTextChangedListener(ComposeActivity.this);
3867 }
3868 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003869}